Commit 6674f4ae authored by Marc Egger's avatar Marc Egger
Browse files

Refs #10120 export form on save works

parent 7d6ca4d0
Pipeline #3598 failed with stages
in 1 minute and 1 second
......@@ -6,9 +6,19 @@ use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\SqlQuery;
// TODO: Form speichern. siehe zettlr note. <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< NEXT
// TODO: bevor ein form/formelement im form editor gespeichert wird, soll es vom file geladen werden (Save.php). Falls das file importiert wurde, gibt es eine fehlermeldung.
// TODO: test saving of FormElement <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// TODO: implement FORM_DELETE in QuickFormQuery.php
// TODO: Form speichern. siehe zettlr note.
// FORM_DELETE: export Form/FormElement
// FORM_REST:
// FORM_LOAD:
// FORM_SAVE: export Form/FormElement
// FORM_UPDATE: (export Form/FormElement) NO, ask carsten
// FORM_DRAG_AND_DROP: export Form/FormElement
// TODO: gibt es UPDATE/INSERT/DELETE statements die an Form/FormElement arbeiten koennen, die nicht nur durch doForm aufgerufen werden?
// TODO: Dies sollte schon erfuellt sein, testen: bevor ein form/formelement im form editor gespeichert wird, soll es vom file geladen werden (Save.php). Falls das file importiert wurde, gibt es eine fehlermeldung.
// TODO: anfrage nach dirty, form file ueberpruefen (importieren)
// TODO: Testen: Dirty tabelle ist aussergewoehnlich voll. werden die records nicht abgeraeumt wegen formAsFile?
// TODO: ausporbieren was passiert wenn ich in Form-Editor auf speicher druecke nachdem ich in einem anderen tab das geanderte form file in die DB lade.
// TODO: form list Report: when form list report is loaded, load new form files into DB and delete removed forms from DB
// TODO: Carsten Fragen: Form backups erstellen vor dem delete?
......@@ -16,8 +26,11 @@ use IMATHUZH\Qfq\Core\Helper\SqlQuery;
// Problem: FormEditor and form list might reference a form by an old id. In that case everything has to be reloaded. That's annoying.
// Variant 1: reference form by name in edit and delete button, not by id (only solver part of the problem)
// Variant 2: track old form ids and relay to new form automatically. Track old form ids in new Form column "oldIds"
// TODO: DONT DO: add column import-modification-date to Form and FormElement and set them to the same as modification date in both exportForm and importForm. compare the two modification dates in the beginning. If the actual modification is after importModification and fileStats are the same in file and in form, then exportForm.
// TODO: DONT DO: what to do if importModificationDate and fileStats have changed? > 1) export Form to new form file named <formName>mergeConflict_timestamp 2) importForm form file, overwrite DB
// TODO: BEFOORE PRUDUCTION
// TODO: move formFile code in QuickFormQuery.php into methods in this file as much as possible?
// TODO: remove DEBUG blocks
// TODO: Form-Editor form name einschraenken auf eindeutige und valide file names
// TODO: Form-Editor anpassen: container name muessen eindeutig sein.
......@@ -29,10 +42,17 @@ use IMATHUZH\Qfq\Core\Helper\SqlQuery;
// TODO: Carsten Fragen: add log messages somewhare?
///////////////// JUST A TEST, DELETE ME! //////////////
// FormAsFile::databaseToFile($formName, $this->dbArray[$this->dbIndexQfq]);
// FormAsFile::FileToDatabase($formName, $this->dbArray[$this->dbIndexQfq]);
// FormAsFile::exportForm($formName, $this->dbArray[$this->dbIndexQfq]);
// FormAsFile::importForm($formName, $this->dbArray[$this->dbIndexQfq]);
///////////////// TEST FINISHED ////////////////////////
/////// DEBUG /////////
// throw new \UserFormException(json_encode([
// ERROR_MESSAGE_TO_USER => json_encode([$pathFileName, file_exists($pathFileName), getcwd()], JSON_PRETTY_PRINT),
// ERROR_MESSAGE_TO_DEVELOPER => json_encode([$pathFileName, file_exists($pathFileName)], JSON_PRETTY_PRINT)
// ]));
///////////////////////
const SYSTEM_FORM_FILE_PATH = 'fileadmin/protected/qfq/form'; // where form specification files are saved and loaded
const FORM_FILE_FORM_ELEMENT = 'FormElement_ff'; // Key for FormElements array saved in Form File
const FORM_FILE_CONTAINER_NAME = 'containerName_ff'; // key for referencing container FormElements by name in form file
......@@ -56,60 +76,45 @@ class FormAsFile
}
/**
* Wrapper to call self::importForm(...) with a formElement ID instead of a form name.
* Wrapper to call self::importForm(...) with an ID instead of a form name.
*
* @param int $formElementId
* @param int $formId
* @param Database $database
* @return bool True if form has been updated.
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public static function importFormElementWithId(int $formElementId, Database $database): bool
public static function importFormWithId(int $formId, Database $database): bool
{
if ($formElementId === 0) {
if ($formId === 0) {
return false;
}
// get form name
$F_NAME = F_NAME;
$TABLE_NAME_FORM_ELEMENT = TABLE_NAME_FORM_ELEMENT;
$TABLE_NAME_FORM = TABLE_NAME_FORM;
$F_ID = F_ID;
$FE_FORM_ID = FE_FORM_ID;
$FE_ID = FE_ID;
$formFromDb = $database->sql("SELECT `f`.`$F_ID`, `f`.`$F_NAME` FROM `$TABLE_NAME_FORM` AS f INNER JOIN `$TABLE_NAME_FORM_ELEMENT` AS fe ON f.`$F_ID`=fe.`$FE_FORM_ID` WHERE `fe`.`$FE_ID`=?", ROW_EXPECT_1,
[$formElementId], "Form element with id '$formElementId' not found. " . self::errorHintFormImport());
// import form
return self::importForm($formFromDb[F_NAME], $database);
$formName = self::queryFormNameWithId($formId, $database);
return self::importForm($formName, $database);
}
/**
* Wrapper to call self::importForm(...) with an ID instead of a form name.
* Wrapper to call self::importForm(...) with a formElement ID instead of a form name.
*
* @param int $formId
* @param int $formElementId
* @param Database $database
* @return bool True if form has been updated.
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public static function importFormWithId(int $formId, Database $database): bool
public static function importFormElementWithId(int $formElementId, Database $database): bool
{
if ($formId === 0) {
if ($formElementId === 0) {
return false;
}
// get form name
$F_NAME = F_NAME;
$TABLE_NAME_FORM = TABLE_NAME_FORM;
$F_ID = F_ID;
$formFromDb = $database->sql("SELECT `$F_NAME` FROM `$TABLE_NAME_FORM` AS f WHERE `f`.`$F_ID`=? AND `f`.`deleted`='no'", ROW_EXPECT_1,
[$formId], "Form with id '$formId' not found. " . self::errorHintFormImport());
$formName = self::queryFormNameWithFormElementId($formElementId, $database);
// import form
return self::importForm($formFromDb[F_NAME], $database);
return self::importForm($formName, $database);
}
/**
......@@ -132,33 +137,20 @@ class FormAsFile
{
// Get file stats both from file system and DB and exit if they are equal
self::enforceFormNameValid($formName);
$pathFileName = HelperFile::correctRelativePathFileName(SYSTEM_FORM_FILE_PATH . '/' . $formName . ".json");
$pathFileName = self::formPathFileName($formName);
$fileReadException = new \UserFormException(json_encode([
ERROR_MESSAGE_TO_USER => "Form file not found or missing permission: {$formName}.json",
ERROR_MESSAGE_TO_DEVELOPER => "Form definition file not found or no permission to read file: '$pathFileName'"]),
ERROR_FORM_NOT_FOUND);
/////// DEBUG /////////
// throw new \UserFormException(json_encode([
// ERROR_MESSAGE_TO_USER => json_encode([$pathFileName, file_exists($pathFileName), getcwd()], JSON_PRETTY_PRINT),
// ERROR_MESSAGE_TO_DEVELOPER => json_encode([$pathFileName, file_exists($pathFileName)], JSON_PRETTY_PRINT)
// ]));
///////////////////////
if(!file_exists($pathFileName)) {
self::deleteForm($formName, $database);
throw $fileReadException;
}
$stat = stat($pathFileName);
if ($stat === false) {
$fileStatsNew = self::formFileStatsJson($pathFileName);
if ($fileStatsNew === false) {
self::deleteForm($formName, $database);
throw $fileReadException;
}
$fileStatsNew = json_encode([
'modified' => $stat['mtime'],
'size' => $stat['size'],
'inode' => $stat['ino']
]);
$F_ID = F_ID; // can't use constants in strings directly
$F_FILE_STATS = F_FILE_STATS; // can't use constants in strings directly
$F_NAME = F_NAME; // can't use constants in strings directly
......@@ -217,6 +209,42 @@ class FormAsFile
return true;
}
/**
* Wrapper to call self::exportForm(...) with an ID instead of a form name.
*
* @param int $formId
* @param Database $database
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public static function exportFormWithId(int $formId, Database $database): void
{
if ($formId === 0) {
return;
}
$formName = self::queryFormNameWithId($formId, $database);
self::exportForm($formName, $database);
}
/**
* Wrapper to call self::exportForm(...) with a formElement ID instead of a form name.
*
* @param int $formElementId
* @param Database $database
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public static function exportFormElementWithId(int $formElementId, Database $database): void
{
if ($formElementId === 0) {
return;
}
$formName = self::queryFormNameWithFormElementId($formElementId, $database);
self::exportForm($formName, $database);
}
/**
* Reads the form from the DB and saves it in the form folder as a form file.
* If the form file path does not exist, it is created.
......@@ -283,17 +311,41 @@ class FormAsFile
// write form as JSON to file
$form[FORM_FILE_FORM_ELEMENT] = $formElements;
$formJson = json_encode($form, JSON_PRETTY_PRINT);
if (!is_dir(SYSTEM_FORM_FILE_PATH)) {
mkdir(SYSTEM_FORM_FILE_PATH, 0777, true);
}
$pathFileName = HelperFile::correctRelativePathFileName(SYSTEM_FORM_FILE_PATH . '/' . $formName . ".json");
$pathFileName = self::formPathFileName($formName);
$success = file_put_contents($pathFileName, $formJson);
if ($success === false) {
throw new \UserFormException(json_encode([
ERROR_MESSAGE_TO_USER => 'writing file failed',
ERROR_MESSAGE_TO_USER => "writing form file '{$formName}.json' failed",
ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$pathFileName'"]),
ERROR_IO_WRITE_FILE);
}
// Update column fileStats
$fileStats = self::formFileStatsJson($pathFileName);
list($sql, $parameterArray) = SqlQuery::updateRecord(TABLE_NAME_FORM, [F_FILE_STATS => $fileStats], $formId);
$database->sql($sql, ROW_REGULAR, $parameterArray);
}
/**
* Throw exception if Form file is not writeable.
*
* @param int $formId
* @param Database $database
* @return void
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public static function enforceFormFileWritable(int $formId, Database $database): void
{
$formName = self::queryFormNameWithId($formId, $database);
$pathFileName = self::formPathFileName($formName);
if (!is_writable($pathFileName)) {
throw new \UserFormException(json_encode([
ERROR_MESSAGE_TO_USER => "Can't write to form file '{$formName}.json'. Check permissions.",
ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$pathFileName'."]),
ERROR_IO_WRITE_FILE);
}
}
/**
......@@ -381,4 +433,82 @@ class FormAsFile
$FE_FORM_ID = FE_FORM_ID; // can't use constants in strings directly
$database->sql("DELETE FROM `$TABLE_NAME_FORM_ELEMENT` WHERE `$FE_FORM_ID`=?", ROW_REGULAR, [$formId]);
}
/**
* @param int $formId
* @param Database $database
* @return string
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private static function queryFormNameWithId(int $formId, Database $database): string
{
$F_NAME = F_NAME;
$TABLE_NAME_FORM = TABLE_NAME_FORM;
$F_ID = F_ID;
$formFromDb = $database->sql("SELECT `$F_NAME` FROM `$TABLE_NAME_FORM` AS f WHERE `f`.`$F_ID`=? AND `f`.`deleted`='no'", ROW_EXPECT_1,
[$formId], "Form with id '$formId' not found. " . self::errorHintFormImport());
return $formFromDb[F_NAME];
}
/**
* @param int $formElementId
* @param Database $database
* @return string
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private static function queryFormNameWithFormElementId(int $formElementId, Database $database): string
{
$F_NAME = F_NAME;
$TABLE_NAME_FORM_ELEMENT = TABLE_NAME_FORM_ELEMENT;
$TABLE_NAME_FORM = TABLE_NAME_FORM;
$F_ID = F_ID;
$FE_FORM_ID = FE_FORM_ID;
$FE_ID = FE_ID;
$formFromDb = $database->sql("SELECT `f`.`$F_NAME` FROM `$TABLE_NAME_FORM` AS f INNER JOIN `$TABLE_NAME_FORM_ELEMENT` AS fe ON f.`$F_ID`=fe.`$FE_FORM_ID` WHERE `fe`.`$FE_ID`=?", ROW_EXPECT_1,
[$formElementId], "Form element with id '$formElementId' not found. " . self::errorHintFormImport());
return $formFromDb[F_NAME];
}
/**
* @param string $pathFileName
* @return false|string
*/
private static function formFileStatsJson(string $pathFileName)
{
$stats = stat($pathFileName);
if ($stats === false) {
return false;
}
return json_encode([
'modified' => $stats['mtime'],
'size' => $stats['size'],
'inode' => $stats['ino']
]);
}
/**
* Return correct pathFileName of form file relative to current working directory.
* Create path if it doesn't exist.
*
* @param string $formName
* @return string
* @throws \UserFormException
*/
private static function formPathFileName(string $formName): string
{
if (!is_dir(SYSTEM_FORM_FILE_PATH)) {
$success = mkdir(SYSTEM_FORM_FILE_PATH, 0777, true);
if ($success === false) {
throw new \UserFormException(json_encode([
ERROR_MESSAGE_TO_USER => "Can't create form file path.",
ERROR_MESSAGE_TO_DEVELOPER => "Can't create path: " . SYSTEM_FORM_FILE_PATH]),
ERROR_IO_WRITE_FILE);
}
}
return HelperFile::correctRelativePathFileName(SYSTEM_FORM_FILE_PATH . '/' . $formName . ".json");
}
}
\ No newline at end of file
......@@ -422,6 +422,12 @@ class QuickFormQuery {
}
}
// Make sure form file is writable (before DB changes are made)
if (in_array($this->formSpec[F_TABLE_NAME], [TABLE_NAME_FORM, TABLE_NAME_FORM_ELEMENT])
&& in_array($formModeNew, [FORM_SAVE, FORM_DRAG_AND_DROP, FORM_DELETE])) {
FormAsFile::enforceFormFileWritable($recordId, $this->dbArray[$this->dbIndexQfq]);
}
// For 'new' record always create a new Browser TAB-uniq (for this current form, nowhere else used) SIP.
// With such a Browser TAB-uniq SIP, multiple Browser TABs and following repeated NEWs are easily implemented.
if ($formMode != FORM_REST) {
......@@ -620,6 +626,26 @@ class QuickFormQuery {
$data = $this->groupElementUpdateEntries($data);
}
// export Form to file, if loaded record is a Form/FormElement
switch ($formModeNew) {
case FORM_SAVE:
case FORM_DRAG_AND_DROP:
if (TABLE_NAME_FORM === $this->formSpec[F_TABLE_NAME]) {
FormAsFile::exportFormWithId($recordId, $this->dbArray[$this->dbIndexQfq]);
} elseif (TABLE_NAME_FORM_ELEMENT === $this->formSpec[F_TABLE_NAME]) {
FormAsFile::exportFormElementWithId($recordId, $this->dbArray[$this->dbIndexQfq]);
}
break;
case FORM_DELETE:
if (TABLE_NAME_FORM === $this->formSpec[F_TABLE_NAME]) {
// TODO: delete form file
} elseif (TABLE_NAME_FORM_ELEMENT === $this->formSpec[F_TABLE_NAME]) {
// TODO: find out the formId of the FormElement. should be in store. Or save it in a variable earlier.
// TODO: export Form
}
}
return $data;
}
......@@ -998,20 +1024,22 @@ class QuickFormQuery {
$this->store->setVar(CLIENT_RECORD_ID, $rTmp, STORE_TYPO3);
}
// Load form
// Check for form file changes
FormAsFile::importForm($formName, $this->dbArray[$this->dbIndexQfq]);
// Load form
$constant = F_NAME; // PhpStorm complains if the constant is directly defined in the string below
$form = $this->dbArray[$this->dbIndexQfq]->sql("SELECT * FROM `Form` AS f WHERE `f`.`$constant` LIKE ? AND `f`.`deleted`='no'", ROW_EXPECT_1,
[$formName], 'Form "' . $formName . '" not found or multiple forms with the same name.');
// If form operates on Form/FormElement then import form file (If form was changed, throw exception)
$formChanged = false;
// Import Form from file if loaded record is Form/FormElement (If form file was changed, throw exception)
$throwException = false;
if ($form[F_TABLE_NAME] === TABLE_NAME_FORM) {
$formChanged = FormAsFile::importFormWithId($recordId, $this->dbArray[$this->dbIndexQfq]);
$throwException = FormAsFile::importFormWithId($recordId, $this->dbArray[$this->dbIndexQfq]);
} elseif ($form[F_TABLE_NAME] === TABLE_NAME_FORM_ELEMENT) {
$formChanged = FormAsFile::importFormElementWithId($recordId, $this->dbArray[$this->dbIndexQfq]);
$throwException = FormAsFile::importFormElementWithId($recordId, $this->dbArray[$this->dbIndexQfq]);
}
if($formChanged) {
if($throwException) {
throw new \UserFormException(json_encode([
ERROR_MESSAGE_TO_USER => 'Form file was changed.',
ERROR_MESSAGE_TO_DEVELOPER => "Form definition file has been changed. Please close tab and reload the form list and Form-Editor from scratch."]),
......
Supports Markdown
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