Commit 802ef88d authored by Marc Egger's avatar Marc Egger
Browse files

Refs #10120 export forms after all UPDATE statements which are not called by doForm()

parent ce1a3871
Pipeline #3618 failed with stages
in 42 seconds
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace IMATHUZH\Qfq\Core\Database; namespace IMATHUZH\Qfq\Core\Database;
use IMATHUZH\Qfq\Core\Form\FormAsFile;
use IMATHUZH\Qfq\Core\Helper\Logger; use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Store\Store; use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Typo3\T3Handler; use IMATHUZH\Qfq\Core\Typo3\T3Handler;
...@@ -143,6 +144,8 @@ class DatabaseUpdate { ...@@ -143,6 +144,8 @@ class DatabaseUpdate {
if ($dbUpdate === SYSTEM_DB_UPDATE_ALWAYS || ($dbUpdate === SYSTEM_DB_UPDATE_AUTO && $new != $old)) { if ($dbUpdate === SYSTEM_DB_UPDATE_ALWAYS || ($dbUpdate === SYSTEM_DB_UPDATE_AUTO && $new != $old)) {
FormAsFile::importAllForms($this->db, true);
$newFunctionHash = $this->updateSqlFunctions($versionInfo[QFQ_VERSION_KEY_FUNCTION_HASH] ?? ''); $newFunctionHash = $this->updateSqlFunctions($versionInfo[QFQ_VERSION_KEY_FUNCTION_HASH] ?? '');
if (null !== $newFunctionHash) { if (null !== $newFunctionHash) {
$versionInfo[QFQ_VERSION_KEY_FUNCTION_HASH] = $newFunctionHash; $versionInfo[QFQ_VERSION_KEY_FUNCTION_HASH] = $newFunctionHash;
...@@ -158,6 +161,8 @@ class DatabaseUpdate { ...@@ -158,6 +161,8 @@ class DatabaseUpdate {
// Finally write the latest version number. // Finally write the latest version number.
$versionInfo[QFQ_VERSION_KEY] = $new; $versionInfo[QFQ_VERSION_KEY] = $new;
$this->setDatabaseVersion($versionInfo); $this->setDatabaseVersion($versionInfo);
FormAsFIle::exportAllForms($this->db);
} }
if ($old === false) { if ($old === false) {
...@@ -167,6 +172,7 @@ class DatabaseUpdate { ...@@ -167,6 +172,7 @@ class DatabaseUpdate {
if (version_compare($old, '19.9.0') === -1) { if (version_compare($old, '19.9.0') === -1) {
$this->updateSpecialColumns(); $this->updateSpecialColumns();
FormAsFIle::exportAllForms($this->db);
} }
} }
......
...@@ -204,8 +204,17 @@ class DragAndDrop { ...@@ -204,8 +204,17 @@ class DragAndDrop {
return $data; return $data;
} }
// Import Form from file if loaded record is Form/FormElement (If form file was changed, throw exception)
// Note: This is here since this code is called outside QuickFormQuery->doForm
$formFileName = FormAsFile::importFormRecordId($id, $tableName, $this->db);
$this->db->sql("UPDATE `$tableName` SET `$orderColumn`=? WHERE `id`=?", ROW_REGULAR, [$ordNew, $id]); $this->db->sql("UPDATE `$tableName` SET `$orderColumn`=? WHERE `id`=?", ROW_REGULAR, [$ordNew, $id]);
// Export Form file
if ($formFileName !== null) {
FormAsFile::exportForm($formFileName, $this->db);
}
// Converting to string is necessary: JSON detects int else. // Converting to string is necessary: JSON detects int else.
$data[API_ELEMENT_UPDATE][DND_ORD_HTML_ID_PREFIX . $id][API_ELEMENT_CONTENT] = (string)$ordNew; $data[API_ELEMENT_UPDATE][DND_ORD_HTML_ID_PREFIX . $id][API_ELEMENT_CONTENT] = (string)$ordNew;
......
...@@ -8,17 +8,22 @@ use IMATHUZH\Qfq\Core\Helper\HelperFile; ...@@ -8,17 +8,22 @@ use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\OnString; use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\SqlQuery; use IMATHUZH\Qfq\Core\Helper\SqlQuery;
// TODO: test form copying, does it create a form file? // TODO: queryFormNames is executed before Form table exists. Add error to sql and make sure this does not happen in DatabaseUpdate.php
// TODO: ensure that form file is imported if Form is loaded via Rest API
// TODO: gibt es UPDATE/INSERT/DELETE statements die an Form/FormElement arbeiten koennen, die nicht nur durch doForm aufgerufen werden? // TODO: gibt es UPDATE/INSERT/DELETE statements die an Form/FormElement arbeiten koennen, die nicht nur durch doForm aufgerufen werden?
//
// TODO: Testen: QFQ komplett neu installiert => form dir existiert mit allen form files aktuell. QFQ neu installation mit existierenden forms => alte forms sind noch da. QFQ update => alte forms noch da, updates an FormEditor ersichtlich
// TODO: Testen: 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: Testen: 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: Testen: anfrage nach dirty, form file ueberpruefen (importieren) // TODO: Testen: anfrage nach dirty, form file ueberpruefen (importieren)
// TODO: Testen: Dirty tabelle ist aussergewoehnlich voll. werden die records nicht abgeraeumt wegen formAsFile? // TODO: Testen: Dirty tabelle ist aussergewoehnlich voll. werden die records nicht abgeraeumt wegen formAsFile?
// TODO: Testen: 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: Testen: 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: Testen: importAllForms() wird nur ausgefuehrt, wenn der Report Form/FormElement aufruft
// TODO: Carsten Fragen: Form backups erstellen vor deleteFormFile und exportForm? // TODO: Carsten Fragen: Form backups erstellen vor deleteFormFile und exportForm?
// TODO: Carsten Fragen: "git rm" anstatt "rm" benutzen falls ordner ein git repo? Einziger unterschied: macht "git add" // TODO: Carsten Fragen: "git rm" anstatt "rm" benutzen falls ordner ein git repo? Einziger unterschied: macht "git add"
// TODO: Carsten Fragen: Form Copy ist broken. Aber jetzt auch unnötig, da man einfach die files kopieren kann. // TODO: Carsten Fragen: Form Copy ist broken. Aber jetzt auch unnötig, da man einfach die files kopieren kann.
// TODO: Carsten Fragen: Koennte die column "lastImport" zu Form hinzufuegen. Dieser wird dann manuell zusammen mit "changed" beim import und export gesetzt. Mittels Left Join query mit den created columns von FormElement koennte man in importForm() feststellen, ob sich ein form geaendert hat und dieses expotieren, falls der fingerabdruck des files noch gleich ist.
// TODO: Carsten Fragen: QFQ formulare haben id unter 1000 und user forms ueber 1000. Wird ein QFQ formular neu importiert, erhaelt es aber eine id ueber 1000. ist dies ein problem?
// TODO: Carsten Fragen: ZUERST NOCHMAL GENAU NACHTEILE PRUEFEN: In doForm koennte man auch einfach pauschal importAllForms(enforce writeable=true) und exportAllForms() machen, wenn der geladene record ein Form/Formelement ist. Bei vielen forms koennte es langsam sein. ansonsten sehe ich gerade keine klaren nachteile.
// TODO: BEFOORE PRUDUCTION // TODO: BEFOORE PRUDUCTION
// TODO: add Exception hint as hint instead of in main message? // TODO: add Exception hint as hint instead of in main message?
...@@ -26,7 +31,7 @@ use IMATHUZH\Qfq\Core\Helper\SqlQuery; ...@@ -26,7 +31,7 @@ use IMATHUZH\Qfq\Core\Helper\SqlQuery;
// TODO: remove DEBUG blocks // TODO: remove DEBUG blocks
// TODO: Form-Editor form name einschraenken auf eindeutige und valide file names // TODO: Form-Editor form name einschraenken auf eindeutige und valide file names
// TODO: Form-Editor anpassen: container name muessen eindeutig sein. // TODO: Form-Editor anpassen: container name muessen eindeutig sein.
// TODO: add fileStats column to Form schema // TODO: add fileStats column to Form schema and to DatabaseUpdateData.php. Actually formEditor.sql is run in DatabaseUpdate.php after the DatabaseUpdateData.php changes are executed. So is this neccessary? (ASK Carsten)
// TODO: move Constants to Constants.php if this file is actually used // TODO: move Constants to Constants.php if this file is actually used
// TODO: extract all sql queries and make them reusable // TODO: extract all sql queries and make them reusable
// TODO: write tests (file test https://medium.com/weebly-engineering/phpunit-mocking-the-file-system-using-vfsstream-5d7d79b1eb2a) // TODO: write tests (file test https://medium.com/weebly-engineering/phpunit-mocking-the-file-system-using-vfsstream-5d7d79b1eb2a)
...@@ -50,6 +55,8 @@ use IMATHUZH\Qfq\Core\Helper\SqlQuery; ...@@ -50,6 +55,8 @@ use IMATHUZH\Qfq\Core\Helper\SqlQuery;
// form file form element changed => changed in form editor // form file form element changed => changed in form editor
// form file form element removed => removed in form editor // form file form element removed => removed in form editor
// form folder does not exist but formEditor is opened => form folder is created, all forms exported
// TODO: MAYBE // TODO: MAYBE
// TODO: Maybe: solve reference by ID after file change Problem (might not be a big deal since it only happens on git pull) // TODO: Maybe: solve reference by ID after file change Problem (might not be a big deal since it only happens on git pull)
// Problem: FormEditor and form list might reference a form by an old id. In that case everything has to be reloaded. That's annoying. // Problem: FormEditor and form list might reference a form by an old id. In that case everything has to be reloaded. That's annoying.
...@@ -182,9 +189,10 @@ class FormAsFile ...@@ -182,9 +189,10 @@ class FormAsFile
/** /**
* Reads the form from the DB and saves it in the form folder as a form file. * 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. * Warning: Overwrites form file without any checks or warning.
* Updates column fileStats with new stats from new file. * If the form file path does not exist, it is created and all forms are exported.
* *
* FileStats: After the export the column "fileStats" is updated with new stats from new file.
* Container FormElements: The form file uses names instead if ids to reference containers. These references are translated before saving the file. * Container FormElements: The form file uses names instead if ids to reference containers. These references are translated before saving the file.
* Ignored columns: The columns 'id', 'name' (only Form table), 'fileStats', 'formId', 'feIdContainer' are not saved in the file. * Ignored columns: The columns 'id', 'name' (only Form table), 'fileStats', 'formId', 'feIdContainer' are not saved in the file.
* FormElement order: The formElements are ordered by the 'ord' column before writing them to the file. * FormElement order: The formElements are ordered by the 'ord' column before writing them to the file.
...@@ -260,6 +268,34 @@ class FormAsFile ...@@ -260,6 +268,34 @@ class FormAsFile
$database->sql($sql, ROW_REGULAR, $parameterArray); $database->sql($sql, ROW_REGULAR, $parameterArray);
} }
/**
* Import Form from file if loaded record is Form/FormElement.
* If form file was changed, import it and throw exception.
* If form file is not writeable, throw exception.
*
* @param int $recordId
* @param string $tableName
* @param Database $database
* @return string|null formName|null
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public static function importFormRecordId(int $recordId, string $tableName, Database $database): ?string
{
$recordFormName = self::formNameFromFormRelatedRecord($recordId, $tableName, $database);
if ($recordFormName !== null) {
if(self::importForm($recordFormName, $database)) {
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."]),
ERROR_FORM_NOT_FOUND);
}
}
self::enforceFormFileWritable($recordFormName, $database);
return $recordFormName;
}
/** /**
* Deletes the form file for the given form. * Deletes the form file for the given form.
* *
...@@ -274,7 +310,13 @@ class FormAsFile ...@@ -274,7 +310,13 @@ class FormAsFile
self::enforceFormFileWritable($formName, $database); self::enforceFormFileWritable($formName, $database);
$pathFileName = self::formPathFileName($formName, $database); $pathFileName = self::formPathFileName($formName, $database);
if(file_exists($pathFileName)) { if(file_exists($pathFileName)) {
unlink($pathFileName); $success = unlink($pathFileName);
if ($success === false) {
throw new \UserFormException(json_encode([
ERROR_MESSAGE_TO_USER => "Deleting form file failed: " . baseName($pathFileName),
ERROR_MESSAGE_TO_DEVELOPER => "Can't delete form file '$pathFileName'"]),
ERROR_IO_WRITE_FILE);
}
} }
} }
...@@ -388,14 +430,15 @@ class FormAsFile ...@@ -388,14 +430,15 @@ class FormAsFile
} }
/** /**
* Import all form files and delete forms in the DB for which there are not form files. * Import all form files and DELETE forms in the DB for which there are not form files.
* *
* @param Database $database * @param Database $database
* @param bool $enforceWritable
* @throws \CodeException * @throws \CodeException
* @throws \DbException * @throws \DbException
* @throws \UserFormException * @throws \UserFormException
*/ */
public static function updateAllForms(Database $database): void public static function importAllForms(Database $database, bool $enforceWritable = false): void
{ {
// Import all form files // Import all form files
$formPath = self::formPath($database); $formPath = self::formPath($database);
...@@ -406,25 +449,46 @@ class FormAsFile ...@@ -406,25 +449,46 @@ class FormAsFile
ERROR_MESSAGE_TO_DEVELOPER => "Can't read directory: " . $formPath]), ERROR_MESSAGE_TO_DEVELOPER => "Can't read directory: " . $formPath]),
ERROR_IO_READ_FILE); ERROR_IO_READ_FILE);
} }
$formNamesFile = []; $formFileNames = [];
foreach ($files as $file) { foreach ($files as $file) {
$fileInfo = pathinfo($file); $fileInfo = pathinfo($file);
if ($fileInfo['extension'] === 'json') { if ($fileInfo['extension'] === 'json') {
$formNamesFile[] = $fileInfo['filename']; $fileName = $fileInfo['filename'];
self::importForm($fileInfo['filename'], $database); $formFileNames[] = $fileName;
self::importForm($fileName, $database);
if ($enforceWritable) {
self::enforceFormFileWritable($fileName, $database);
}
} }
} }
// Delete all forms which are in DB but not in files // Delete all forms which are in DB but not in files
$NAME = F_NAME; $formNamesDB = self::queryAllFormNames($database);
$FORM = TABLE_NAME_FORM; $formsToDelete = array_diff($formNamesDB, $formFileNames);
$formNamesDB = array_column($database->sql("SELECT `$NAME` FROM `$FORM`", ROW_REGULAR), $NAME);
$formsToDelete = array_diff($formNamesDB, $formNamesFile);
foreach ($formsToDelete as $formToDelete) { foreach ($formsToDelete as $formToDelete) {
self::deleteFormDB($formToDelete, $database); self::deleteFormDB($formToDelete, $database);
} }
} }
/**
* Export all forms in database to form files.
* Warning: This overwrites form files without any checks or warning. Use cautiously.
*
* Note: Does not delete form files if the corresponding forms are not present in the database.
*
* @param Database $database
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
public static function exportAllForms(Database $database): void
{
$formNamesDB = self::queryAllFormNames($database);
foreach ($formNamesDB as $formNameDB) {
self::exportForm($formNameDB, $database);
}
}
/** /**
* Insert formElement to the given form. * Insert formElement to the given form.
* Keys removed before insert: id, formId, feIdContainer * Keys removed before insert: id, formId, feIdContainer
...@@ -563,13 +627,24 @@ class FormAsFile ...@@ -563,13 +627,24 @@ class FormAsFile
} }
// export all forms // export all forms
$NAME = F_NAME; self::exportAllForms($database);
$FORM = TABLE_NAME_FORM;
$formNamesDB = array_column($database->sql("SELECT `$NAME` FROM `$FORM`", ROW_REGULAR), $NAME);
foreach ($formNamesDB as $formNameDB) {
self::exportForm($formNameDB, $database);
}
} }
return $formPath; return $formPath;
} }
/**
* Return array of all form names in the Form table.
*
* @param Database $database
* @return array [formName]
* @throws \CodeException
* @throws \DbException
* @throws \UserFormException
*/
private static function queryAllFormNames(Database $database): array
{
$NAME = F_NAME;
$FORM = TABLE_NAME_FORM;
return array_column($database->sql("SELECT `$NAME` FROM `$FORM`", ROW_REGULAR), $NAME);
}
} }
\ No newline at end of file
...@@ -424,22 +424,22 @@ class QuickFormQuery { ...@@ -424,22 +424,22 @@ class QuickFormQuery {
// Make sure form file is writable (before DB changes are made) // Make sure form file is writable (before DB changes are made)
// Note: This can't be done earlier because $formModeNew might be changed in the lines above. // Note: This can't be done earlier because $formModeNew might be changed in the lines above.
$formFileNameDB = FormAsFile::formNameFromFormRelatedRecord($recordId, $this->formSpec[F_TABLE_NAME], $this->dbArray[$this->dbIndexQfq]); $formNameDB = FormAsFile::formNameFromFormRelatedRecord($recordId, $this->formSpec[F_TABLE_NAME], $this->dbArray[$this->dbIndexQfq]);
switch ($this->formSpec[F_TABLE_NAME]) { switch ($this->formSpec[F_TABLE_NAME]) {
case TABLE_NAME_FORM: // cases covered: new form, existing form, existing form but form name changed case TABLE_NAME_FORM: // cases covered: new form, existing form, existing form but form name changed
$formFileName = $this->store->getVar(F_NAME, STORE_FORM, SANITIZE_ALLOW_ALNUMX); $formFileName = $this->store->getVar(F_NAME, STORE_FORM, SANITIZE_ALLOW_ALNUMX);
$formFileName = $formFileName === false ? $formFileNameDB : $formFileName; $formFileName = $formFileName === false ? $formNameDB : $formFileName;
if ($formFileNameDB !== null && $formFileName !== $formFileNameDB && $formModeNew === FORM_SAVE) { if ($formNameDB !== null && $formFileName !== $formNameDB && $formModeNew === FORM_SAVE) {
$formFileNameDelete = $formFileNameDB; $formFileNameDelete = $formNameDB;
FormAsFile::enforceFormFileWritable($formFileNameDelete, $this->dbArray[$this->dbIndexQfq]); // file will be deleted after DB changes FormAsFile::enforceFormFileWritable($formFileNameDelete, $this->dbArray[$this->dbIndexQfq]); // file will be deleted after DB changes
} }
break; break;
case TABLE_NAME_FORM_ELEMENT: // cases covered: new formElement, existing formElement case TABLE_NAME_FORM_ELEMENT: // cases covered: new formElement, existing formElement
$formId = $this->store->getVar(FE_FORM_ID, STORE_FORM); $formId = $this->store->getVar(FE_FORM_ID, STORE_FORM);
$formFileName = $formId !== false ? FormAsFile::formNameFromFormRelatedRecord($formId, TABLE_NAME_FORM, $this->dbArray[$this->dbIndexQfq]) : $formFileNameDB; $formFileName = $formId !== false ? FormAsFile::formNameFromFormRelatedRecord($formId, TABLE_NAME_FORM, $this->dbArray[$this->dbIndexQfq]) : $formNameDB;
break; break;
default: default:
$formFileName = $formFileNameDB; $formFileName = $formNameDB;
} }
if ($formFileName !== null && in_array($formModeNew, [FORM_SAVE, FORM_DRAG_AND_DROP, FORM_DELETE])) { if ($formFileName !== null && in_array($formModeNew, [FORM_SAVE, FORM_DRAG_AND_DROP, FORM_DELETE])) {
FormAsFile::enforceFormFileWritable($formFileName, $this->dbArray[$this->dbIndexQfq]); FormAsFile::enforceFormFileWritable($formFileName, $this->dbArray[$this->dbIndexQfq]);
...@@ -1052,15 +1052,7 @@ class QuickFormQuery { ...@@ -1052,15 +1052,7 @@ class QuickFormQuery {
[$formName], 'Form "' . $formName . '" not found or multiple forms with the same name.'); [$formName], 'Form "' . $formName . '" not found or multiple forms with the same name.');
// Import Form from file if loaded record is Form/FormElement (If form file was changed, throw exception) // Import Form from file if loaded record is Form/FormElement (If form file was changed, throw exception)
$recordFormName = FormAsFile::formNameFromFormRelatedRecord($recordId, $form[F_TABLE_NAME], $this->dbArray[$this->dbIndexQfq]); FormAsFile::importFormRecordId($recordId, $form[F_TABLE_NAME], $this->dbArray[$this->dbIndexQfq]);
if ($recordFormName !== null) {
if(FormAsFile::importForm($recordFormName, $this->dbArray[$this->dbIndexQfq])) {
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."]),
ERROR_FORM_NOT_FOUND);
}
}
$form = $this->checkFormLogMode($form); $form = $this->checkFormLogMode($form);
$form = $this->modeCleanFormConfig($mode, $form); $form = $this->modeCleanFormConfig($mode, $form);
......
...@@ -494,7 +494,7 @@ class Report { ...@@ -494,7 +494,7 @@ class Report {
// import form files if changed + delete Forms without form file // import form files if changed + delete Forms without form file
if (FormAsFile::isFormQuery($sql)) { if (FormAsFile::isFormQuery($sql)) {
FormAsFile::updateAllForms($this->db); FormAsFile::importAllForms($this->db);
} }
//Execute SQL. All errors have been already catched. //Execute SQL. All errors have been already catched.
......
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