Commit b31fb9eb authored by Carsten  Rose's avatar Carsten Rose
Browse files

Dynamic Update implemented

load.php: implemented
FillStoreForm.php: implemented
Store.php: phpunit test complains about 'store already filled'. Option set to explicitly allow rewrite.
AbstractBuildForm.php: Added new mode 'FORM_UPDATE'. Elements additionaly create json code. 'data-load' attribute will be added to form elements, if 'dynamicUpdate=yes'
  elements(): added call by reference parameter $json, to return the generated json code.
BodyTextParse.php: added 'r =' as a new 'start new line' indicator. This was necessary at least for phpunit tests to run.
BuildFormBootstrap.php: buildPill() passes json data structure.
BuildFormPlain, BuildFormTable.php: doSubrecords()  passes json data structure.
Constants.php: New FORM_UPDATE, SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER, ERROR_FORM_NOT_FOUND, API_FORM_UPDATE
Evaluate.php: Exception text enhanced.
QuickFormQuery.php: FillStoreForm.php included. Automatic detection of FORM_LOAD and FORM_SAVE removed. Instead the mode are given explicitly. mode=FORM_UPDATE implemented.
Save.php: added TODOs in code.
formEditor.sql: reformat code. Add 'FormElement.dynamicUpdate'. 'FormElemente.checkType': 'number' replaced by 'digit'. Added 'alnumx', 'digit'. Form 'form', 'formElement': output of 'title' replaced by 'name' - outputting 'title' confuses the user (tries to show records which do fit to the formEditor) and might produce recursion in evaluation (did not understand why, but happens). FormEditor: implemented 'dynamicUpdate', escpecially the 'type' select list will be adjusted dynamically.
parent e6d18f0a
......@@ -6,3 +6,85 @@
* Time: 6:17 PM
*/
namespace qfq;
use qfq;
require_once(__DIR__ . '/../qfq/QuickFormQuery.php');
require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
/**
* Return JSON encoded answer
*
* status: success|error
* message: <message>
* redirect: client|url|no
* redirect-url: <url>
* field-name: <field name>
* field-message: <message>
* form-data: [ fieldname1 => value1, fieldname2 => value2, ... ]
* form-control: [ fieldname1 => status1, fieldname2 => status2, ... ] status: show|hide, enabled|disabled, readonly|readwrite
*
* Description:
*
* Save successfull. Button 'close', 'new'. Form.forward: 'auto'. Client logic decide to redirect or not. Show message if no redirect.
* status = 'success'
* message = <message>
* redirect = 'client'
*
* Save successfull. Button 'close': Form.forward: 'page'. Client redirect to url.
* status = 'success'
* message = <message>
* redirect = 'url'
* redirect-url = <URL>
*
* Save failed: Button: any. Show message and set 'alert' on _optional_ specified form element. Bring 'pill' of specified form element to front.
* status = 'error'
* message = <message>
* redirect = 'no'
* Optional:
* field-name = <field name>
* field-message = <message appearing as tooltip (or similar) near the form element>
*/
$answer = array();
$answer[API_REDIRECT] = API_ANSWER_REDIRECT_NO;
$answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
$answer[API_MESSAGE] = '';
try {
$qfq = new \qfq\QuickFormQuery(['bodytext' => ""]);
$data = $qfq->updateForm();
// $answer[API_REDIRECT] = $qfq->getForwardMode($answer[API_REDIRECT_URL]);
$answer[API_STATUS] = API_ANSWER_STATUS_SUCCESS;
$answer[API_MESSAGE] = 'load: success';
$answer[API_FORM_UPDATE] = $data;
} catch (qfq\UserException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
$val = Store::getVar(SYSTEM_FORM_ELEMENT, STORE_SYSTEM);
if ($val !== false)
$answer[API_FIELD_NAME] = $val;
$val = Store::getVar(SYSTEM_FORM_ELEMENT_MESSAGE, STORE_SYSTEM);
if ($val !== false)
$answer[API_FIELD_MESSAGE] = $val;
} catch (qfq\CodeException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (qfq\DbException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (\Exception $e) {
$answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage();
}
header("Content-Type: application/json");
echo json_encode($answer);
This diff is collapsed.
......@@ -61,7 +61,7 @@ class BodytextParser {
$full = '';
foreach ($bodytextArray as $row) {
// Valid 'new line' starts indicators: form, <level>, <level.sublevel>, <level>.<keyword>, {, <level> {, }
if ((1 === preg_match('/^\s*(\d*(\.)?)*\s*(head|althead|tail|sql|rbeg|rend|renr|rsep|fbeg|fend|fsep|form) *=/', $row))
if ((1 === preg_match('/^\s*(\d*(\.)?)*\s*(head|althead|tail|sql|rbeg|rend|renr|rsep|fbeg|fend|fsep|form|debugShowStack|r) *=/', $row))
|| (1 === preg_match('/^\s*(\d*(\.)?)*\s*({|})\s*/', $row))
|| (1 === preg_match('/^\s*(\d+(\.)?)+/', $row))
) {
......@@ -74,6 +74,7 @@ class BodytextParser {
$full = $row;
} else {
// continue row: concat
$full .= ' ' . $row;
}
......@@ -86,6 +87,11 @@ class BodytextParser {
return implode(PHP_EOL, $data);
}
/**
* @param $bodytext
* @return mixed|string
* @throws UserException
*/
private function unNest($bodytext) {
// Replace '\{' | '\}' by internal token. All remaining '}' | '{' means: 'nested'
$bodytext = str_replace('\{', '#&[_#', $bodytext);
......
......@@ -293,7 +293,9 @@ BUTTON;
tabsId: '$tabId',
formId: '$formId',
submitTo: 'typo3conf/ext/qfq/qfq/api/save.php',
deleteUrl: '$deleteUrl'
deleteUrl: '$deleteUrl',
refreshUrl: "typo3conf/ext/qfq/qfq/api/load.php"
});
var qfqRecordList = new QfqNS.QfqRecordList('typo3conf/ext/qfq/qfq/api/delete.php');
......@@ -311,7 +313,7 @@ EOF;
* @param $value
* @return mixed
*/
public function buildPill(array $formElement, $htmlFormElementId, $value) {
public function buildPill(array $formElement, $htmlFormElementId, $value, &$json) {
$html = '';
// save parent processed FE's
$tmpStore = $this->feSpecNative;
......@@ -320,7 +322,7 @@ EOF;
$sql = SQL_FORM_ELEMENT_SPECIFIC_CONTAINER;
$this->feSpecNative = $this->db->sql($sql, ROW_REGULAR, ['yes', $this->formSpec["id"], 'native,container', $formElement['id']]);
HelperFormElement::explodeParameterInArrayElements($this->feSpecNative);
$html = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD);
$html = $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_NATIVE_SUBRECORD, 0, $json);
// restore parent processed FE's
$this->feSpecNative = $tmpStore;
......
......@@ -52,7 +52,9 @@ class BuildFormPlain extends AbstractBuildForm {
* @return string
*/
public function doSubrecords() {
return $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_SUBRECORD);
$json = array();
//TODO: $json is not returned - which is wrong. In this case, dynamic update won't work for subrecords
return $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_SUBRECORD, $json);
}
/**
......
......@@ -56,7 +56,9 @@ class BuildFormTable extends AbstractBuildForm {
* @return string
*/
public function doSubrecords() {
return $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_SUBRECORD);
//TODO: $json is not returned - which is wrong. In this case, dynamic update won't work for subrecords
$json = array();
return $this->elements($this->store->getVar(SIP_RECORD_ID, STORE_SIP), FORM_ELEMENTS_SUBRECORD, $json);
}
/**
......
......@@ -18,6 +18,7 @@ const SESSION_LIFETIME_SECONDS = 86400;
const FORM_LOAD = 'form_load';
const FORM_SAVE = 'form_save';
const FORM_UPDATE = 'form_update';
const FORM_PERMISSION_SIP = 'sip';
const FORM_PERMISSION_LOGGED_IN = 'logged_id';
const FORM_PERMISSION_LOGGED_OUT = 'logged_out';
......@@ -37,6 +38,7 @@ const RETURN_SIP = 'return_sip';
const SQL_FORM_ELEMENT_SPECIFIC_CONTAINER = "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.feIdContainer = ? AND fe.enabled='yes' ORDER BY fe.ord, fe.id";
const SQL_FORM_ELEMENT_ALL_CONTAINER = "SELECT *, ? AS 'nestedInFieldSet' FROM FormElement AS fe WHERE fe.formId = ? AND fe.deleted = 'no' AND FIND_IN_SET(fe.class, ? ) AND fe.enabled='yes' ORDER BY fe.ord, fe.id";
const SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER = "SELECT fe.id, fe.name, fe.type, fe.checkType, fe.checkPattern FROM FormElement AS fe, Form AS f WHERE f.name = ? AND f.id = fe.formId AND fe.deleted = 'no' AND fe.class = 'native' AND fe.enabled='yes' ORDER BY fe.ord, fe.id";
// SANITIZE Classifier
const SANITIZE_ALLOW_ALNUMX = "alnumx";
......@@ -114,6 +116,7 @@ const ERROR_SET_STORE_ZERO = 1048;
const ERROR_MISSING_FORMELEMENT = 1049;
const ERROR_INVALID_OR_MISSING_PARAMETER = 1050;
const ERROR_UNKNOWN_SQL_LOG_MODE = 1051;
const ERROR_FORM_NOT_FOUND = 1052;
// Store
const ERROR_STORE_VALUE_ALREADY_CODPIED = 1100;
......@@ -257,6 +260,7 @@ const API_REDIRECT = 'redirect';
const API_REDIRECT_URL = 'redirect-url';
const API_FIELD_NAME = 'field-name';
const API_FIELD_MESSAGE = 'field-message';
const API_FORM_UPDATE = 'form-update';
const API_ANSWER_STATUS_SUCCESS = 'success';
const API_ANSWER_STATUS_ERROR = 'error';
......@@ -279,6 +283,7 @@ const SUBRECORD_COLUMN_WIDTH = 20;
const FORM_ELEMENTS_NATIVE = 'native';
const FORM_ELEMENTS_SUBRECORD = 'subrecord';
const FORM_ELEMENTS_NATIVE_SUBRECORD = 'native_subrecord';
//const FORM_ELEMENTS_DYNAMIC_UPDATE = 'native_dynamic_update';
const SUBRECORD_NEW = SYMBOL_NEW;
const SUBRECORD_EDIT = SYMBOL_EDIT;
const SUBRECORD_DELETE = SYMBOL_DELETE;
......
......@@ -73,7 +73,7 @@ class Evaluate {
$flagTokenReplaced = false;
if ($recursion > 4) {
throw new qfq\UserException("Recursion too deep: $line", ERROR_RECURSION_TOO_DEEP);
throw new qfq\UserException("Recursion too deep ($recursion). Line: $line", ERROR_RECURSION_TOO_DEEP);
}
$result = $line;
......@@ -133,7 +133,9 @@ class Evaluate {
* If neither a) or b) match, return the token itself, surrounded by single ticks, to emphase that substition failed.
*
* @param $token
* @param string $foundInStore
* @return array|mixed|null|string
* @throws CodeException
* @throws DbException
*/
public function substitute($token, &$foundInStore = '') {
......
......@@ -24,6 +24,7 @@ use qfq;
require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/store/FillStoreForm.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/Save.php');
require_once(__DIR__ . '/../qfq/helper/KeyValueStringParser.php');
......@@ -156,7 +157,7 @@ class QuickFormQuery {
*/
public function process() {
$html = $this->doForm();
$html = $this->doForm(FORM_LOAD);
$html .= $this->doReport();
$class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
......@@ -167,18 +168,21 @@ class QuickFormQuery {
}
/**
* Process form (load or save) if a formname is found.
* Process form. There
*
* @return string
* @throws CodeException
* @throws UserException
*/
private function doForm() {
$html = '';
private function doForm($mode) {
$data = '';
$foundInStore = '';
// Form action: load or save?
$mode = isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' ? FORM_SAVE : FORM_LOAD;
// Fill STORE_FORM
if ($mode === FORM_UPDATE || $mode === FORM_SAVE) {
$fillStoreForm = new FillStoreForm();
$fillStoreForm->process();
}
$formName = $this->loadFormSpecification($mode, $foundInStore);
if ($formName === false)
......@@ -192,6 +196,7 @@ class QuickFormQuery {
switch ($mode) {
case FORM_LOAD:
case FORM_UPDATE:
switch ($this->formSpec['render']) {
case 'plain':
$build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative);
......@@ -205,30 +210,36 @@ class QuickFormQuery {
default:
throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
}
$html = $build->process();
$data = $build->process($mode);
break;
case FORM_SAVE:
$save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative);
$save->process();
break;
default:
throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
}
return $html;
return $data;
}
/**
* Load form. Evaluates form. Load FormElements.
*
* After processing:
* Loaded Form is in $this->formSpec
* Loaded 'action' FormElements are in $this->feSpecAction
* Loaded 'native' FormElements are in $this->feSpecNative
*
* @param string $mode FORM_LOAD|FORM_SAVE
* @return string|bool if found the formName, else 'false'.
* @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
* @param string $foundInStore
* @return bool|string if found the formName, else 'false'.
* @throws CodeException
* @throws DbException
* @throws UserException. F.e.: if the form is loaded via Client, but permission is set to SIP.
* @throws UserException
*/
private function loadFormSpecification($mode, &$foundInStore = '') {
......@@ -245,6 +256,10 @@ class QuickFormQuery {
$this->formSpec = $this->eval->parseArray($form);
HelperFormElement::explodeParameter($this->formSpec);
# Set defaults:
if (!isset($this->formSpec['class']))
$this->formSpec['class'] = '';
// Clear
$this->store->setVar(SYSTEM_FORM_ELEMENT, '', STORE_SYSTEM);
......@@ -262,6 +277,7 @@ class QuickFormQuery {
break;
case FORM_SAVE:
case FORM_UPDATE:
$this->feSpecNative = $this->db->sql(SQL_FORM_ELEMENT_ALL_CONTAINER, ROW_REGULAR,
['no', $this->formSpec["id"], 'native']);
break;
......@@ -289,15 +305,27 @@ class QuickFormQuery {
* Specified in SIP
*
*
* @param $mode
* @param string $mode FORM_LOAD|FORM_SAVE|FORM_UPDATE
* @param string $foundInStore
* @return bool|string Formname (Form.name) or FALSE, if no formname found.
* @return array|bool|mixed|null|string Formname (Form.name) or FALSE, if no formname found.
* @throws CodeException
* @throws UserException
*/
private function getFormName($mode, &$foundInStore = '') {
$dummy = array();
$store = ($mode === FORM_SAVE) ? STORE_SIP : STORE_TYPO3;
switch ($mode) {
case FORM_LOAD:
$store = STORE_TYPO3;
break;
case FORM_SAVE:
case FORM_UPDATE:
$store = STORE_SIP;
break;
default:
throw new CodeException("Unknown mode: $mode.", ERROR_UNKNOWN_MODE);
}
$storeFormName = $this->store->getVar(SIP_FORM, $store, '', $foundInStore);
$formName = $this->eval->parse($storeFormName, 0, $dummy, $foundInStore);
......@@ -317,10 +345,11 @@ class QuickFormQuery {
/**
* Check if loading of the given form is permitted. If not, throw an exception.
*
* @param $foundInStore
* @return bool - 'true' if SIP exists, else 'false'
* @param $formNameFoundInStore
* @return bool 'true' if SIP exists, else 'false'
* @throws CodeException
* @throws UserException
* @internal param $foundInStore
*/
private function validateForm($formNameFoundInStore) {
......@@ -393,11 +422,24 @@ class QuickFormQuery {
*/
public function saveForm() {
$html = $this->doForm();
$html = $this->doForm(FORM_SAVE);
return $html;
}
/**
* Update FormElements and form values. Receives the current form values via POST.
*
* @return array
* @throws CodeException
*/
public function updateForm() {
$json = $this->doForm(FORM_UPDATE);
return $json;
}
/**
* Delete a record (tablename and recordid are given) or process a 'delete form'
*
......
......@@ -73,6 +73,8 @@ class Save {
* @throws UserException
*/
public function elements($recordId) {
//TODO: Umstellen auf Nutzung der Klasse FillStoreForm.
$html = '';
$newValues = array();
......@@ -153,6 +155,8 @@ class Save {
* @throws UserException
*/
public function validateValue($formElement, $value) {
//TODO: Mit Klasse FillStoreForm sollte diese Funktion hier ueberfluessig sein.
$pattern = '';
$minMax = array();
......
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 3/23/16
* Time: 1:31 PM
*/
namespace qfq;
require_once(__DIR__ . '/Store.php');
require_once(__DIR__ . '/../Database.php');
require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../helper/HelperFormElement.php');
class FillStoreForm {
/**
* @var Store
*/
private $store = null;
/**
* @var Database
*/
private $db = null;
/**
* @var array
*/
private $feSpecNative = array();
/**
*
*/
public function __construct() {
$this->store = Store::getInstance();
$this->db = new Database();
$this->feSpecNative = $this->loadFormElementsBasedOnSIP();
}
/**
* Loads a minimal definition of FormElement of the form specified in SIP.
*
* @throws CodeException
* @throws DbException
* @throws UserException
*/
private function loadFormElementsBasedOnSIP() {
$form = $this->store->getVar(SIP_FORM, STORE_SIP);
$feSpecNative = $this->db->sql(SQL_FORM_ELEMENT_SIMPLE_ALL_CONTAINER, ROW_REGULAR, [$form]);
if (count($feSpecNative) === 0) {
throw new UserException('Form not found or multiple forms with the same name.', ERROR_FORM_NOT_FOUND);
}
return $feSpecNative;
}
/**
* Copies all current form parameter from STORE_CLIENT to STORE_FORM. Checks the values against FormElement
* definition and throws an exception if check fails. FormElements.type=hidden will be taken from STORE_SIP.
*
* @throws CodeException
* @throws UserException
*/
public function process() {
$html = '';
$newValues = array();
$clientValues = $this->store->getStore(STORE_CLIENT);
// Retrieve SIP vars, e.g. for HIDDEN elements.
$sipValues = $this->store->getStore(STORE_SIP);
// Iterate over all formelements. Sanatize values. Built an assoc array $newValues.
foreach ($this->feSpecNative AS $formElement) {
// Never get a predefined 'id'
if ($formElement['name'] === 'id')
continue;
// Get related formElement.
// construct the field name used in the form
$clientFieldName = HelperFormElement::buildFormElementId($formElement['name'], $sipValues[SIP_RECORD_ID]);
// Preparation for Log, Debug
// $this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'] . ' / ' . $formElement['id'], STORE_SYSTEM);
if ($formElement['type'] == 'hidden') {
// Hidden elements will be transferred by SIP
if (!isset($sipValues[$formElement['name']])) {
throw new CodeException("Missing the hidden field '" . $formElement['name'] . "' in SIP.", ERROR_MISSING_HIDDEN_FIELD_IN_SIP);
}
$newValues[$formElement['name']] = $sipValues[$formElement['name']];
continue;
}
if (isset($clientValues[$clientFieldName])) {
// SELECT with multiple values, or Multi CHECKBOX are delivered as array: implode them
if (is_array($clientValues[$clientFieldName])) {
// E.g. Checkboxes needs a 'HIDDEN' HTML input to detect 'unset' of values. These 'HIDDEN' element
// needs to be removed, if there is at least one checkbox is checked (=submitted)
if (count($clientValues[$clientFieldName]) > 1)
array_shift($clientValues[$clientFieldName]);
$clientValues[$clientFieldName] = implode(',', $clientValues[$clientFieldName]);
}
$newValues[$formElement['name']] = $this->validateValue($formElement, $clientValues[$clientFieldName]);
}
}
$this->store->setVarArray($newValues, STORE_FORM, true);
}
/**
* Check $value against given checkType/pattern. Throws an UserException if check fails.
*
* @param $formElement
* @param $value
* @return string
* @throws CodeException
* @throws UserException
*/
public function validateValue($formElement, $value) {
$pattern = '';
$minMax = array();
$this->store->setVar(SYSTEM_FORM_ELEMENT, $formElement['name'], STORE_SYSTEM);
switch ($formElement['checkType']) {
case 'min|max':
$valueCompare = $value;
$minMax = explode('|', $formElement['checkPattern']);
if ($minMax[0] === '' || $minMax[1] === '') {
$this->store->setVar(SYSTEM_FORM_ELEMENT_MESSAGE, 'Missing definition of value for min or max.', STORE_SYSTEM);
throw new UserException('Missing definition of value for min or max.', ERROR_MISSING_MIN_MAX);
}
$errorText = "Value '$value' is smaller than min '$minMax[0]' or bigger than max '$minMax[1]'.";
// Date/Time: convert in object to make a comparison.
if (preg_match('date|time', $formElement['type']) === 1) {
$valueCompare = date_create($value);
$minMax[0] = date_create($minMax[0]);
$minMax[1] = date_create($minMax[1]);
}
if ($minMax[0] <= $valueCompare && $valueCompare <= $minMax[1])
return $value;
$this->store->setVar(SYSTEM_FORM_ELEMENT_MESSAGE, $errorText, STORE_SYSTEM);
throw new UserException($errorText, ERROR_MIN_MAX_VIOLATION);
break;
case 'pattern':
$pattern = $formElement['checkPattern'];
break;
case 'number':
case 'email':
$arr = OnArray::inputCheckPatternArray();
$htmlPatternAttribute = $arr[$formElement['checkType']];
// remove 'pattern="' and closing ".
$pattern = substr($htmlPatternAttribute, 9, strlen($htmlPatternAttribute) - 9 - 1);
break;
case '': // no checktype specified.
return $value;
default:
throw new CodeException("Unknown checkType: " . $formElement['checkType'], ERROR_UNKNOWN_CHECKTYPE);
}
if (preg_match($pattern, $value) === 1)
return $value;
$errorText = "Value $value violates checkrule " . $formElement['checkType'] . " with pattern '$pattern'.";
$this->store->setVar(SYSTEM_FORM_ELEMENT_MESSAGE, $errorText, STORE_SYSTEM);