+ foreach ($sxThen->compute_date as $x) {
+ $this->add_THEN_ComputeDateClause($sqlBuilder, $databox, strtoupper(trim($x['direction'])), trim($x['field']), strtoupper(trim($x['delta'])), trim($x['computed']));
+ }
+
+ // action
+ foreach ($sxThen->coll as $x) {
+ $this->add_THEN_CollClause($sqlBuilder, $databox, trim($x['id']));
+ }
+
+ // action
+ foreach($sxThen->status as $x) {
+ $this->add_THEN_StatusClause($sqlBuilder, trim($x['mask']));
+ }
+
+ // action
+ foreach ($sxThen->set_field as $x) {
+ $this->add_THEN_SetFieldClause($sqlBuilder, $databox, trim($x['field']), (string)$x['value']);
+ }
+ }
+
+ private function add_IF_RecordTypeClause(SqlBuilder $sqlBuilder, databox $databox, string $type)
+ {
+ switch (strtoupper($type)) {
+ case 'RECORD':
+ $sqlBuilder->addWhere('parent_record_id!=record_id');
+ break;
+ case 'STORY':
+ $sqlBuilder->addWhere('parent_record_id=record_id');
+ break;
+ default:
+ throw new Exception(sprintf("bad record_type (%s)\n", $type));
+ }
+ }
+
+ private function add_IF_NumberClause(SqlBuilder $sqlBuilder, databox $databox, string $fieldName, string $operator, float $value)
+ {
+ if (!in_array($operator, array('<', '>', '<=', '>=', '=', '!='))) {
+ throw new Exception(sprintf("bad comparison operator (%s)\n", $operator));
+ }
+ switch ($fieldName) {
+ case "#filesize":
+ $ijoin = $sqlBuilder->incIjoin();
+ $sqlBuilder->addFrom(sprintf('INNER JOIN subdef AS p%d ON(p%d.record_id=record.record_id)', $ijoin, $ijoin));
+ $sqlBuilder->addWhere(sprintf(
+ 'p%d.name=%s',
+ $ijoin,
+ $databox->get_connection()->quote('document')
+ ));
+ $sqlBuilder->addWhere(sprintf(
+ 'p%d.size%s%s',
+ $ijoin,
+ $operator,
+ $value
+ ));
+ break;
+ default:
+ $field = $this->getByIdOrNameHelper->getField($databox, $fieldName);
+ if (!$field) {
+ throw new Exception(sprintf("unknown field (%s)\n", $fieldName));
+ }
+ $ijoin = $sqlBuilder->incIjoin();
+ $sqlBuilder->addFrom(sprintf('INNER JOIN metadatas AS p%d ON(p%d.record_id=record.record_id)', $ijoin, $ijoin));
+ $sqlBuilder->addWhere(sprintf(
+ 'p%d.meta_struct_id=%s',
+ $ijoin,
+ $databox->get_connection()->quote($field->get_id())
+ ));
+ $sqlBuilder->addWhere(sprintf(
+ 'CAST(p%d.value AS DECIMAL)%s%s',
+ $ijoin,
+ $operator,
+ $value
+ ));
+ break;
+ }
+ }
+
+ private function add_IF_FieldSetClause(SqlBuilder $sqlBuilder, databox $databox, string $fieldName)
+ {
+ $field = $this->getByIdOrNameHelper->getField($databox, $fieldName);
+ if (!$field) {
+ throw new Exception(sprintf("unknown field (%s)\n", $fieldName));
+ }
+ $ijoin = $sqlBuilder->incIjoin();
+ $sqlBuilder->addFrom(sprintf(
+ "INNER JOIN metadatas AS p%d ON(p%d.record_id=record.record_id)",
+ $ijoin,
+ $ijoin
+ ));
+ $sqlBuilder->addWhere(sprintf(
+ "p%d.meta_struct_id=%s",
+ $ijoin,
+ $databox->get_connection()->quote($field->get_id())
+ ));
+ }
+
+ private function add_IF_FieldUnsetClause(SqlBuilder $sqlBuilder, databox $databox, string $fieldName)
+ {
+ $field = $this->getByIdOrNameHelper->getField($databox, $fieldName);
+ if (!$field) {
+ throw new Exception(sprintf("unknown field (%s)\n", $fieldName));
+ }
+ $ijoin = $sqlBuilder->incIjoin();
+ $sqlBuilder->addFrom(sprintf(
+ 'LEFT JOIN metadatas AS p%d ON(record.record_id=p%d.record_id AND p%d.meta_struct_id=%s)',
+ $ijoin,
+ $ijoin,
+ $ijoin,
+ $databox->get_connection()->quote($field->get_id())
+ ));
+ $sqlBuilder->addWhere(sprintf(
+ "ISNULL(p%d.id)",
+ $ijoin
+ ));
+ }
+
+ private function add_IF_TextClause(SqlBuilder $sqlBuilder, databox $databox, string $fieldName, string $operator, string $value)
+ {
+ $field = $this->getByIdOrNameHelper->getField($databox, $fieldName);
+ if (!$field) {
+ throw new Exception(sprintf("unknown field (%s)\n", $fieldName));
+ }
+ if (!in_array($operator, array('<', '>', '<=', '>=', '=', '!='))) {
+ throw new Exception(sprintf("bad comparison operator (%s)\n", $operator));
+ }
+ $ijoin = $sqlBuilder->incIjoin();
+ $sqlBuilder->addFrom(sprintf("INNER JOIN metadatas AS p%d ON(p%d.record_id=record.record_id)", $ijoin, $ijoin));
+ $sqlBuilder->addWhere(sprintf(
+ "p%d.meta_struct_id=%s ",
+ $ijoin,
+ $databox->get_connection()->quote($field->get_id())
+ ));
+ $sqlBuilder->addWhere(sprintf(
+ "p%d.value%s%s",
+ $ijoin,
+ $operator,
+ $databox->get_connection()->quote($value)
+ ));
+ }
+
+ private function add_IF_DateClause(SqlBuilder $sqlBuilder, databox $databox, string $dir, string $fieldName, string $delta)
+ {
+ $unit = "DAY";
+ $matches = [];
+ $computedSql = null;
+ if($delta === "") {
+ $delta = 0;
+ }
+ else {
+ if (preg_match('/^([-+]?\d+)(\s+(HOUR|DAY|WEEK|MONTH|YEAR)S?)?$/', $delta, $matches) === 1) {
+ if (count($matches) === 4) {
+ $delta = (int)($matches[1]);
+ $unit = $matches[3];
+ }
+ else if (count($matches) === 2) {
+ $delta = (int)($matches[1]);
+ }
+ else {
+ throw new Exception(sprintf("bad delta (%s)\n", $delta));
+ }
+ }
+ else {
+ throw new Exception(sprintf("bad delta (%s)\n", $delta));
+ }
+ }
+
+ $dirop = "";
+ if (in_array($dir, array('BEFORE', 'AFTER'))) {
+ $dirop .= ($dir == 'BEFORE') ? '<' : '>=';
+ }
+ else {
+ // bad direction
+ throw new Exception(sprintf("bad direction (%s)\n", $dir));
+ }
+
+ switch ($fieldName) {
+ case '#moddate':
+ case '#credate':
+ $dbField = substr($fieldName, 1);
+ if($delta == 0) {
+ $computedSql = sprintf("record.%s AS DATETIME", $dbField);
+ }
+ else {
+ $computedSql = sprintf("(record.%s%sINTERVAL %d %s)", $dbField, $delta > 0 ? '+' : '-', abs($delta), $unit);
+ }
+ $sqlBuilder->addWhere(sprintf(
+ "NOW()%s%s",
+ $dirop,
+ $computedSql
+ ));
+ break;
+
+ default:
+ $field = $this->getByIdOrNameHelper->getField($databox, $fieldName);
+ if (!$field) {
+ throw new Exception(sprintf("unknown field (%s)\n", $fieldName));
+ }
+
+ $ijoin = $sqlBuilder->incIjoin();
+
+ // prevent malformed dates to act
+ $sqlBuilder->addWhere(sprintf(
+ "!ISNULL(CAST(p%d.value AS DATETIME))",
+ $ijoin
+ ));
+
+ if($delta == 0) {
+ $computedSql = sprintf("CAST(p%d.value AS DATETIME)", $ijoin);
+ }
+ else {
+ $computedSql = sprintf("(p%d.value%sINTERVAL %d %s)", $ijoin, $delta > 0 ? '+' : '-', abs($delta), $unit);
+ }
+
+ $sqlBuilder->addFrom(sprintf(
+ 'INNER JOIN metadatas AS p%d ON(p%d.record_id=record.record_id)',
+ $ijoin,
+ $ijoin
+ ));
+ $sqlBuilder->addWhere(sprintf(
+ "p%d.meta_struct_id=%s",
+ $ijoin,
+ $databox->get_connection()->quote($field->get_id())
+ ));
+ $sqlBuilder->addWhere(sprintf(
+ "NOW()%s%s",
+ $dirop,
+ $computedSql
+ ));
+
+ break;
+ }
+ }
+
+ private function add_IF_StatusClause(SqlBuilder $sqlBuilder, string $mask)
+ {
+ $mask = preg_replace('/[^0-1]/', 'x', $mask);
+ $mx = str_replace(' ', '0', ltrim(str_replace(['0', 'x'], [' ', ' '], $mask)));
+ $ma = str_replace(' ', '0', ltrim(str_replace(['x', '0'], [' ', '1'], $mask)));
+
+ if ($mx && $ma) {
+ $sqlBuilder->addWhere(sprintf("((status ^ 0b%s) & 0b%s)=0", $mx, $ma));
+ }
+ elseif ($mx) {
+ $sqlBuilder->addWhere(sprintf("(status ^ 0b%s)=0", $mx));
+ }
+ elseif ($ma) {
+ $sqlBuilder->addWhere(sprintf("(status & 0b%s)=0", $ma));
+ }
+ }
+
+ /**
+ * add coll clause to the query builder
+ *
+ * @param SqlBuilder $sqlBuilder
+ * @param databox $databox
+ * @param string $collList
+ * @param string $operator
+ * @return void
+ * @throws Exception
+ */
+ private function add_IF_CollClause(SqlBuilder $sqlBuilder, databox $databox, string $collList, string $operator)
+ {
+ if(!in_array($operator, ['=', '!='])) {
+ // bad operator
+ throw new Exception(sprintf("bad comparison operator (%s)\n", $operator));
+ }
+ $tcoll = explode(',', $collList);
+ foreach ($tcoll as $i => $c) {
+ $coll = $this->getByIdOrNameHelper->getCollection($databox->get_sbas_id(), $c);
+ if(!$coll) {
+ throw new Exception(sprintf("unknown collection %s", $c));
+ }
+ $tcoll[$i] = $coll->get_coll_id();
+ }
+ if(count($tcoll) > 0) {
+ if ($operator == '=') {
+ if (count($tcoll) == 1) {
+ $sqlBuilder->addWhere('coll_id=' . $tcoll[0]);
+ }
+ else {
+ $sqlBuilder->addWhere('coll_id IN(' . implode(',', $tcoll) . ')');
+ }
+ }
+ else {
+ if (count($tcoll) == 1) {
+ $sqlBuilder->addWhere('coll_id!=' . $tcoll[0]);
+ }
+ else {
+ $sqlBuilder->addWhere('coll_id NOT IN(' . implode(',', $tcoll) . ')');
+ }
+ }
+ }
+ }
+
+ private function checkComputedRefKey(string $s)
+ {
+ if($s === '') {
+ throw new Exception(sprintf("mssing compute reference\n"));
+ }
+ $_s = strtolower($s);
+ foreach(str_split($_s) as $i => $c) {
+ if(!($c=='_' || ($c >= 'a' && $c <= 'z') || ($i>0 && $c >= '0' && $c <= '9'))) {
+ throw new Exception(sprintf("bad compute reference (%s)\n", $s));
+ }
+ }
+ }
+
+ private function add_THEN_ComputeDateClause(SqlBuilder $sqlBuilder, databox $databox, string $dir, string $fieldName, string $delta, string $computedRefKey)
+ {
+ $this->checkComputedRefKey($computedRefKey);
+
+ $unit = "DAY";
+ $matches = [];
+ $computedSql = null;
+ if($delta === "") {
+ $delta = 0;
+ }
+ else {
+ if (preg_match('/^([-+]?\d+)(\s+(HOUR|DAY|WEEK|MONTH|YEAR)S?)?$/', $delta, $matches) === 1) {
+ if (count($matches) === 4) {
+ $delta = (int)($matches[1]);
+ $unit = $matches[3];
+ }
+ else if (count($matches) === 2) {
+ $delta = (int)($matches[1]);
+ }
+ else {
+ throw new Exception(sprintf("bad delta (%s)\n", $delta));
+ }
+ }
+ else {
+ throw new Exception(sprintf("bad delta (%s)\n", $delta));
+ }
+ }
+
+ $dirop = "";
+ if (in_array($dir, array('BEFORE', 'AFTER'))) {
+ $dirop .= ($dir == 'BEFORE') ? '<' : '>=';
+ }
+ else {
+ // bad direction
+ throw new Exception(sprintf("bad direction (%s)\n", $dir));
+ }
+
+ switch ($fieldName) {
+ case '#moddate':
+ case '#credate':
+ $dbField = substr($fieldName, 1);
+ if($delta == 0) {
+ $computedSql = sprintf("record.%s AS DATETIME", $dbField);
+ }
+ else {
+ $computedSql = sprintf("(record.%s%sINTERVAL %d %s)", $dbField, $delta > 0 ? '+' : '-', abs($delta), $unit);
+ }
+ break;
+
+ default:
+ $field = $this->getByIdOrNameHelper->getField($databox, $fieldName);
+ if (!$field) {
+ throw new Exception(sprintf("unknown field (%s)\n", $fieldName));
+ }
+
+ $ijoin = $sqlBuilder->incIjoin();
+
+ // prevent malformed dates to act
+ $sqlBuilder->addWhere(sprintf(
+ "!ISNULL(CAST(p%d.value AS DATETIME))",
+ $ijoin
+ ));
+
+ if($delta == 0) {
+ $computedSql = sprintf("CAST(p%d.value AS DATETIME)", $ijoin);
+ }
+ else {
+ $computedSql = sprintf("(p%d.value%sINTERVAL %d %s)", $ijoin, $delta > 0 ? '+' : '-', abs($delta), $unit);
+ }
+
+ $sqlBuilder->addFrom(sprintf(
+ 'INNER JOIN metadatas AS p%d ON(p%d.record_id=record.record_id)',
+ $ijoin,
+ $ijoin
+ ));
+
+ break;
+ }
+
+ if($computedRefKey && $computedSql !== null) {
+ $sqlBuilder->addSelect(sprintf("%s AS %s",
+ $computedSql,
+ $databox->get_connection()->quoteIdentifier($computedRefKey)
+ ));
+ $sqlBuilder->addReference($computedRefKey, $computedSql);
+ }
+ }
+
+ /**
+ * add THEN.coll clause to the query builder (negated)
+ *
+ * @param SqlBuilder $sqlBuilder
+ * @param databox $databox
+ * @param string $collId
+ * @return void
+ * @throws Exception
+ */
+ private function add_THEN_CollClause(SqlBuilder $sqlBuilder, databox $databox, string $collId)
+ {
+ $coll = $this->getByIdOrNameHelper->getCollection($databox->get_sbas_id(), $collId);
+ if(!$coll) {
+ throw new Exception(sprintf("unknown collection %s", $collId));
+ }
+ $sqlBuilder->addNegWhere('coll_id=' . $coll->get_coll_id());
+ }
+
+ private function add_THEN_StatusClause(SqlBuilder $sqlBuilder, string $mask)
+ {
+ $mask = preg_replace('/[^0-1]/', 'x', $mask);
+ $mx = str_replace(' ', '0', ltrim(str_replace(['0', 'x'], [' ', ' '], $mask)));
+ $ma = str_replace(' ', '0', ltrim(str_replace(['x', '0'], [' ', '1'], $mask)));
+
+ if ($mx && $ma) {
+ $sqlBuilder->addNegWhere(sprintf("((status ^ 0b%s) & 0b%s)=0", $mx, $ma));
+ }
+ elseif ($mx) {
+ $sqlBuilder->addNegWhere(sprintf("(status ^ 0b%s)=0", $mx));
+ }
+ elseif ($ma) {
+ $sqlBuilder->addNegWhere(sprintf("(status & 0b%s)=0", $ma));
+ }
+ }
+
+ private function add_THEN_SetFieldClause(SqlBuilder $sqlBuilder, databox $databox, string $fieldName, string $value)
+ {
+ $field = $this->getByIdOrNameHelper->getField($databox, $fieldName);
+ if (!$field) {
+ throw new Exception(sprintf("unknown field (%s)\n", $fieldName));
+ }
+
+ if(substr($value, 0, 1) === '$') {
+ // reference to a previously computed expression (only THEN.compute_date does that)
+ $k = substr($value, 1);
+ $this->checkComputedRefKey($k);
+
+ if(!($value = $sqlBuilder->getReference($k))) {
+ throw new Exception(sprintf("unknown reference (\$%s)\n", $k));
+ }
+ }
+ else {
+ // constant
+ $value = $databox->get_connection()->quote($value);
+ }
+
+ $ijoin = $sqlBuilder->incIjoin();
+ $sqlBuilder->addFrom(sprintf(
+ "LEFT JOIN metadatas AS p%d ON(p%d.record_id=record.record_id AND p%d.meta_struct_id=%s AND p%d.value=%s)",
+ $ijoin,
+ $ijoin,
+ $ijoin,
+ $databox->get_connection()->quote($field->get_id()),
+ $ijoin,
+ $value
+ ));
+ $sqlBuilder->addWhere(sprintf(
+ "ISNULL(p%d.id)",
+ $ijoin
+ ));
+ }
+
+}
diff --git a/lib/Alchemy/Phrasea/WorkerManager/Worker/RecordsActionsWorker/SqlBuilder.php b/lib/Alchemy/Phrasea/WorkerManager/Worker/RecordsActionsWorker/SqlBuilder.php
new file mode 100644
index 0000000000..75aa5ded20
--- /dev/null
+++ b/lib/Alchemy/Phrasea/WorkerManager/Worker/RecordsActionsWorker/SqlBuilder.php
@@ -0,0 +1,122 @@
+databox = $databox;
+ }
+
+ public function addReference($key, $value)
+ {
+ $this->references[$key] = $value;
+ }
+
+ /**
+ * @param $key
+ * @return string|null
+ */
+ public function getReference($key)
+ {
+ return $this->references[$key] ?: null;
+ }
+
+ public function incIjoin(): int
+ {
+ $this->ijoin++;
+ return $this->ijoin;
+ }
+
+ public function addSelect(string $s): self
+ {
+ $this->selectClauses[] = $s;
+
+ return $this;
+ }
+
+ public function addWhere(string $clause): self
+ {
+ $this->whereClauses[] = $clause;
+ return $this;
+ }
+
+ public function addNegWhere(string $clause): self
+ {
+ $this->negWhereClauses[] = $clause;
+ return $this;
+ }
+
+ public function addFrom(string $table): self
+ {
+ $this->fromClauses[] = $table;
+
+ return $this;
+ }
+
+ public function getWhereSql()
+ {
+ $w = $this->whereClauses;
+
+ if(!empty($this->negWhereClauses)) {
+ if(count($this->negWhereClauses) == 1) {
+ $neg = $this->negWhereClauses[0];
+ }
+ else {
+ $neg = "(" . join(") AND (", $this->negWhereClauses) . ")";
+ }
+ $w[] = "NOT(" . $neg . ")";
+ }
+
+ if(empty($w)) {
+ return "";
+ }
+ if(count($w) === 1) {
+ return $w[0];
+ }
+ return "(" . join(") AND (", $w) . ")";
+ }
+
+ public function getSql(): string
+ {
+ $sql = "";
+
+ if(!empty($this->selectClauses)) {
+ $sql .= $sql ? ' ' : '';
+ $sql .= sprintf("SELECT %s",
+ join(', ', $this->selectClauses)
+ );
+ }
+
+ if(!empty($this->fromClauses)) {
+ $sql .= $sql ? ' ' : '';
+ $sql .= sprintf("FROM %s",
+ join(' ', $this->fromClauses)
+ );
+ }
+
+ if(!empty($this->whereClauses)) {
+ $sql .= $sql ? ' ' : '';
+ $sql .= sprintf("WHERE %s", $this->getWhereSql());
+ }
+
+ return $sql;
+ }
+}
diff --git a/lib/conf.d/data_templates/DublinCore.xml b/lib/conf.d/data_templates/DublinCore.xml
index 8dafea842c..ed4b57fbae 100644
--- a/lib/conf.d/data_templates/DublinCore.xml
+++ b/lib/conf.d/data_templates/DublinCore.xml
@@ -209,6 +209,7 @@
+
@@ -232,14 +233,24 @@
- Nicht gefüllt
- Caption not filled
- Média non renseigné
-
- Gefüllt
- Caption filled
- Média renseigné
-
+ Nicht gefüllt
+ Caption not filled
+ Média non renseigné
+
+ Gefüllt
+ Caption filled
+ Média renseigné
+
+
+
+
+ Rights not expired
+ Droits non expirés
+
+
+ Rights expired
+ Droits expirés
+
diff --git a/templates/web/admin/worker-manager/worker_records_actions.html.twig b/templates/web/admin/worker-manager/worker_records_actions.html.twig
index 81700e3aca..708bb923df 100644
--- a/templates/web/admin/worker-manager/worker_records_actions.html.twig
+++ b/templates/web/admin/worker-manager/worker_records_actions.html.twig
@@ -62,6 +62,12 @@
{{ form_row(form.xmlSetting, {'attr': {'style': 'width:99%;height:250px;'}}) }}
+{#
+
+#}
+
{{ 'Refresh graphic view' | trans }}
@@ -166,9 +172,20 @@
, dataType:'json'
, type:"POST"
, async:true
+ , error: function(data) {
+ $("#sqla").html(data.statusText);
+ }
, success:function(data) {
+ if(data.error) {
+ $("#sqla").text(data.error);
+ return;
+ }
t = "";
for (i in data.tasks) {
+ // o = $("
")
+ // .append($(" X "))
+ // ;
+ //$("#sqla").append()
t += " ";
if (data.tasks[i].active) {
t += " X ";
diff --git a/templates/web/admin/worker-manager/worker_records_actions.md b/templates/web/admin/worker-manager/worker_records_actions.md
new file mode 100644
index 0000000000..824611e761
--- /dev/null
+++ b/templates/web/admin/worker-manager/worker_records_actions.md
@@ -0,0 +1,183 @@
+Act on records matching a list of criteria.
+
+# Changelog / bc break:
+
+`from` group is renamed `if`
+
+`to` group is renamed `then`
+
+`trash` action is removed ; use `update` with `then` ` `
+
+`type` clause (used for record|story) is renamed `record_type`
+
+# Doc:
+
+The worker will play __tasks__, each task must specify the databox to act on
+and the action to do on selected records, e.g.:
+
+```xml
+
+
+
+
+
+
+ ...
+
+
+ ...
+
+
+
+
+ ...
+
+
+
+```
+
+A task marked as `active="0"` is ignored.
+
+A task marked as `dry="1"` is executed, but the actions on record are not executed.
+This allows to check sql and actions (log) whithout altering data.
+
+The databox to act on is `databoxId` can be specified by __Id__ or __name__.
+
+The "Test the rules" button will display the select sql and the number or record selected for action.
+
+`action` can be one of:
+
+#### update
+
+update action can move record to another collection and / or change status-bits.
+
+#### delete
+
+delete the records
+
+## `if` clauses to select records to act on:
+
+__All__ clauses must match for the record to be selected.
+
+Some clauses (eg. coll ids) can be a list, allowing to define sort of "or" clauses.
+
+To set "or" clauses that can't be expressed with the rules syntax, one must define
+many tasks.
+
+### select on type of record.
+
+```xml
+
+```
+
+```xml
+
+```
+
+### select on collection
+`id` is a list of collection id ("base_id" API side) or collection name.
+
+```xml
+
+
+```
+
+```xml
+
+
+```
+_nb:_ Since a record belongs to only one collection, specifiying many `coll` clauses has no sense.
+
+### select on set / unset field.
+
+```xml
+
+```
+
+```xml
+
+```
+
+### select on text values.
+
+```xml
+
+```
+
+```xml
+
+```
+
+_warning:_ comparison is made using __alphabetic__ value.
+Using `< > <= >=` compare operators
+__is possible__ but may have unexpected result, depending on case, accents, signs etc.
+
+### select on numeric values.
+
+```xml
+
+```
+
+possible compare oerators are `= != < > <= >=`
+
+pseudo-field `#filesize` can be used to test document file size.
+```xml
+
+
+```
+
+### select on date values
+
+```xml
+
+
+```
+
+`direction`: "after" or "before"
+
+`delta`: +/- N ("hour" or "day" or "week" or "month" or "year")
+
+pseudo-fields `#credate` and `#moddate` can be used to test creation date
+and last modification date of records.
+
+
+### select on status-bits
+
+```xml
+
+
+```
+
+## `then` actions (for task with "update" action)
+
+### change collection ; change status-bits ; set a field value
+```xml
+
+
+
+
+
+
+
+
+```
+
+### set a field to a computed value
+
+The `compute_date` parameters are the same as `date` clause. The result is then referenced
+by the `computed` reference.
+
+The computed value can then be used as a value for a `set_field` action.
+
+```xml
+
+
+
+
+```
+
+_nb_: For now this only allow to compute from / to a __datetime__ value. Computing from a "non-date" value
+has unpredictable result.
+
+
+