Skip to content

Commit

Permalink
fix Multiline /w SQL expr and test
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Oct 13, 2022
1 parent c7793dc commit a5a4226
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 90 deletions.
25 changes: 19 additions & 6 deletions demos/_demo-data/create-db.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ public function import(array $rowsMulti)
return parent::import(array_map(function (array $rows): array {
$rowsPrefixed = [];
foreach ($rows as $k => $v) {
$rowsPrefixed[$this->prefixFieldName($k)] = $v;
$field = $this->getField($this->prefixFieldName($k));
if (in_array($field->type, ['date', 'time', 'datetime'], true)) {
$v = new \DateTime($v . ' GMT');
}

$rowsPrefixed[$field->shortName] = $v;
}

return $rowsPrefixed;
Expand Down Expand Up @@ -1100,11 +1105,6 @@ public function import(array $rowsMulti)
['id' => 3, 'project_name' => 'Agile Data', 'project_code' => 'at03', 'description' => 'Agile Data implements an entirely new pattern for data abstraction, that is specifically designed for remote databases such as RDS, Cloud SQL, BigQuery and other distributed data storage architectures. It focuses on reducing number of requests your App have to send to the Database by using more sophisticated queries while also offering full Domain Model mapping and Database vendor abstraction.', 'client_name' => 'Agile Toolkit', 'client_address' => 'Some Street,' . "\n" . 'Garden City' . "\n" . 'UK', 'client_country_iso' => 'GB', 'is_commercial' => 0, 'currency' => 'GBP', 'is_completed' => 1, 'project_budget' => 12000, 'project_invoiced' => 0, 'project_paid' => 0, 'project_hour_cost' => 0, 'project_hours_est' => 300, 'project_hours_reported' => 394, 'project_expenses_est' => 600, 'project_expenses' => 430, 'project_mgmt_cost_pct' => 0.2, 'project_qa_cost_pct' => 0.3, 'start_date' => '2016-04-17', 'finish_date' => '2016-06-20', 'finish_time' => '03:04:00', 'created' => '2017-04-06 10:30:15', 'updated' => '2017-04-06 10:35:04'],
['id' => 4, 'project_name' => 'Agile UI', 'project_code' => 'at04', 'description' => 'Web UI Component library.', 'client_name' => 'Agile Toolkit', 'client_address' => 'Some Street,' . "\n" . 'Garden City' . "\n" . 'UK', 'client_country_iso' => 'GB', 'is_commercial' => 0, 'currency' => 'GBP', 'is_completed' => 0, 'project_budget' => 20000, 'project_invoiced' => 0, 'project_paid' => 0, 'project_hour_cost' => 0, 'project_hours_est' => 600, 'project_hours_reported' => 368, 'project_expenses_est' => 1200, 'project_expenses' => 0, 'project_mgmt_cost_pct' => 0.3, 'project_qa_cost_pct' => 0.4, 'start_date' => '2016-09-17', 'finish_date' => '', 'finish_time' => '', 'created' => '2017-04-06 10:30:15', 'updated' => '2017-04-06 10:35:04'],
];
foreach ($data as $rowIndex => $row) {
foreach (['start_date', 'finish_date', 'finish_time', 'created', 'updated'] as $k) {
$data[$rowIndex][$k] = new \DateTime($row[$k] . ' GMT');
}
}
$model->import($data);

$model = new ImportModelWithPrefixedFields($db, ['table' => 'product_category']);
Expand Down Expand Up @@ -1148,4 +1148,17 @@ public function import(array $rowsMulti)
['id' => 7, 'name' => 'Ice Cream', 'brand' => 'Milk Corp.', 'product_category_id' => 3, 'product_sub_category_id' => 8],
]);

$model = new ImportModelWithPrefixedFields($db, ['table' => 'multiline_item']);
$model->addField('item', ['type' => 'string']);
$model->addField('inv_date', ['type' => 'date']);
$model->addField('inv_time', ['type' => 'time']);
$model->addField('country_id', ['type' => 'bigint']);
$model->addField('qty', ['type' => 'integer']);
$model->addField('box', ['type' => 'integer']);
(new Migrator($model))->create();
$model->import([
['id' => 1, 'item' => 'Chocolate', 'inv_date' => '2020-02-20', 'inv_time' => '7:20', 'country_id' => 80, 'qty' => 7, 'box' => 5],
['id' => 2, 'item' => 'DAP delivery', 'inv_date' => '2020-02-01', 'inv_time' => '8:33', 'country_id' => 223, 'qty' => 2, 'box' => 100],
]);

echo 'import complete!' . "\n\n";
92 changes: 18 additions & 74 deletions demos/form-control/multiline.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Atk4\Ui\Demos;

use Atk4\Data\Model;
use Atk4\Data\Persistence;
use Atk4\Ui\Form;
use Atk4\Ui\Header;
use Atk4\Ui\JsExpression;
Expand All @@ -17,73 +15,15 @@

Header::addTo($app, ['Multiline form control', 'icon' => 'database', 'subHeader' => 'Collect/Edit multiple rows of table record.']);

/** @var Model $inventoryItemClass */
$inventoryItemClass = AnonymousClassNameCache::get_class(fn () => new class() extends Model {
public Persistence $countryPersistence;

protected function init(): void
{
parent::init();

$this->addField('item', [
'required' => true,
'default' => 'item',
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->addField('inv_date', [
'default' => new \DateTime(),
'type' => 'date',
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->addField('inv_time', [
'default' => new \DateTime(),
'type' => 'time',
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->hasOne('country', [
'model' => new Country($this->countryPersistence),
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 3]]],
]);
$this->addField('qty', [
'type' => 'integer',
'caption' => 'Qty / Box',
'default' => 1,
'required' => true,
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->addField('box', [
'type' => 'integer',
'caption' => '# of Boxes',
'default' => 1,
'required' => true,
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->addExpression('total', [
'expr' => function (Model $row) {
return $row->get('qty') * $row->get('box');
},
'type' => 'integer',
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']]],
]);
}
});

$inventory = new $inventoryItemClass(new Persistence\Array_(), ['countryPersistence' => $app->db]);

// Populate some data.
$total = 0;
for ($i = 1; $i < 3; ++$i) {
$entity = $inventory->createEntity();
$entity->set('id', $i);
$entity->set('inv_date', new \DateTime());
$entity->set('inv_time', new \DateTime());
$entity->set('item', 'item_' . $i);
$entity->set('country', random_int(1, 100));
$entity->set('qty', random_int(10, 100));
$entity->set('box', random_int(1, 10));
$total += $entity->get('qty') * $entity->get('box');
$entity->saveAndUnload();
}
$inventory = new MultilineItem($app->db);
$inventory->getField($inventory->fieldName()->item)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->inv_date)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->inv_date)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->country_id)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 3]];
$inventory->getField($inventory->fieldName()->qty)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->box)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->total_sql)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']];
$inventory->getField($inventory->fieldName()->total_php)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']];

$form = Form::addTo($app);

Expand All @@ -93,6 +33,10 @@ protected function init(): void
$multiline->setModel($inventory);

// Add total field.
$total = 0;
foreach ($inventory as $item) {
$total += $item->qty * $item->box;
}
$sublayout = $form->layout->addSubLayout([Form\Layout\Section\Columns::class]);
$sublayout->addColumn(12);
$column = $sublayout->addColumn(4);
Expand All @@ -102,19 +46,19 @@ protected function init(): void
$multiline->onLineChange(function (array $rows, Form $form) use ($controlTotal) {
$total = 0;
foreach ($rows as $row => $cols) {
$qty = $cols['qty'] ?? 0;
$box = $cols['box'] ?? 0;
$total += $qty * $box;
$total += $cols[MultilineItem::hinting()->fieldName()->qty] * $cols[MultilineItem::hinting()->fieldName()->box];
}

return $controlTotal->jsInput()->val($total);
}, ['qty', 'box']);
}, [$inventory->fieldName()->qty, $inventory->fieldName()->box]);

$multiline->jsAfterAdd = new JsFunction(['value'], [new JsExpression('console.log(value)')]);
$multiline->jsAfterDelete = new JsFunction(['value'], [new JsExpression('console.log(value)')]);

$form->onSubmit(function (Form $form) use ($multiline) {
$rows = $multiline->saveRows()->model->export();
$rows = $multiline->model->atomic(function () use ($multiline) {
return $multiline->saveRows()->model->export();
});

return new JsToast($form->getApp()->encodeJson(array_values($rows)));
});
41 changes: 41 additions & 0 deletions demos/init-db.php
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,44 @@ protected function init(): void
])->addTitle();
}
}

/**
* @property string $item @Atk4\Field()
* @property \DateTime $inv_date @Atk4\Field()
* @property \DateTime $inv_time @Atk4\Field()
* @property Country $country_id @Atk4\RefOne()
* @property int $qty @Atk4\Field()
* @property int $box @Atk4\Field()
* @property int $total_sql @Atk4\Field()
* @property int $total_php @Atk4\Field()
*/
class MultilineItem extends ModelWithPrefixedFields
{
public $table = 'multiline_item';

protected function init(): void
{
parent::init();

$this->addField($this->fieldName()->item, ['required' => true]);
$this->addField($this->fieldName()->inv_date, ['type' => 'date']);
$this->addField($this->fieldName()->inv_time, ['type' => 'time']);
$this->hasOne($this->fieldName()->country_id, [
'model' => [Country::class],
]);
$this->addField($this->fieldName()->qty, ['type' => 'integer', 'required' => true]);
$this->addField($this->fieldName()->box, ['type' => 'integer', 'required' => true]);
$this->addExpression($this->fieldName()->total_sql, [
'expr' => function (Model /* TODO self is not working bacause of clone in Multiline */ $row) {
return $row->expr('{' . $this->fieldName()->qty . '} * {' . $this->fieldName()->box . '}'); // @phpstan-ignore-line
},
'type' => 'integer',
]);
$this->addCalculatedField($this->fieldName()->total_php, [
'expr' => function (self $row) {
return $row->qty * $row->box;
},
'type' => 'integer',
]);
}
}
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ parameters:
-
path: 'src/Form/Control/Checkbox.php'
message: '~^Access to an undefined property Atk4\\Ui\\Jquery::\$checked\.$~'
-
path: 'src/Form/Control/Multiline.php'
message: '~^Call to an undefined method Atk4\\Data\\Model::expr\(\)\.$~'
-
path: 'src/JsVueService.php'
message: '~^Call to an undefined method Atk4\\Ui\\JsChain::createAtkVue\(\)\.$~'
Expand Down
35 changes: 25 additions & 10 deletions src/Form/Control/Multiline.php
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,8 @@ private function getCallbackValues(Model $model): array
*/
private function setDummyModelValue(Model $model): Model
{
$model = clone $model; // for clearing "required"

foreach ($this->fieldDefs as $def) {
$fieldName = $def['name'];
if ($fieldName === $model->idField) {
Expand All @@ -738,9 +740,10 @@ private function setDummyModelValue(Model $model): Model
$value = $this->getApp()->uiPersistence->typecastLoadField($field, $_POST[$fieldName] ?? null);
if ($field->isEditable()) {
try {
$field->required = false;
$model->set($fieldName, $value);
} catch (ValidationException $e) {
// Bypass validation at this point.
// bypass validation at this point
}
}
}
Expand All @@ -753,26 +756,36 @@ private function setDummyModelValue(Model $model): Model
*/
private function getExpressionValues(Model $model): array
{
$dummyFields = [];
$dummyFields = $this->getExpressionFields($model);
$formatValues = [];

foreach ($this->getExpressionFields($model) as $k => $field) {
foreach ($dummyFields as $k => $field) {
if (!is_callable($field->expr)) {
$dummyFields[$k]['name'] = $field->shortName;
$dummyFields[$k]['expr'] = $this->getDummyExpression($field, $model);
$dummyFields[$k]->expr = $this->getDummyExpression($field, $model);
}
}

if ($dummyFields !== []) {
$dummyModel = new Model($model->getPersistence(), ['table' => $model->table]);
foreach ($dummyFields as $field) {
$dummyModel->addExpression($field['name'], ['expr' => $field['expr'], 'type' => $model->getField($field['name'])->type]);
$dummyModel->removeField('id');
$dummyModel->idField = $model->idField;
foreach ($model->getFields() as $field) {
$dummyModel->addExpression($field->shortName, [
'expr' => isset($dummyFields[$field->shortName])
? $dummyFields[$field->shortName]->expr
: ($field->shortName === $dummyModel->idField
? '-1'
: $dummyModel->expr('[]', [$model->getPersistence()->typecastSaveField($field, $field->get($model))])),
'type' => $field->type,
'actual' => $field->actual,
]);
}
$values = $dummyModel->loadAny()->get();
$dummyModel->setLimit(1); // TODO must work with empty table, no table should be used
$values = $dummyModel->loadOne()->get();
unset($values[$model->idField]);

foreach ($values as $f => $value) {
if ($value) {
if (isset($dummyFields[$f])) {
$field = $model->getField($f);
$formatValues[$f] = $this->getApp()->uiPersistence->typecastSaveField($field, $value);
}
Expand All @@ -785,6 +798,8 @@ private function getExpressionValues(Model $model): array
/**
* Get all field expression in model, but only evaluate expression used in
* rowFields.
*
* @return array<string, SqlExpressionField>
*/
private function getExpressionFields(Model $model): array
{
Expand All @@ -794,7 +809,7 @@ private function getExpressionFields(Model $model): array
continue;
}

$fields[] = $field;
$fields[$field->shortName] = $field;
}

return $fields;
Expand Down
8 changes: 8 additions & 0 deletions tests-behat/multiline.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Feature: Multiline

Scenario:
Given I am on "form-control/multiline.php"
When I fill in "-atk_fp_multiline_item__qty" with "2"
When I fill in "-atk_fp_multiline_item__box" with "67"
Then I press button "Save"
Then Toast display should contain text '"atk_fp_multiline_item__qty": 2, "atk_fp_multiline_item__box": 67, "atk_fp_multiline_item__total_sql": 134 }'

0 comments on commit a5a4226

Please sign in to comment.