diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..417dfed --- /dev/null +++ b/.gitattributes @@ -0,0 +1,56 @@ +# Used to remove files from deployment using `git archive` +# git files +.gitattributes export-ignore +.gitignore export-ignore +# buildpath files +.buildpath export-ignore +# project files and settings +.project export-ignore +.settings +# composer files +composer.* export-ignore +# travis file +.travis.yml export-ignore +# phpunit files +phpunit.* export-ignore +# CodeSniffer files +phpcs.* export-ignore +# phpDocumentor files +phpdoc.* export-ignore +phpDocumentor.phar export-ignore +# Codeception files +codeception.* export-ignore +codeception-setup.* export-ignore +# general text files +*README export-ignore +*LICENSE.txt export-ignore +*license.txt export-ignore +*LICENSE.html export-ignore +COPYRIGHT.txt export-ignore +CHANGELOG.* export-ignore +*changelog.txt export-ignore +*changelog.html export-ignore +*CHANGES.html export-ignore +*INSTALL.html export-ignore +*TODO.txt export-ignore +*API.txt export-ignore +*api.html export-ignore +*KNOWN_ISSUES.txt export-ignore +UPGRADE.txt export-ignore +web.config export-ignore +MAINTAINERS.txt export-ignore +INSTALL*.txt export-ignore +# scripts +bin export-ignore +# data files +data export-ignore +# docs +docs export-ignore +# docs-api +docs-api export-ignore +# libs +lib export-ignore +# tests +tests export-ignore +# vendor +vendor export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15e603b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.settings/ +.project +.buildpath +deployment.xml +deployment.properties +.idea/ +vendor/* +data/* +tests/_output/* +composer.lock \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 0000000..a07ae52 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG + +## 1.0.0 (2023-02-27) + +- Initial version of the MoodleORM local plugin. diff --git a/COPYING.txt b/COPYING.txt new file mode 100644 index 0000000..626c67d --- /dev/null +++ b/COPYING.txt @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Andrew Caya + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d8f048 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Moodle Local moodleorm +====================== + +Simple ORM with Unit of Work to easily save cascading data from Moodle Forms, using Moodle Entities, through single database transactions. diff --git a/classes/entity.php b/classes/entity.php new file mode 100644 index 0000000..3b13fae --- /dev/null +++ b/classes/entity.php @@ -0,0 +1,77 @@ +. + +/** + * Class for entity persistence. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing an entity from the database. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ +class entity extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = null; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + ], + 'name' => [ + 'type' => PARAM_TEXT, + ], + 'description' => [ + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ], + 'entityid' => [ + 'type' => PARAM_INT, + ], + 'timecreated' => [ + 'type' => PARAM_INT, + 'default' => time(), + ], + 'timemodified' => [ + 'type' => PARAM_INT, + 'default' => time(), + ], + ]; + } +} diff --git a/classes/traits/unitofworkaware.php b/classes/traits/unitofworkaware.php new file mode 100644 index 0000000..538fb16 --- /dev/null +++ b/classes/traits/unitofworkaware.php @@ -0,0 +1,52 @@ +. + +/** + * Trait for getting data from Moodle persistent objects. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm\traits; + +/** + * Trait to allow access to the data payload of Moodle persistent objects. + */ +trait unitofworkaware { + + /** + * Getter method for Moodle persistent objects. + * + * @return object|bool + */ + public function get_data() { + $reflection = new \ReflectionClass($this); + $propertiesraw = $reflection->getParentClass()->getProperties(); + + foreach ($propertiesraw as $property) { + if ($property->getName() === 'data') { + $property->setAccessible(true); + + return (object) $property->getValue($this); + } + } + + return false; + } +} diff --git a/classes/unitofwork.php b/classes/unitofwork.php new file mode 100644 index 0000000..553d57e --- /dev/null +++ b/classes/unitofwork.php @@ -0,0 +1,847 @@ +. + +/** + * Class of a unit of work for persistence management. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm; + +/** + * Class for unit of work persistence of Moodle persistent entities. + */ +class unitofwork { + /** + * @var string Contains a mapping of the main database table id with the table's name as its id index key. + */ + protected $maintablename = ''; + + /** + * @var int Contains a mapping of the main database table id with the table's name as its id index key. + */ + protected $maintablekey = 0; + + /** + * @var string Contains the name of the child's column name which contains the parent's foreign key. + * + * Optional - can be a self-referenced column for single tables without child tables. + */ + protected $childparentidcolumnname = ''; + + /** + * @var \moodle_database Contains a Moodle database connection $DB. + */ + protected $db; + + /** + * @var array Contains a list of objects containing the parameters of this activity (id) in the main table. + */ + protected $maintablesettings = []; + + /** + * @var array Contains a list of mapped persistent classes (dependencies are sub-arrays). + */ + protected $classmap = []; + + /** + * @var array Contains registered persistent objects. + */ + protected $registry = []; + + /** + * @var array Contains keys of dirty persistent objects. + */ + protected $dirty = []; + + /** + * @var array Contains references to staged persistent objects. + */ + protected $stage = []; + + /** + * @var bool Flag representing current Unit of Work state. + */ + protected $committed = false; + + /** + * Class constructor method. + * + * @param \moodle_database $DB + * @param array $classmap + * @param string $maintablename + * @param int $maintablekey + * @param string $childparentidcolumnname defaults to null + * @param bool $enableconstraints defaults to false + * + * @throws \dml_exception + */ + public function __construct( + \moodle_database $DB, + array $classmap, + string $maintablename, + int $maintablekey, + string $childparentidcolumnname = null, + bool $enableconstraints = false + ) { + $this->db = $DB; + $this->maintablename = $maintablename; + $this->maintablekey = $maintablekey; + $this->childparentidcolumnname = $childparentidcolumnname ?? ''; + $this->classmap_bubble_sort($classmap); + + if ($enableconstraints) { + foreach ($this->classmap as $parentrepositoryname => $parentclassinfo) { + list($parentrepositoryname, $parentclassname, $parentradical) = $this->get_class_information($parentrepositoryname); + + $classchildren = $this->get_class_children($parentclassname); + + foreach ($classchildren as $childrepositoryname => $childclassinformation) { + $this->try_add_cascade_delete_check( + $parentrepositoryname, + $childrepositoryname, + $parentradical . 'id', + 'id' + ); + } + } + } + + $this->read_all(); + } + + /** + * Class destructor method. + */ + public function __destruct() { + if ($this->committed === false) { + return $this->commit(); + } + } + + /** + * Getter method of the $maintablesettings property. + * + * @return array + */ + public function get_maintable_settings(): array { + return $this->maintablesettings; + } + + /** + * Getter method of the $classmap property. + * + * @return array + */ + public function get_classmap(): array { + return $this->classmap; + } + + /** + * Getter method of the $registry property. + * + * @return array + */ + public function get_registry(): array { + return $this->registry; + } + + /** + * Getter method of the $dirty property. + * + * @return array + */ + public function get_dirty(): array { + return $this->dirty; + } + + /** + * Getter method of the $stage property. + * + * @return array + */ + public function get_stage(): array { + return $this->stage; + } + + /** + * Getter method of the $committed property. + * + * @return bool + */ + public function is_committed(): bool { + return $this->committed; + } + + /** + * Method to check if the Unit of Work needs to commit any changes. + * + * @return bool + */ + public function is_dirty(): bool { + foreach ($this->dirty as $action) { + if (!empty($action)) { + return true; + } + } + + return false; + } + + /** + * Commit all changes to the database, using a single transaction. + * + * @return bool + */ + public function commit() { + if ($this->committed) { + return false; + } else { + $this->committed = true; + } + + try { + try { + $transaction = $this->db->start_delegated_transaction(); + + if (is_array($this->registry) && !empty($this->registry)) { + if (isset($this->dirty['create']) + && is_array($this->dirty['create']) + && !empty($this->dirty['create']) + ) { + foreach ($this->dirty['create'] as $repositoryname => &$entitynames) { + array_walk($entitynames, function(&$entityname) use (&$repositoryname) { + if ( + array_key_exists($entityname, $this->registry[$repositoryname]) + && !isset($this->registry[$repositoryname][$entityname]['persistent_object']->get_data()->id) + && !isset($this->registry[$repositoryname][$entityname]['hash']) + ) { + if (isset($this->registry[$repositoryname][$entityname]['parentuuid'])) { + $parentuuid = $this->registry[$repositoryname][$entityname]['parentuuid']; + + foreach ($this->registry as $subrepositoryname => $subitem) { + if (is_array($subitem)) { + array_walk($subitem, + function($subentity, $subentityname) + use ($repositoryname, $entityname, $parentuuid) { + if (is_array($subentity) + && isset($subentity['entityuuid']) + && $subentity['entityuuid'] === $parentuuid + ) { + $id = $subentity['persistent_object']->get_data()->id; + $propertyname = explode('_', $subentityname)[0] . 'id'; + + $this->registry[$repositoryname][$entityname]['persistent_object'] + ->set( + $propertyname, + $id + ); + } + } + ); + } + } + } + + $this->registry[$repositoryname][$entityname]['persistent_object']->create(); + + $this->stage[spl_object_id($this->registry[$repositoryname][$entityname]['persistent_object'])] + = &$this->registry[$repositoryname][$entityname]; + + unset($this->dirty['create'][$repositoryname][$entityname]); + } + }); + } + } + + if (isset($this->dirty['update']) + && is_array($this->dirty['update']) + && !empty($this->dirty['update']) + ) { + foreach ($this->dirty['update'] as $repositoryname => &$entitynames) { + array_walk($entitynames, function(&$entityname) use (&$repositoryname) { + if ( + array_key_exists($entityname, $this->registry[$repositoryname]) + && $this->registry[$repositoryname][$entityname]['persistent_object']->get_data()->id > 0 + && isset($this->registry[$repositoryname][$entityname]['hash']) + && $this->hash_sign( + json_encode($this->registry[$repositoryname][$entityname]['persistent_object']->get_data() + ) + !== $this->registry[$repositoryname][$entityname]['hash']) + ) { + $this->registry[$repositoryname][$entityname]['persistent_object']->update(); + + $this->stage[spl_object_id($this->registry[$repositoryname][$entityname]['persistent_object'])] + = &$this->registry[$repositoryname][$entityname]; + + unset($this->dirty['update'][$repositoryname][$entityname]); + } + }); + } + } + + if (isset($this->dirty['delete']) + && is_array($this->dirty['delete']) + && !empty($this->dirty['delete']) + ) { + foreach ($this->dirty['delete'] as $repositoryname => &$entitynames) { + array_walk($entitynames, function($entityname) use (&$repositoryname) { + $id = $this->registry[$repositoryname][$entityname]['persistent_object']->get_data()->id; + + if ( + array_key_exists($entityname, $this->registry[$repositoryname]) + && $id > 0 + && !isset($this->registry[$repositoryname][$entityname]['hash']) + ) { + $this->registry[$repositoryname][$entityname]['persistent_object']->delete(); + + $this->stage[spl_object_id($this->registry[$repositoryname][$entityname]['persistent_object'])] + = &$this->registry[$repositoryname][$entityname]; + + unset($this->dirty['delete'][$repositoryname][$entityname]); + } + }); + } + } + } + + $transaction->allow_commit(); + } catch (\Exception $e) { + // Make sure transaction is valid. + if (!empty($transaction) && !$transaction->is_disposed()) { + $transaction->rollback($e); + } + + \core\notification::error($e->getMessage()); + } + } catch (\Exception $e) { + \core\notification::error($e->getMessage()); + } + + foreach ($this->stage as &$committed) { + unset($committed); + } + + return $this->is_committed(); + } + + /** + * Export (hydrate and format) all registry data to an array that is compatible + * with the save() method. + * + * @return array|false + */ + public function export_data() { + if ($this->is_dirty() || $this->is_committed()) { + return false; + } else { + $data = []; + + foreach ($this->registry as $repositoryname => $repository) { + if (is_array($repository) && !empty($repository)) { + array_walk($repository, function($item, $entityname) use (&$data, &$repositoryname) { + $payload = json_decode(json_encode($item['persistent_object']->get_data()), true); + $data[] = [ + 'entityid' => $payload['id'], + 'repositoryname' => $repositoryname, + 'data' => $payload, + ]; + }); + } + } + + return $data; + } + } + + /** + * Save all data to the registry and mark the appropriate entities as dirty. + * + * @param array $data + * + * @return bool + */ + public function save(array $data) { + if (!$this->sort_dependencies()) { + return false; + } + + try { + foreach ($data as $datum) { + if (is_array($datum) && isset($datum['repositoryname'])) { + if (!isset($datum['entityid'])) { + if (list($repositoryname, $classname, $radical) = $this->get_class_information($datum['repositoryname'])) { + $newrecord = (object) $datum['data']; + $persistentobject = new $classname(null, $newrecord); + $tempid = spl_object_id($persistentobject); + + $this->registry[$repositoryname][$radical . '_' . $tempid] = [ + 'persistent_object' => $persistentobject, + 'entityuuid' => $datum['entityuuid'] ?? null, + 'parentuuid' => $datum['parentuuid'] ?? null, + ]; + + $this->dirty['create'][$repositoryname][] = $radical . '_' . $tempid; + } + } else if (isset($datum['entityid'])) { + foreach ($this->registry as $repositoryname => &$item) { + if ($repositoryname === $datum['repositoryname']) { + $id = (string) $datum['entityid']; + $datum['data']['id'] = $id; + + if (list($repositoryname, $classname, $radical) = $this->get_class_information($repositoryname)) { + if (array_key_exists($radical . '_' . $id, $item)) { + $propertynames = array_keys($datum['data']); + + foreach ($propertynames as $propertyname) { + $this->registry[$repositoryname][$radical . '_' . $id]['persistent_object'] + ->set($propertyname, $datum['data'][$propertyname]); + } + + $this->dirty['update'][$repositoryname][] = $radical . '_' . $id; + } + } + } + } + } + } + } + } catch (\Exception $e) { + return false; + } + + foreach ($this->registry as $repositoryname => &$item) { + if (is_array($item)) { + array_walk($item, function(&$entity, $entityname) use (&$repositoryname) { + if (list($repositoryname, $classname, $radical) = $this->get_class_information($repositoryname)) { + if ( + (isset($this->dirty['create'][$repositoryname]) + && !in_array($entityname, $this->dirty['create'][$repositoryname])) + && (isset($this->dirty['update'][$repositoryname]) + && !in_array($entityname, $this->dirty['update'][$repositoryname])) + ) { + $entity['hash'] = null; + + $this->dirty['delete'][$repositoryname][] = $entityname; + } + } + }); + } + } + + return true; + } + + /** + * Bubble sort the classmap array, based on parenthood. + * + * @param array $classmap + * + * @return bool Array was sorted + */ + protected function classmap_bubble_sort(array $classmap) { + $parentclassmap = []; + + foreach ($classmap as $classrepositoryname => $classinfo) { + if (empty($classinfo['parent_class_fqn'])) { + $parentclassmap[$classrepositoryname] = $classinfo; + + unset($classmap[$classrepositoryname]); + } + } + + $newclassmap = array_merge($parentclassmap, $classmap); + + $classmap = $newclassmap; + + $tempclassmap = []; + + foreach ($classmap as $classrepositoryname => $classinfo) { + $tempclassmap[] = array_merge(['classrepositoryname' => $classrepositoryname], $classinfo); + } + + $size = count($tempclassmap) - 1; + + for ($i = 0; $i < $size; $i++) { + for ($j = 0; $j < $size - $i; $j++) { + $k = $j + 1; + if ($tempclassmap[$j]['parent_class_fqn'] === $tempclassmap[$k]['class_fqn']) { + // Swap elements. + list($tempclassmap[$j], $tempclassmap[$k]) = [$tempclassmap[$k], $tempclassmap[$j]]; + } + } + } + + foreach ($tempclassmap as $classentity) { + $this->classmap[$classentity['classrepositoryname']] = $classentity; + + unset($this->classmap[$classentity['classrepositoryname']]['classrepositoryname']); + } + + return true; + } + + /** + * Get all child classes of a given parent class. + * + * @param string $classname + * + * @return array + */ + protected function get_class_children(string $classname) { + $children = []; + + foreach ($this->classmap as $classrepositoryname => $classinfo) { + if (isset($classinfo['parent_class_fqn']) && $classinfo['parent_class_fqn'] === $classname) { + $children[$classrepositoryname] = $this->get_class_information($classrepositoryname); + } + } + + return $children; + } + + /** + * Get all useful information concerning a particular class map. + * + * @param string $repositoryname + * + * @return array|false + */ + protected function get_class_information(string $repositoryname) { + if (!isset($this->classmap[$repositoryname])) { + return false; + } + + $classname = $this->classmap[$repositoryname]['class_fqn']; + $radical = array_reverse(explode('\\', $classname))[0]; + + return [$repositoryname, $classname, $radical]; + } + + /** + * Sign all committed entities with a hash. + * + * @param string $jsonstring + * + * @return false|string + */ + protected function hash_sign(string $jsonstring) { + return hash('sha512', $jsonstring); + } + + /** + * Read all the data from the database and store the corresponding entities to the registry. + * + * @return bool + * + * @throws \dml_exception + */ + protected function read_all() { + if (!empty($this->maintablename) && $this->maintablekey != 0 && empty($this->childparentidcolumnname)) { + $this->maintablesettings = $this->db->get_records($this->maintablename, ['id' => $this->maintablekey]); + } else if (!empty($this->maintablename) && $this->maintablekey != 0 && !empty($this->childparentidcolumnname)) { + $this->maintablesettings = $this->db->get_records($this->maintablename, ['id' => $this->maintablekey]); + } else { + $this->maintablesettings = []; + } + + if (is_array($this->maintablesettings) && !empty($this->maintablesettings)) { + foreach ($this->classmap as $repositoryname => $item) { + if (is_array($item) && empty($item['parent_class_fqn'])) { + if (list($repositoryname, $classname, $radical) = $this->get_class_information($repositoryname)) { + $table = $classname::TABLE; + + if ($table === $this->maintablename && empty($this->childparentidcolumnname)) { + $childparentidcolumnname = 'id'; + } else if (!empty($this->childparentidcolumnname)) { + $childparentidcolumnname = $this->childparentidcolumnname; + } else { + $childparentidcolumnname = $this->maintablename . 'id'; + } + + if (isset(reset($this->maintablesettings)->$childparentidcolumnname) + && !is_null(reset($this->maintablesettings)->$childparentidcolumnname) + ) { + $tablekey = reset($this->maintablesettings)->$childparentidcolumnname; + } else { + $tablekey = $this->maintablekey; + } + + if ($results = $this->db->get_records($table, [$childparentidcolumnname => $tablekey], 'id')) { + foreach ($results as $row) { + try { + $persistentobject = new $classname($row->id); + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + + $hash = $this->hash_sign(json_encode($persistentobject->get_data())); + + $this->registry[$repositoryname][$radical . '_' . $row->id] = [ + 'persistent_object' => $persistentobject, + 'hash' => $hash, + ]; + } + } + } + } + } + + foreach ($this->classmap as $repositoryname => $item) { + if (is_array($item) && !empty($item['parent_class_fqn'])) { + $parentclassfqn = $item['parent_class_fqn']; + + if (list($repositoryname, $classname, $radical) = $this->get_class_information($repositoryname)) { + foreach ($this->classmap as $parentname => $parentitem) { + if ($parentname !== $repositoryname && in_array($parentclassfqn, $parentitem)) { + $parentrepositoryname = $parentname; + } + } + + $table = $classname::TABLE; + $columnname = array_reverse(explode('_', $parentrepositoryname))[0] . 'id'; + + if (isset($this->registry[$parentrepositoryname]) && is_array($this->registry[$parentrepositoryname])) { + array_walk($this->registry[$parentrepositoryname], + function($parentitem, $parentname) + use ($repositoryname, $classname, $table, $radical, $columnname) { + $id = array_reverse(explode('_', $parentname))[0]; + + if ($results = $this->db->get_records($table, [$columnname => $id], 'id')) { + foreach ($results as $row) { + $persistentobject = new $classname($row->id); + $hash = $this->hash_sign(json_encode($persistentobject->get_data())); + + $this->registry[$repositoryname][$radical . '_' . $row->id] = [ + 'persistent_object' => $persistentobject, + 'hash' => $hash, + ]; + } + } + } + ); + } + } + } + } + + return true; + } else { + return false; + } + } + + /** + * Sort the class map by class dependencies. + * + * @return bool + */ + protected function sort_dependencies(): bool { + $dependencysortedarray = []; + + array_walk($this->classmap, function($classinfo, $repositoryname) use (&$dependencysortedarray) { + if (empty($classinfo['parent_class_fqn'])) { + $dependencysortedarray[$repositoryname] = []; + } else { + $dependencysortedarray[$repositoryname] = []; + } + }); + + foreach ($this->classmap as $repositoryname => $classinfo) { + $dependencysortedarray[$repositoryname] = []; + } + + $this->dirty['create'] = $dependencysortedarray; + $this->dirty['update'] = $dependencysortedarray; + $this->dirty['delete'] = array_reverse($dependencysortedarray); + + return true; + } + + /** + * Try to add a cascade delete to the database tables. + * + * @param string $parent The parent (one) table in the relationship. + * @param string $tablename The child (many) table in the relationship. + * @param string $fieldname The child field that may contain the parent id. + * @param string $indexname The indexname upon which to base the constraint name. + * + * @return bool Whether a relationship was added. + * + * @copyright 2015 Catalyst IT + * @author Nigel Cunningham + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + protected function try_add_cascade_delete($parent, $tablename, $fieldname, $indexname): bool { + try { + $this->db->execute( + "DELETE FROM {{$tablename}} WHERE NOT EXISTS ( + SELECT 1 FROM {{$parent}} WHERE {{$tablename}} . {$fieldname} = {{$parent}} . id)"); + + $this->db->execute("ALTER TABLE {{$tablename}} + ADD CONSTRAINT c_{{$tablename}}_{{$parent}}_{$indexname} + FOREIGN KEY ({$fieldname}) + REFERENCES {{$parent}}(id) + ON DELETE CASCADE"); + + return true; + } catch (\dml_write_exception $e) { + if (substr($e->error, -14) == "already exists") { + return true; + } else { + \core\notification::error( + "Failed ({$e->getMessage()}).\n" + ); + } + } catch (\dml_read_exception $e) { + // Trying to match fields of different types? + if (substr($e->error, 0, 32) == "ERROR: operator does not exist:") { + \core\notification::error( + "ID field from {$parent} table and {$fieldname} from {$tablename} have different data types.\n" + ); + } else if (substr($e->error, 0, 16) == "ERROR: relation") { + \core\notification::error( + "{$tablename} table missing?! Perhaps there's an upgrade to be done.\n" + ); + } else { + \core\notification::error( + "Failed ({$e->getMessage()}).\n" + ); + } + } + + return false; + } + + /** + * Try to remove a cascade delete to the database tables. + * + * @param string $parent The parent (one) table in the relationship. + * @param string $tablename The child (many) table in the relationship. + * @param string $fieldname The child field that may contain the parent id. + * @param string $indexname The indexname upon which to base the constraint name. + * + * @return bool Whether a relationship was removed. + * + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + protected function try_remove_cascade_delete($parent, $tablename, $fieldname, $indexname): bool { + try { + $this->db->execute("ALTER TABLE {{$tablename}} + DROP INDEX c_{{$tablename}}_{{$parent}}_{$indexname} + "); + + return true; + } catch (\dml_write_exception $e) { + \core\notification::error( + "Failed ({$e->getMessage()}).\n" + ); + } catch (\dml_read_exception $e) { + // Trying to match fields of different types? + if (substr($e->error, 0, 32) == "ERROR: operator does not exist:") { + \core\notification::error( + "ID field from {$parent} table and {$fieldname} from {$tablename} have different data types.\n" + ); + } else if (substr($e->error, 0, 16) == "ERROR: relation") { + \core\notification::error( + "{$tablename} table missing?! Perhaps there's an upgrade to be done.\n" + ); + } else { + \core\notification::error( + "Failed ({$e->getMessage()}).\n" + ); + } + } + + return false; + } + + /** + * Check if it is necessary to add a cascade delete constraint to the database tables. + * + * @param string $parent The parent (one) table in the relationship. + * @param string $tablename The child (many) table in the relationship. + * @param string $fieldname The child field that may contain the parent id. + * @param string $indexname The indexname upon which to base the constraint name. + * + * @return bool Whether a relationship was found or created. + * + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + public function try_add_cascade_delete_check($parent, $tablename, $fieldname, $indexname): bool { + $indexes = $this->db->get_indexes($tablename); + + if (!empty($indexes)) { + foreach ($indexes as $index => $details) { + if (isset($details['columns']) && in_array($fieldname, $details['columns'])) { + return true; + } + } + + return $this->try_add_cascade_delete($parent, $tablename, $fieldname, $indexname); + } else { + return $this->try_add_cascade_delete($parent, $tablename, $fieldname, $indexname); + } + } + + /** + * Check if it is necessary to remove a cascade delete constraint from the database tables. + * + * @return bool Whether a relationship was found and removed. + * + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + public function try_remove_cascade_delete_check(): bool { + foreach ($this->classmap as $parentrepositoryname => $parentclassinfo) { + list($parent, $parentclassname, $parentradical) = $this->get_class_information($parentrepositoryname); + $fieldname = $parentradical . 'id'; + $indexname = 'id'; + $classchildren = $this->get_class_children($parentclassname); + + foreach ($classchildren as $childrepositoryname => $childclassinformation) { + $tablename = $childrepositoryname; + + $indexes = $this->db->get_indexes($tablename); + + if (!empty($indexes)) { + foreach ($indexes as $index => $details) { + if (isset($details['columns']) && in_array($fieldname, $details['columns'])) { + return true; + } + } + + return $this->try_remove_cascade_delete($parent, $tablename, $fieldname, $indexname); + } else { + return $this->try_remove_cascade_delete($parent, $tablename, $fieldname, $indexname); + } + } + } + + return false; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3574d61 --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "andrewscaya/moodle-local_moodleorm", + "description": "Moodle ORM", + "license": "Apache-2.0", + "authors": [ + { + "name": "Andrew Caya", + "email": "andrewscaya@yahoo.ca" + } + ], + "type": "moodle-local", + "require": { + "composer/installers": "~1.0" + }, + "extra": { + "installer-name": "moodleorm" + } +} diff --git a/dist/classes/circuit.php.dist b/dist/classes/circuit.php.dist new file mode 100644 index 0000000..c91f2d6 --- /dev/null +++ b/dist/classes/circuit.php.dist @@ -0,0 +1,82 @@ +. + +/** + * Class for circuit persistence. + * + * @package local_moodleorm + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_moodleorm; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing a circuit from the DB. + * + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class circuit extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'moodleorm_circuit'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'waveid' => array( + 'type' => PARAM_INT, + ), + 'timestart' => array( + 'type' => PARAM_INT, + ), + 'timeend' => array( + 'type' => PARAM_INT, + ), + 'timecreated' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + 'timemodified' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + ); + } +} diff --git a/dist/classes/form/editform.php.dist b/dist/classes/form/editform.php.dist new file mode 100644 index 0000000..e58d6c2 --- /dev/null +++ b/dist/classes/form/editform.php.dist @@ -0,0 +1,1472 @@ +. + +/** + * The definition of local_moodleorm's 'edit' form. + * + * @package local_moodleorm + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_moodleorm\form; + +use local_moodleorm\substation; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + +/** + * The editform class. + * + * This class is this plugin's 'edit' form. + */ +class editform extends \moodleform { + + /** + * Array that contains the current Unit of Work classmap. + * + * @var array + */ + protected $currentclassmap; + + /** + * Object of stdClass of the current course. + * + * @var \stdClass + */ + protected $currentcourse; + + /** + * Array that contains the current registry from the Unit of Work. + * + * @var array + */ + protected $currentormregistry = []; + + /** + * Array that contains the current moodleorm groups and their respective members. + * + * @var array + */ + protected $groups = []; + + /** + * Array that contains the current moodleorm roles with their respective ids as keys. + * + * @var array + */ + protected $roles = []; + + /** + * Gets the currentclassmap property. + * + * @return array + */ + public function get_current_classmap(): array { + return $this->currentclassmap; + } + + /** + * Sets the currentclassmap property. + * + * @param array $currentclassmap + * @return editform + */ + public function set_current_classmap(array $currentclassmap): editform { + $this->currentclassmap = $currentclassmap; + return $this; + } + + /** + * Gets the currentcourse property. + * + * @return \stdClass + */ + public function get_current_course(): \stdClass { + return $this->currentcourse; + } + + /** + * Sets the currentcourse property. + * + * @param \stdClass $currentcourse + * + * @return editform + */ + public function set_current_course(\stdClass $currentcourse): editform { + $this->currentcourse = $currentcourse; + return $this; + } + + /** + * Gets the contents of the currentormregistry property. + * + * @return array + */ + public function get_current_orm_registry(): array { + return $this->currentormregistry; + } + + /** + * Sets the contents of the currentormregistry property. + * + * @param array $currentormregistry + * + * @return editform + */ + public function set_current_orm_registry(array $currentormregistry): editform { + $this->currentormregistry = $currentormregistry; + return $this; + } + + /** + * Gets the groups array. + * + * @return array + */ + public function get_groups(): array { + return $this->groups; + } + + /** + * Gets the roles array. + * + * @return array + */ + public function get_roles(): array { + return $this->roles; + } + + /** + * Gets the form's translated strings. + * + * @return array + * + * @throws \coding_exception + */ + public function get_translations() { + return [ + 'buttonaddwave' => get_string('buttonaddwave', 'local_moodleorm'), + 'buttondeletewave' => get_string('buttondeletewave', 'local_moodleorm'), + 'buttonaddcircuit' => get_string('buttonaddcircuit', 'local_moodleorm'), + 'buttondeletecircuit' => get_string('buttondeletecircuit', 'local_moodleorm'), + 'buttonaddstation' => get_string('buttonaddstation', 'local_moodleorm'), + 'buttondeletestation' => get_string('buttondeletestation', 'local_moodleorm'), + 'buttonaddparticipant' => get_string('buttonaddparticipant', 'local_moodleorm'), + 'buttondeleteparticipant' => get_string('buttondeleteparticipant', 'local_moodleorm'), + 'circuitname' => get_string('circuitname', 'local_moodleorm'), + 'circuitbegins' => get_string('circuitbegins', 'local_moodleorm'), + 'circuitends' => get_string('circuitends', 'local_moodleorm'), + 'stationaddinfo' => get_string('stationaddinfo', 'local_moodleorm'), + 'stationgroup' => get_string('stationgroup', 'local_moodleorm'), + 'stationintroroom' => get_string('stationintroroom', 'local_moodleorm'), + 'stationname' => get_string('stationname', 'local_moodleorm'), + 'substationparticipant' => get_string('substationparticipant', 'local_moodleorm'), + 'substationstation' => get_string('substationstation', 'local_moodleorm'), + 'substationrole' => get_string('substationrole', 'local_moodleorm'), + 'wavename' => get_string('wavename', 'local_moodleorm'), + ]; + } + + /** + * Dummy stub method - override if you needed to perform some extra validation. + * If there are errors return array of errors ("fieldname"=>"error message"), + * otherwise true if ok. + * + * Server side rules do not work for uploaded files, implement serverside rules here if needed. + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK (true allowed for backwards compatibility too). + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + // Make sure all date fields are set properly. + foreach ($data as $key => $element) { + if (preg_match('/-moodleorm_circuit_\d+-timestart$/s', $key)) { + $timestartfield = $key; + $timeendfield = str_replace('timestart', 'timeend', $timestartfield); + $timestartfieldunix = strtotime($_POST[$timestartfield]); + $timeendfieldunix = strtotime($_POST[$timeendfield]); + + if ($timestartfieldunix >= $timeendfieldunix) { + $errors[$timeendfield] = get_string('errorinvalidtstartenddates', 'local_moodleorm'); + } + } + } + + return $errors; + } + + /** + * Moodle Form method that is called after definition(), data submission and set_data(). + * + * All form setup that is dependent on form values should go in here. + */ + public function definition_after_data() { + parent::definition_after_data(); + + if (!empty($_POST)) { + $classmap = $this->get_current_classmap(); + + $classmapkeysarray = array_keys($classmap); + + $fieldrootnames = array_reverse($classmapkeysarray); + + $postarraykeys = array_keys($_POST); + + // Remove any duplicates of the userid field within the same station. + $substationuseridfields = preg_grep('/-moodleorm_substation_\d+-userid$/s', $postarraykeys); + + sort($substationuseridfields); + + foreach ($substationuseridfields as $substationuseridfieldfirst) { + foreach ($substationuseridfields as $substationuseridfieldsecond) { + $substationpatternfirst = substr( + $substationuseridfieldfirst, + 0, + strrpos($substationuseridfieldfirst, '_') + ); + + $substationpatternsecond = substr( + $substationuseridfieldsecond, + 0, + strrpos($substationuseridfieldsecond, '_') + ); + + if ( + $substationuseridfieldfirst !== $substationuseridfieldsecond + && $substationpatternfirst === $substationpatternsecond + ) { + if ( + isset($_POST[$substationuseridfieldfirst]) + && isset($_POST[$substationuseridfieldsecond]) + && $_POST[$substationuseridfieldfirst] === $_POST[$substationuseridfieldsecond] + ) { + $substationpattern = substr( + $substationuseridfieldfirst, + 0, + strrpos($substationuseridfieldfirst, '-') + ); + + $substationfields = preg_grep('/' . $substationpattern . '/s', $postarraykeys); + + foreach ($substationfields as $substationsinglefield) { + unset($_POST[$substationsinglefield]); + + $substationfieldkey = array_search($substationsinglefield, $postarraykeys); + + unset($postarraykeys[$substationfieldkey]); + } + } + } + } + } + + // Set the validation rules for each form field. + foreach ($postarraykeys as $formfieldkey => $formfieldname) { + foreach ($fieldrootnames as $fieldrootname) { + if (preg_match('/^.*' . $fieldrootname . '_\d+-[a-z]+$/', $formfieldname) != 0) { + $this->add_form_elements($fieldrootname, $formfieldname); + + unset($postarraykeys[$formfieldkey]); + } + } + } + } + } + + /** + * Exports the validated form data object from moodleform's get_data() + * method and converts it into an array format that is compatible with + * the Unit of Work's save() method. + * + * @param \stdClass $validformdata + * + * @return array + */ + public function export_valid_data_to_orm(\stdClass $validformdata): array { + $newvalidformdata = []; + + $classmap = $this->get_current_classmap(); + + $classmapkeysarray = array_keys($classmap); + + $fieldrootnames = array_reverse($classmapkeysarray); + + $validformdataarray = json_decode(json_encode($validformdata), true); + + if (!empty($validformdataarray)) { + $postarraykeys = array_keys($validformdataarray); + + $registry = []; + + foreach ($postarraykeys as $formfieldkey => $formfieldname) { + foreach ($fieldrootnames as $fieldrootname) { + if (preg_match('/^.*' . $fieldrootname . '_\d+-[a-z]+$/', $formfieldname) != 0) { + $formfieldnamearrayreversed = array_reverse(explode('-', $formfieldname)); + $fieldname = $formfieldnamearrayreversed[0]; + $end = strrpos($formfieldname, '-'); + + if ($end !== false) { + $fieldgroupname = substr($formfieldname, 0, $end); + } + + $registry[$fieldrootname][$fieldgroupname][$fieldname] = $validformdataarray[$formfieldname]; + unset($postarraykeys[$formfieldkey]); + } + } + } + + if (isset($registry['moodleorm_wave']) && !empty($registry['moodleorm_wave'])) { + foreach ($registry['moodleorm_wave'] as $wavekey => $wave) { + $waveid = ctype_digit($wave['id'][0]) ? (int) $wave['id'] : $wave['id']; + + $newvalidformdata[$wavekey] = [ + 'entityid' => is_int($waveid) ? $waveid : null, + 'entityuuid' => !is_int($waveid) ? $waveid : null, + 'parentuuid' => null, + 'repositoryname' => 'moodleorm_wave', + 'data' => [ + 'moodleormid' => $wave['moodleormid'], + 'name' => $wave['name'], + 'description' => $wave['description'], + 'timemodified' => time(), + ], + ]; + + if (is_null($newvalidformdata[$wavekey]['entityid'])) { + unset($newvalidformdata[$wavekey]['entityid']); + } + } + + if (isset($registry['moodleorm_circuit']) && !empty($registry['moodleorm_circuit'])) { + foreach ($registry['moodleorm_circuit'] as $circuitkey => $circuit) { + $circuitid = ctype_digit($circuit['id'][0]) ? (int) $circuit['id'] : $circuit['id']; + + $newvalidformdata[$circuitkey] = [ + 'entityid' => is_int($circuitid) ? $circuitid : null, + 'entityuuid' => !is_int($circuitid) ? $circuitid : null, + 'parentuuid' => !ctype_digit($circuit['waveid']) ? $circuit['waveid'] : null, + 'repositoryname' => 'moodleorm_circuit', + 'data' => [ + 'waveid' => $circuit['waveid'], + 'name' => $circuit['name'], + 'description' => $circuit['description'], + 'timestart' => strtotime($circuit['timestart']), + 'timeend' => strtotime($circuit['timeend']), + ], + ]; + + if (is_null($newvalidformdata[$circuitkey]['entityid'])) { + unset($newvalidformdata[$circuitkey]['entityid']); + } + } + } + + if (isset($registry['moodleorm_station']) && !empty($registry['moodleorm_station'])) { + foreach ($registry['moodleorm_station'] as $stationkey => $station) { + $stationid = ctype_digit($station['id'][0]) ? (int) $station['id'] : $station['id']; + + $newvalidformdata[$stationkey] = [ + 'entityid' => is_int($stationid) ? $stationid : null, + 'entityuuid' => !is_int($stationid) ? $stationid : null, + 'parentuuid' => !ctype_digit($station['circuitid']) ? $station['circuitid'] : null, + 'repositoryname' => 'moodleorm_station', + 'data' => [ + 'circuitid' => $station['circuitid'], + 'name' => $station['name'], + 'description' => $station['description'], + 'groupid' => $station['groupid'], + 'introroom' => $station['introroom'], + 'addinfo' => $station['addinfo'], + ], + ]; + + if (is_null($newvalidformdata[$stationkey]['entityid'])) { + unset($newvalidformdata[$stationkey]['entityid']); + } + } + } + + if (isset($registry['moodleorm_substation']) + && !empty($registry['moodleorm_substation']) + ) { + foreach ($registry['moodleorm_substation'] as $substationkey => $substation) { + $substationid = ctype_digit($substation['id'][0]) ? (int) $substation['id'] : $substation['id']; + + $newvalidformdata[$substationkey] = [ + 'entityid' => is_int($substationid) ? $substationid : null, + 'entityuuid' => !is_int($substationid) ? $substationid : null, + 'parentuuid' => !ctype_digit($substation['stationid']) ? $substation['stationid'] : null, + 'repositoryname' => 'moodleorm_substation', + 'data' => [ + 'stationid' => $substation['stationid'], + 'name' => $substation['name'], + 'userid' => $substation['userid'], + 'role' => $substation['role'], + ], + ]; + + if (is_null($newvalidformdata[$substationkey]['entityid'])) { + unset($newvalidformdata[$substationkey]['entityid']); + } + } + } + } + } + + return $newvalidformdata; + } + + /** + * Method that makes it possible to create form elements without adding them + * to the moodleform object. The resulting array is formatted to be compatible + * with the form's PHTML template. + * + * @return array + */ + public function render_form_elements(): array { + return $this->post_definition(); + } + + /** + * Method that adds Moodle form elements for the purpose of validating them only. + * + * @param string $fieldrootname + * @param string $formfieldname + * + * @throws \coding_exception + */ + protected function add_form_elements(string $fieldrootname, string $formfieldname) { + $mform = &$this->_form; + + if ($fieldrootname === 'moodleorm_wave') { + $fieldsuffix = array_reverse(explode('-', $formfieldname))[0]; + + if ($fieldsuffix === 'id') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + } else if ($fieldsuffix === 'name') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_TEXT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'description') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + } else if ($fieldsuffix === 'moodleormid') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } + } else if ($fieldrootname === 'moodleorm_circuit') { + $fieldsuffix = array_reverse(explode('-', $formfieldname))[0]; + + if ($fieldsuffix === 'id') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'name') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'description') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + } else if ($fieldsuffix === 'waveid') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'timestart') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + $mform->addRule( + $formfieldname, + get_string('errorinvaliddate', 'calendar'), + 'regex', + '/^\d{4}\-\d{2}-\d{2}\s\d{2}:\d{2}$/' + ); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'timeend') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + $mform->addRule( + $formfieldname, + get_string('errorinvaliddate', 'calendar'), + 'regex', + '/^\d{4}\-\d{2}-\d{2}\s\d{2}:\d{2}$/' + ); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } + } else if ($fieldrootname === 'moodleorm_station') { + $fieldsuffix = array_reverse(explode('-', $formfieldname))[0]; + + if ($fieldsuffix === 'id') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'name') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'description') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + } else if ($fieldsuffix === 'circuitid') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'groupid') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'introroom') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + } else if ($fieldsuffix === 'addinfo') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + } + } else if ($fieldrootname === 'moodleorm_substation') { + $fieldsuffix = array_reverse(explode('-', $formfieldname))[0]; + + if ($fieldsuffix === 'id') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'name') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_CLEANHTML); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'stationid') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'userid') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } else if ($fieldsuffix === 'role') { + $mform->addElement('hidden', $formfieldname); + $mform->setType($formfieldname, PARAM_INT); + $mform->addRule($formfieldname, get_string('requiredelement', 'form'), 'required'); + } + } + } + + /** + * The form's main definition method. + * + * @throws \coding_exception + */ + protected function definition() { + if (isset($this->_customdata['currentclassmap'])) { + $this->set_current_classmap($this->_customdata['currentclassmap']); + } + + if (isset($this->_customdata['currentcourse'])) { + $this->set_current_course($this->_customdata['currentcourse']); + } + + if (isset($this->_customdata['currentormregistry'])) { + $this->set_current_orm_registry($this->_customdata['currentormregistry']); + } + + // Default moodleorm role ids and values. + $this->roles = [ + substation::MOODLEORM_ROLE_OBSERVER => get_string('substationroleobserver', 'local_moodleorm'), + substation::MOODLEORM_ROLE_HOTSEAT => get_string('substationrolehotseat', 'local_moodleorm'), + substation::MOODLEORM_ROLE_COLDSEAT => get_string('substationrolecoldseat', 'local_moodleorm'), + ]; + } + + /** + * Method that creates form elements without adding them to moodleform. + * + * @return array + * + * @throws \coding_exception + */ + protected function post_definition(): array { + $mform = &$this->_form; + + $submitted = $this->is_submitted(); + + $formelements = []; + + if ($submitted && !$this->is_validated()) { + if (!empty($_POST)) { + $formdata = $this->prepare_form_data_for_display($_POST); + } else { + $formdata = $this->prepare_orm_registry_for_form($this->get_current_orm_registry()); + } + } else { + $formdata = $this->prepare_orm_registry_for_form($this->get_current_orm_registry()); + } + + $groups = groups_get_all_groups($this->get_current_course()->id); + + $groupsmembers = []; + + $listofgroups = []; + + if (!empty($groups)) { + foreach ($groups as &$group) { + $listofgroups[$group->id] = $group->name; + + $groupsmembers[$group->id] = $group; + $groupusers = groups_get_members($group->id); + + if (!empty($groupusers)) { + foreach ($groupusers as $user) { + $groupsmembers[$group->id]->members[$user->id] = $user; + } + } else { + unset($groupsmembers[$group->id]); + unset($listofgroups[$group->id]); + } + } + } + + $this->groups = $groupsmembers; + + if (!empty($formdata)) { + $wavetempid = 1; + + if (is_array(current($formdata)) && !empty(current($formdata))) { + foreach ($formdata as $wave) { + $tempdatacircuit = []; + + if (is_array($wave['wave_circuits']) && !empty($wave['wave_circuits'])) { + $circuittempid = 1; + + foreach ($wave['wave_circuits'] as $circuit) { + $tempdatastation = []; + + if (is_array($circuit['circuit_stations']) && !empty($circuit['circuit_stations'])) { + $stationtempid = 1; + + foreach ($circuit['circuit_stations'] as $station) { + $listofgroupstemp = $listofgroups; + + $defaultgroup = $station['station_groupid']; + + $listofgroupsdefault = array_intersect_key($listofgroupstemp, array_flip([$defaultgroup])); + + // Make sure that the default group was found, before unsetting it in the original array. + if (isset($listofgroupsdefault[$defaultgroup])) { + unset($listofgroupstemp[$defaultgroup]); + } + + ksort($listofgroupstemp); + + $listofgroupsordered = $listofgroupsdefault + $listofgroupstemp; + + $tempdatasubstation = []; + + if (is_array($station['station_substations']) && !empty($station['station_substations'])) { + $substationtempid = 1; + + foreach ($station['station_substations'] as $substation) { + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-moodleorm_substation_' + . $substationtempid + . '-id'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $substationformrefid = &$mform->createElement( + 'hidden', + $elementname, + $substation['substation_id'] + ); + + $substationformrefid->setValue($substation['substation_id']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-moodleorm_substation_' + . $substationtempid + . '-stationid'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $substationformrefstationid = &$mform->createElement( + 'hidden', + $elementname, + $substation['substation_stationid'] + ); + + $substationformrefstationid->setValue($substation['substation_stationid']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-moodleorm_substation_' + . $substationtempid + . '-name'; + + $elementerror = $mform->getElementError($elementname); + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + if (!is_null($elementerror)) { + $substationformrefname = new custominvalidelement($elementname, $elementerror, + $substation['substation_name']); + } else { + $substationformrefname = &$mform->createElement( + 'text', + $elementname, + $substation['substation_name'] + ); + + $substationformrefname->setValue($substation['substation_name']); + } + + $listofmembers = []; + + if ( + isset($groupsmembers[$defaultgroup]->members) + && !empty($groupsmembers[$defaultgroup]->members) + ) { + foreach ($groupsmembers[$defaultgroup]->members as $defaultgroupmember) { + $listofmembers[$defaultgroupmember->id] = + $defaultgroupmember->firstname . ' ' . $defaultgroupmember->lastname; + } + } + + $defaultuser = $substation['substation_userid']; + + $listofmembersdefault = array_intersect_key($listofmembers, array_flip([$defaultuser])); + + // Make sure that the default group was found, before unsetting it. + if (isset($listofmembersdefault[$defaultuser])) { + unset($listofmembers[$defaultuser]); + } + + ksort($listofmembers); + + $listofmembersordered = $listofmembersdefault + $listofmembers; + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-moodleorm_substation_' + . $substationtempid + . '-userid'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $substationformrefuserid = &$mform->createElement( + 'select', + $elementname, + $substation['substation_userid'], + $listofmembersordered + ); + + $substationformrefuserid->setValue($substation['substation_userid']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-moodleorm_substation_' + . $substationtempid + . '-role'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $substationformrefrole = &$mform->createElement( + 'select', + $elementname, + $substation['substation_role'], + $this->get_roles() + ); + + $substationformrefrole->setValue($substation['substation_role']); + + $tempdatasubstation[] = [ + 'substation_id' => $substation['substation_id'], + 'substation_repositoryname' => 'moodleorm_substation', + 'substation_name' => strtoupper($substation['substation_name']), + 'substation_form_ref' => [ + 'id' => $substationformrefid, + 'stationid' => $substationformrefstationid, + 'name' => $substationformrefname, + 'userid' => $substationformrefuserid, + 'role' => $substationformrefrole, + ], + ]; + + $substationtempid++; + } + } + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-id'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $stationformrefid = &$mform->createElement( + 'hidden', + $elementname, + $station['station_id'] + ); + + $stationformrefid->setValue($station['station_id']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-circuitid'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $stationformrefcircuitid = &$mform->createElement( + 'hidden', + $elementname, + $station['station_circuitid'] + ); + + $stationformrefcircuitid->setValue($station['station_circuitid']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-name'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $stationformrefname = &$mform->createElement( + 'hidden', + $elementname, + $station['station_name'] + ); + + $stationformrefname->setValue($station['station_name']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-description'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $stationformrefdescription = &$mform->createElement( + 'hidden', + $elementname, + $station['station_description'] + ); + + $stationformrefdescription->setValue($station['station_description']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-groupid'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $stationformrefgroupid = &$mform->createElement( + 'select', + $elementname, + $station['station_groupid'], + $listofgroupsordered + ); + + $stationformrefgroupid->setValue($station['station_groupid']); + + $stationformrefgroupid->setLabel(get_string('stationgroup', 'local_moodleorm')); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-introroom'; + + $elementerror = $mform->getElementError($elementname); + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + if (!is_null($elementerror)) { + $stationformrefintroroom = + new custominvalidelement($elementname, $elementerror, $station['station_introroom']); + } else { + $stationformrefintroroom = &$mform->createElement( + 'text', + $elementname, + $station['station_introroom'] + ); + + $stationformrefintroroom->setValue($station['station_introroom']); + } + + $stationformrefintroroom->setLabel(get_string('stationintroroom', 'local_moodleorm')); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-moodleorm_station_' + . $stationtempid + . '-addinfo'; + + $elementerror = $mform->getElementError($elementname); + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + if (!is_null($elementerror)) { + $stationformrefaddinfo = + new custominvalidelement($elementname, $elementerror, $station['station_addinfo']); + } else { + $stationformrefaddinfo = &$mform->createElement( + 'textarea', + $elementname, + $station['station_addinfo'] + ); + + $stationformrefaddinfo->setValue($station['station_addinfo']); + } + + $stationformrefaddinfo->setLabel(get_string('stationaddinfo', 'local_moodleorm')); + + $tempdatastation[] = [ + 'station_id' => $station['station_id'], + 'station_repositoryname' => 'moodleorm_station', + 'station_name' => strtoupper($station['station_name']), + 'station_form_ref' => [ + 'id' => $stationformrefid, + 'circuitid' => $stationformrefcircuitid, + 'name' => $stationformrefname, + 'description' => $stationformrefdescription, + 'groupid' => $stationformrefgroupid, + 'introroom' => $stationformrefintroroom, + 'addinfo' => $stationformrefaddinfo, + ], + 'station_substations' => $tempdatasubstation ?? null, + ]; + + $stationtempid++; + } + } + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-id'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $circuitformrefid = &$mform->createElement( + 'hidden', + $elementname, + $circuit['circuit_id'] + ); + + $circuitformrefid->setValue($circuit['circuit_id']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-waveid'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $circuitformrefwaveid = &$mform->createElement( + 'hidden', + $elementname, + $circuit['circuit_waveid'] + ); + + $circuitformrefwaveid->setValue($circuit['circuit_waveid']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-name'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $circuitformrefname = &$mform->createElement( + 'hidden', + $elementname, + $circuit['circuit_name'] + ); + + $circuitformrefname->setValue($circuit['circuit_name']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-description'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $circuitformrefdescription = &$mform->createElement( + 'hidden', + $elementname, + $circuit['circuit_description'] + ); + + $circuitformrefdescription->setValue($circuit['circuit_description']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-timestart'; + + $elementerror = $mform->getElementError($elementname); + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + if (!is_null($elementerror)) { + $circuitformreftimestart = new custominvalidelement($elementname, $elementerror, + date('Y-m-d H:i', $circuit['circuit_timestart'])); + } else { + $circuitformreftimestart = &$mform->createElement( + 'text', + $elementname + ); + + $circuitformreftimestart->setValue( + date('Y-m-d H:i', $circuit['circuit_timestart']) + ); + } + + $circuitformreftimestart->setLabel(get_string('circuitbegins', 'local_moodleorm')); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleorm_circuit_' + . $circuittempid + . '-timeend'; + + $elementerror = $mform->getElementError($elementname); + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + if (!is_null($elementerror)) { + $circuitformreftimeend = new custominvalidelement($elementname, $elementerror, + date('Y-m-d H:i', $circuit['circuit_timeend'])); + } else { + $circuitformreftimeend = &$mform->createElement( + 'text', + $elementname + ); + + $circuitformreftimeend->setValue( + date('Y-m-d H:i', $circuit['circuit_timeend']) + ); + } + + $circuitformreftimeend->setLabel(get_string('circuitends', 'local_moodleorm')); + + $tempdatacircuit[] = [ + 'circuit_id' => $circuit['circuit_id'], + 'circuit_repositoryname' => 'moodleorm_circuit', + 'circuit_name' => strtoupper($circuit['circuit_name']), + 'circuit_form_ref' => [ + 'id' => $circuitformrefid, + 'waveid' => $circuitformrefwaveid, + 'name' => $circuitformrefname, + 'description' => $circuitformrefdescription, + 'timestart' => $circuitformreftimestart, + 'timeend' => $circuitformreftimeend, + ], + 'circuit_stations' => $tempdatastation ?? null, + ]; + + $circuittempid++; + } + } + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-id'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $waveformrefid = &$mform->createElement( + 'hidden', + $elementname, + $wave['wave_id'] + ); + + $waveformrefid->setValue($wave['wave_id']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-name'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $waveformrefname = &$mform->createElement( + 'hidden', + $elementname, + $wave['wave_name'] + ); + + $waveformrefname->setValue($wave['wave_name']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-description'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $waveformrefdescription = &$mform->createElement( + 'hidden', + $elementname, + $wave['wave_description'] + ); + + $waveformrefdescription->setValue($wave['wave_description']); + + $elementname = 'moodleorm_wave_' + . $wavetempid + . '-moodleormid'; + + $submitted && isset($this->_form->_elementIndex[$elementname]) + ? $mform->removeElement($elementname) + : false; + + $waveformrefmoodleormid = &$mform->createElement( + 'hidden', + $elementname, + $wave['wave_moodleormid'] + ); + + $waveformrefmoodleormid->setValue($wave['wave_moodleormid']); + + $formelements[] = [ + 'wave_id' => $wave['wave_id'], + 'wave_repositoryname' => 'moodleorm_wave', + 'wave_name' => strtoupper($wave['wave_name']), + 'wave_form_ref' => [ + 'id' => $waveformrefid, + 'name' => $waveformrefname, + 'description' => $waveformrefdescription, + 'moodleormid' => $waveformrefmoodleormid, + ], + 'wave_circuits' => $tempdatacircuit ?? null, + ]; + + $wavetempid++; + } + } + } + + return $formelements; + } + + /** + * Method that formats an array of validated form elements in order to make + * the array compatible with the form's main PHTML template. + * + * @param array $post + * + * @return array + */ + protected function prepare_form_data_for_display(array $post): array { + $classmap = $this->get_current_classmap(); + + $classmapkeysarray = array_keys($classmap); + + $fieldrootnames = array_reverse($classmapkeysarray); + + $postarraykeys = array_keys($post); + + $registry = []; + + foreach ($postarraykeys as $formfieldkey => $formfieldname) { + foreach ($fieldrootnames as $fieldrootname) { + if (preg_match('/^.*' . $fieldrootname . '_\d+-[a-z]+$/', $formfieldname) != 0) { + $formfieldnamearrayreversed = array_reverse(explode('-', $formfieldname)); + $fieldname = $formfieldnamearrayreversed[0]; + $end = strrpos($formfieldname, '-'); + + if ($end !== false) { + $fieldgroupname = substr($formfieldname, 0, $end); + } + + $registry[$fieldrootname][$fieldgroupname][$fieldname] = $_POST[$formfieldname]; + unset($postarraykeys[$formfieldkey]); + } + } + } + + $formdata = []; + + if (isset($registry['moodleorm_wave']) && !empty($registry['moodleorm_wave'])) { + foreach ($registry['moodleorm_wave'] as $wave) { + $tempdatacircuit = []; + + $waveid = $wave['id']; + + if (isset($registry['moodleorm_circuit']) && !empty($registry['moodleorm_circuit'])) { + foreach ($registry['moodleorm_circuit'] as $circuit) { + $tempdatastation = []; + + if ($circuit['waveid'] === $waveid) { + $circuitid = $circuit['id']; + + if (isset($registry['moodleorm_station']) && !empty($registry['moodleorm_station'])) { + foreach ($registry['moodleorm_station'] as $station) { + $tempdatasubstation = []; + + if ($station['circuitid'] === $circuitid) { + $stationid = $station['id']; + + if (isset($registry['moodleorm_substation']) + && !empty($registry['moodleorm_substation']) + ) { + foreach ($registry['moodleorm_substation'] as $substation) { + if ($substation['stationid'] === $stationid) { + $substationid = $substation['id']; + + $tempdatasubstation[] = [ + 'substation_id' => $substationid, + 'substation_stationid' => $substation['stationid'], + 'substation_name' => $substation['name'], + 'substation_userid' => $substation['userid'], + 'substation_role' => $substation['role'], + ]; + } + } + } + + $tempdatastation[] = [ + 'station_id' => $stationid, + 'station_circuitid' => $station['circuitid'], + 'station_name' => $station['name'], + 'station_description' => $station['description'], + 'station_groupid' => $station['groupid'], + 'station_introroom' => $station['introroom'], + 'station_addinfo' => $station['addinfo'], + 'station_substations' => $tempdatasubstation ?? null, + ]; + } + } + } + + $tempdatacircuit[] = [ + 'circuit_id' => $circuitid, + 'circuit_waveid' => $circuit['waveid'], + 'circuit_name' => $circuit['name'], + 'circuit_description' => $circuit['description'], + 'circuit_timestart' => strtotime($circuit['timestart']), + 'circuit_timeend' => strtotime($circuit['timeend']), + 'circuit_stations' => $tempdatastation ?? null, + ]; + } + } + } + + $formdata[] = [ + 'wave_id' => $waveid, + 'wave_moodleormid' => $wave['moodleormid'], + 'wave_name' => $wave['name'], + 'wave_description' => $wave['description'], + 'wave_circuits' => $tempdatacircuit ?? null, + ]; + } + } + + return $formdata; + } + + /** + * Method that converts the ORM registry to an array format that is compatible + * with the form's PHTML template. + * + * @param array $registry + * + * @return array + */ + protected function prepare_orm_registry_for_form(array $registry): array { + $formdata = []; + + if (isset($registry['moodleorm_wave']) && !empty($registry['moodleorm_wave'])) { + foreach ($registry['moodleorm_wave'] as $wave) { + $tempdatacircuit = []; + + $waveid = $wave['persistent_object']->get('id'); + + if (isset($registry['moodleorm_circuit']) && !empty($registry['moodleorm_circuit'])) { + foreach ($registry['moodleorm_circuit'] as $circuit) { + $tempdatastation = []; + + if ($circuit['persistent_object']->get('waveid') === $waveid) { + $circuitid = $circuit['persistent_object']->get('id'); + + if (isset($registry['moodleorm_station']) && !empty($registry['moodleorm_station'])) { + foreach ($registry['moodleorm_station'] as $station) { + $tempdatasubstation = []; + + if ($station['persistent_object']->get('circuitid') === $circuitid) { + $stationid = $station['persistent_object']->get('id'); + + if (isset($registry['moodleorm_substation']) && + !empty($registry['moodleorm_substation']) + ) { + foreach ($registry['moodleorm_substation'] as $substation) { + if ($substation['persistent_object']->get('stationid') === $stationid) { + $substationid = $substation['persistent_object']->get('id'); + + $tempdatasubstation[] = [ + 'substation_id' => + $substationid, + 'substation_stationid' => + $substation['persistent_object']->get('stationid'), + 'substation_name' => + $substation['persistent_object']->get('name'), + 'substation_userid' => + $substation['persistent_object']->get('userid'), + 'substation_role' => + $substation['persistent_object']->get('role'), + ]; + } + } + } + + $tempdatastation[] = [ + 'station_id' => $stationid, + 'station_circuitid' => $station['persistent_object']->get('circuitid'), + 'station_name' => $station['persistent_object']->get('name'), + 'station_description' => $station['persistent_object']->get('description'), + 'station_groupid' => $station['persistent_object']->get('groupid'), + 'station_introroom' => $station['persistent_object']->get('introroom'), + 'station_addinfo' => $station['persistent_object']->get('addinfo'), + 'station_substations' => $tempdatasubstation ?? null, + ]; + } + } + } + + $tempdatacircuit[] = [ + 'circuit_id' => $circuitid, + 'circuit_waveid' => $circuit['persistent_object']->get('waveid'), + 'circuit_name' => $circuit['persistent_object']->get('name'), + 'circuit_description' => $circuit['persistent_object']->get('description'), + 'circuit_timestart' => $circuit['persistent_object']->get('timestart'), + 'circuit_timeend' => $circuit['persistent_object']->get('timeend'), + 'circuit_stations' => $tempdatastation ?? null, + ]; + } + } + } + + $formdata[] = [ + 'wave_id' => $waveid, + 'wave_moodleormid' => $wave['persistent_object']->get('moodleormid'), + 'wave_name' => $wave['persistent_object']->get('name'), + 'wave_description' => $wave['persistent_object']->get('description'), + 'wave_circuits' => $tempdatacircuit ?? null, + ]; + } + } + + return $formdata; + } +} diff --git a/dist/classes/station.php.dist b/dist/classes/station.php.dist new file mode 100644 index 0000000..ee8ff88 --- /dev/null +++ b/dist/classes/station.php.dist @@ -0,0 +1,91 @@ +. + +/** + * Class for station persistence. + * + * @package local_moodleorm + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_moodleorm; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing a station from the DB. + * + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class station extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'moodleorm_station'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'circuitid' => array( + 'type' => PARAM_INT, + ), + 'groupid' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'introroom' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'addinfo' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'timecreated' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + 'timemodified' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + ); + } +} diff --git a/dist/classes/substation.php.dist b/dist/classes/substation.php.dist new file mode 100644 index 0000000..7e81bf5 --- /dev/null +++ b/dist/classes/substation.php.dist @@ -0,0 +1,78 @@ +. + +/** + * Class for substation persistence. + * + * @package local_moodleorm + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_moodleorm; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing a substation from the DB. + * + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class substation extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'moodleorm_substation'; + + /** Observer role */ + const MOODLEORM_ROLE_OBSERVER = 1; + + /** Hot Seat role */ + const MOODLEORM_ROLE_HOTSEAT = 2; + + /** Cold Seat role */ + const MOODLEORM_ROLE_COLDSEAT = 3; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'stationid' => array( + 'type' => PARAM_INT, + ), + 'userid' => array( + 'type' => PARAM_INT, + ), + 'role' => array( + 'type' => PARAM_INT, + ), + ); + } +} diff --git a/dist/classes/wave.php.dist b/dist/classes/wave.php.dist new file mode 100644 index 0000000..90cdd3c --- /dev/null +++ b/dist/classes/wave.php.dist @@ -0,0 +1,76 @@ +. + +/** + * Class for wave persistence. + * + * @package local_moodleorm + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace local_moodleorm; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing a wave from the DB. + * + * @copyright 2022 Andrew Caya + * @author Andrew Caya + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class wave extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'moodleorm_wave'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'moodleormid' => array( + 'type' => PARAM_INT, + ), + 'timecreated' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + 'timemodified' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + ); + } +} diff --git a/dist/editform.php.dist b/dist/editform.php.dist new file mode 100644 index 0000000..715cd61 --- /dev/null +++ b/dist/editform.php.dist @@ -0,0 +1,232 @@ +. + +/** + * Display information about all the local_moodleorm modules in the requested course. + * + * @package local_moodleorm + * @author Andrew Caya + * @copyright 2022 Andrew Caya + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); + +use \local_moodleorm\unitofwork; +use \local_moodleorm\form\editform; + +global $CFG, $DB, $PAGE, $OUTPUT; + +require_once($CFG->dirroot . '/local/moodleorm/lib.php'); + +// Course module id. +$id = optional_param('id', 0, PARAM_INT); + +// Getting course module instance. +$cm = get_coursemodule_from_id('moodleorm', $id); + +// Make sure the course exists. +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + +require_course_login($course, true, $cm); + +$moduleinstance = $DB->get_record('moodleorm', array('id' => $cm->instance), '*', MUST_EXIST); +$context = context_module::instance($cm->id); + +$moodleormid = $cm->instance; + +$actionurl = new moodle_url('/local/moodleorm/editform.php', ['id' => $cm->id]); +$redirecturl = new moodle_url('/local/moodleorm/view.php', ['id' => $cm->id]);; + +$pageheading = get_string('moodleormedit', 'local_moodleorm'); +$pagetitle = get_string('pluginname', 'local_moodleorm') . ' - ' . $pageheading; + +$PAGE->set_url(new moodle_url($actionurl)); +$PAGE->set_context($context); +$PAGE->set_title($pagetitle); +$PAGE->set_heading(format_string($course->fullname)); +$PAGE->requires->css('/local/moodleorm/css/flatpickr.min.css'); +$PAGE->requires->css('/local/moodleorm/css/styles.css'); +$PAGE->requires->js('/local/moodleorm/js/editform.js'); + +// Define the ORM classmap. +$classmap = [ + 'moodleorm_wave' => [ + 'class_fqn' => '\local_moodleorm\wave', + 'parent_class_fqn' => '', + ], + 'moodleorm_circuit' => [ + 'class_fqn' => '\local_moodleorm\circuit', + 'parent_class_fqn' => '\local_moodleorm\wave', + ], + 'moodleorm_station' => [ + 'class_fqn' => '\local_moodleorm\station', + 'parent_class_fqn' => '\local_moodleorm\circuit', + ], + 'moodleorm_substation' => [ + 'class_fqn' => '\local_moodleorm\substation', + 'parent_class_fqn' => '\local_moodleorm\station', + ], +]; + +// Instantiate the Unit of Work. +$unitofwork = new unitofwork($DB, $classmap, 'moodleorm', $moodleormid); + +// Get the data. +$currentormregistry = $unitofwork->get_registry(); + +// Build the form. +$editform = new editform( + $actionurl, + [ + 'currentclassmap' => $unitofwork->get_classmap(), + 'currentcourse' => $course, + 'currentormregistry' => $currentormregistry, + ] +); + +$postrawdataarraykeys = array_keys($_POST); + +if (!empty($postrawdataarraykeys)) { + $emptypostflag = true; + + array_walk($postrawdataarraykeys, function($value, $key) use (&$emptypostflag) { + if (strpos($value, 'moodleorm_wave') !== false) { + $emptypostflag = false; + } + }); +} else { + $emptypostflag = false; +} + +// Form processing and displaying is done here. +if ($editform->is_cancelled()) { + redirect($redirecturl . '?id=' . $moodleormid); +} elseif ($editform->is_submitted() && $emptypostflag && is_null($editform->get_data())) { + $validformdata = new stdClass(); + + try { + $unitofwork->save( + $editform->export_valid_data_to_orm($validformdata) + ); + } catch (\Exception $e) { + \core\notification::error(get_string('moodleormnotsaved', 'local_moodleorm')); + \core\notification::error($e->getMessage()); + + redirect($actionurl); + } + + \core\notification::success(get_string('moodleormsaved', 'local_moodleorm')); + + redirect($actionurl); +} elseif ($validformdata = $editform->get_data()) { + try { + $unitofwork->save( + $editform->export_valid_data_to_orm($validformdata) + ); + } catch (\Exception $e) { + \core\notification::error(get_string('moodleormnotsaved', 'local_moodleorm')); + \core\notification::error($e->getMessage()); + + redirect($actionurl); + } + + \core\notification::success(get_string('moodleormsaved', 'local_moodleorm')); + + redirect($actionurl); +} else { + if (!empty($_POST) && !$emptypostflag && $editform->get_data() === null) { + \core\notification::error(get_string('moodleormnotsaved', 'local_moodleorm')); + } + + $currentlang = current_language(); + + $utfstring = strpos($currentlang, '_'); + + if ($utfstring !== false) { + $currentlangrootname = substr($currentlang, 0, strpos($currentlang, '_')); + } else { + $currentlangrootname = $currentlang; + } + + $data['currentlangrootname'] = $currentlangrootname; + + if (file_exists(__DIR__ . '/js/flatpickr.l10n/' . $currentlangrootname . '.js')) { + $data['jslangfile'] = '/local/moodleorm/js/flatpickr.l10n/' . $currentlangrootname . '.js'; + } else { + $data['jslangfile'] = '/local/moodleorm/js/flatpickr.l10n/default.js'; + $data['currentlangrootname'] = 'default'; + } + + // Build the page with Mustache. + echo $OUTPUT->header(); + + echo $OUTPUT->heading($pageheading); + + echo '
'; + + $data['moodleormid'] = $moodleormid; + $data['actionurl'] = $actionurl; + $data['redirecturl'] = $redirecturl; + + $data['elements'] = $editform->render_form_elements(); + + $data['form_open'] = $editform->render(); + + $data['groups'] = $editform->get_groups(); // IMPORTANT: Must be after render(), because it depends on rendering. + + if (empty($data['groups'])) { + \core\notification::error(get_string('nogroups', 'core_group')); + + echo $OUTPUT->render_from_template('moodleorm/error', ['managementtoolurl' => $redirecturl]); + echo $OUTPUT->footer(); + + exit; + } + + $data['roles'] = $editform->get_roles(); // IMPORTANT: Must be after render(), because it depends on rendering. + + $data['translations'] = $editform->get_translations(); + + $data['icontrashwave'] = $OUTPUT->image_icon('i/trash', $data['translations']['buttondeletewave']); + $data['icontrashcircuit'] = $OUTPUT->image_icon('i/trash', $data['translations']['buttondeletecircuit']); + $data['icontrashstation'] = $OUTPUT->image_icon('i/trash', $data['translations']['buttondeletestation']); + $data['icontrashparticipant'] = $OUTPUT->image_icon('i/trash', $data['translations']['buttondeleteparticipant']); + $data['iconaddparticipant'] = $OUTPUT->image_icon('t/add', $data['translations']['buttonaddparticipant']); + + $data['form_open'] = str_replace('', '', $data['form_open']); + + $templatename = 'local_moodleorm/editformclose'; + + $data['form_close'] = $OUTPUT->render_from_template($templatename, []); + $data['form_close'] = $data['form_close'] . ''; + + ob_start(); + require_once(__DIR__ . '/templates/editform/editform.phtml'); + $data['body'] = ob_get_clean(); + + $templatename = 'local_moodleorm/editform'; + + echo $OUTPUT->render_from_template($templatename, $data); + + ob_start(); + echo $OUTPUT->footer(); + $data['footer'] = ob_get_clean(); + $data['footer'] = str_replace('', '', $data['footer']); + $data['footer'] = str_replace('', '', $data['footer']); + + echo $data['footer'] . "\n\n"; +} diff --git a/docs/Makefile b/docs/Makefile new file mode 100755 index 0000000..45a5129 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SimpleEmailForm.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SimpleEmailForm.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/SimpleEmailForm" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SimpleEmailForm" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_build/_static/.gitignore b/docs/_build/_static/.gitignore new file mode 100755 index 0000000..a3a0c8b --- /dev/null +++ b/docs/_build/_static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/docs/_build/doctrees/.gitignore b/docs/_build/doctrees/.gitignore new file mode 100755 index 0000000..a3a0c8b --- /dev/null +++ b/docs/_build/doctrees/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/docs/_build/html/.gitignore b/docs/_build/html/.gitignore new file mode 100755 index 0000000..a3a0c8b --- /dev/null +++ b/docs/_build/html/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100755 index 0000000..1910fa9 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# moodle-local_moodleorm documentation build configuration file, created by +# sphinx-quickstart on Sun Jul 17 00:20:22 2019. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'MoodleORM' +copyright = '2021, Andrew Caya' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_build/_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'MoodleORMdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'MoodleORM.tex', 'MoodleORM Documentation', + 'Andrew Caya', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'moodleorm', 'MoodleORM Documentation', + ['Andrew Caya'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'MoodleORM', 'MoodleORM Documentation', + 'Andrew Caya', 'MoodleORM', 'A simple ORM to process data from Moodle forms', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/configuration.rst b/docs/configuration.rst new file mode 100755 index 0000000..e76ce07 --- /dev/null +++ b/docs/configuration.rst @@ -0,0 +1,74 @@ +.. _ConfigurationAnchor: + +.. index:: Configuration + +.. _configuration: + +Configuration +============= + +The plugin's configuration is set by passing the following parameters to the ``unitofwork``'s constructor +upon object instantiation. The constructor's function signature is as follows: + +.. code-block:: php + + public function __construct(\moodle_database $DB, array $classmap, string $maintablename, int $maintablekey, string $childparentidcolumnname = null, bool $enableconstraints = false) {} + +The first parameter is an instance of the Moodle database abstraction layer (global $DB). + +The second parameter is an array containing a classmap of the tables that the Unit of Work should manage +for you. The array should have the following layout: the array must contain an array for each database table +that should be managed. Each one of these arrays should have an index with the name of the table, that is +associated to an array containing these two elements: the fully-qualified class name (FQCN) of the Moodle +entity that is to be mapped to the table, and an optional element containing the FQCN of its parent entity. + +Here is an example of a classmap for four tables with three parent/child relationships, in concatenation, +where one child becomes the parent of the next child: + +.. code-block:: php + + $this->classmap = [ + 'simulation_wave' => [ + 'class_fqn' => '\local_moodleorm\tests\wave', + 'parent_class_fqn' => '', + ], + 'simulation_circuit' => [ + 'class_fqn' => '\local_moodleorm\tests\circuit', + 'parent_class_fqn' => '\local_moodleorm\tests\wave', + ], + 'simulation_station' => [ + 'class_fqn' => '\local_moodleorm\tests\station', + 'parent_class_fqn' => '\local_moodleorm\tests\circuit', + ], + 'simulation_substation' => [ + 'class_fqn' => '\local_moodleorm\tests\substation', + 'parent_class_fqn' => '\local_moodleorm\tests\station', + ], + ]; + +By default, an entity that does not have a parent is considered to be the fully-managed (CRUD) child of the +main table, which, in turn, should always be read-only. But, if need be, one could easily create an entity +for the main table and designate it as its own child, thus allowing for single table management (complete CRUD) +of the main table. This being said, this would be a limit use case, since accessing the main table through the +standard Moodle DBAL might be a better choice in this case. Again, the full potential of the ORM becomes +clear when managing complex relationships between elements of a Moodle form, for example. + +.. note:: The ORM manages 1 -> 1 subordination in its version 1.0.0, and 1 -> N parent to child relationships will be added eventually. + +The third parameter is the main table name. This is the read-only reference table that is usually the anchor for +a data structure. For example, usually, a course module, or a particular activity, is the anchor point of any +form element or data structure in Moodle. + +The fourth parameter is the primary key (``id`` field) that should be fetched from the main table. + +The fifth parameter is optional, and makes it possible to determine the nomenclature of the index that is to be +considered as a foreign key to the parent's id for the first level child (an entity with no direct parent FQCN). +This is particularly useful when Moodle core tables do not use any particular pattern in their index nomenclature. +For second level children and beyond, the nomenclature must be the parent table's name element that follows the +last underscore, followed by the expression 'id', without any spaces, underscores, or hyphens. For example, +if the parent table's name is 'simulation_circuit', then the name of child's foreign key to the parent id would be +'circuitid'. This is also the default behaviour for first level children if nothing is specified for this parameter. + +The sixth and final paramater is also optional and allows the user to enable constraints on all of the children's foreign keys. It is recommended +to avoid using constraints with Moodle tables, since it is not considered to be a standard way of handling tables +in the Moodle community for now. By default, this parameter is set to ``false``. diff --git a/docs/index.rst b/docs/index.rst new file mode 100755 index 0000000..ee67ae0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,23 @@ +.. _IndexAnchor: + +MoodleORM's documentation +================================== + +**A simple ORM, with a Unit of Work, to easily save cascading data from Moodle Forms, using Moodle Entities, via single database transactions.** + +`MoodleORM Home Page `_ + +.. toctree:: + :maxdepth: 3 + + installation + configuration + methods + whats_new + license + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` \ No newline at end of file diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100755 index 0000000..abb0a7c --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,16 @@ +.. _InstallationAnchor: + +Installation +============ + +.. index:: Installation + +Prerequisites +------------- + +* Minimum: PHP 7.3, recommended: PHP 8.0, or greater. + +Moodle Plugin Installation +-------------------------- + +To add the **MoodleORM local plugin** package to your Moodle project, please follow the standard `Moodle instructions `_ to install a local plugin. diff --git a/docs/license.rst b/docs/license.rst new file mode 100755 index 0000000..cd0e214 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,76 @@ +.. _LicenseAnchor: + +.. index:: License + +MoodleORM License +================= + + Copyright 2021, Andrew Caya. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + ``_ + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +Apache License +-------------- + +Version 2.0, January 2004 + +``_ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + You must give any other recipients of the Work or Derivative Works a copy of this License; and + You must cause any modified files to carry prominent notices stating that You changed the files; and + You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100755 index 0000000..35f7b78 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SimpleEmailForm.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SimpleEmailForm.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/methods.rst b/docs/methods.rst new file mode 100755 index 0000000..13ead18 --- /dev/null +++ b/docs/methods.rst @@ -0,0 +1,121 @@ +.. _MethodsAnchor: + +.. index:: ORM Methods + +.. _methods: + +ORM Methods +=========== + +MoodleORM has twelve (12) public methods that will help you to work with complex data. + +The main public methods are:: + + * export_data() + * save() and, + * commit() + +.. index:: export_data Method + +.. _export_data Method: + +export_data() Method +-------------------- + +The ``export_data()`` method allows you to extract an array of the data that was read from the database, +into a format that is perfectly compatible with the ORM's ``save()`` method. + +.. note:: The ``export_data()`` will only return data if the ORM's ``save()`` method was not previously invoked, and the ORM's registry is clean. + +The format of the data is fairly straightforward. Each data stucture has a ``repositoryname`` index, which must +match the name of the table in the classmap array. The ``data`` index contains the fields as defined in the +table schema and in the corresponding properties of the Moodle entity. Finally, the array must either contain an +``entityid`` index, that holds the id of the existing entity, or an ``entityuuid`` index if the entity is to be +inserted into the database. In order to avoid collisions, it is recommended to use the +``bin2hex(random_bytes(20))`` PHP functions to generate the UUID. If a new entity has a newly created parent +entity, it will also be necessary to create a ``parentuuid`` index and insert the corresponding UUID at this +index of the array. Here is an example of a new entity that must be created: + +.. code-block:: php + + $simulationid = $course->cmid; + + $uuid = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Wave 2', + 'description' => 'Second wave.', + 'simulationid' => $simulationid, + ], + ]; + +An example of the array's structure when extracting the data from the database would be something like +the following example: + +.. code-block:: php + + $data[] = [ + 'entityid' => 4, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Wave 2', + 'description' => 'Second wave.', + 'simulationid' => 1, + ], + ]; + +Updating is a question of modifying elements of an entity in the array. Deleting is simply a question of removing +the appropriate element from the array. + +.. index:: save Method + +.. _save Method: + +save() Method +------------- + +Once the data array is set, it will be necessary to invoke the ``save()`` method, in order for the ORM to start +tracking changes in the entities. Its Unit of Work will start building a registry of "dirty" entities, that will +then be used to define the elements of the transaction that will be sent to the database when invoking its +``commit()`` method. + +.. index:: commit Method + +.. _commit Method: + +commit() Method +--------------- + +The ``commit()`` method will clean the dirty registry of entities by running an SQL transaction, with all of the +required queries, in order to save the new data to the database. + +.. note:: The ``commit()`` method will automatically be invoked by the ORM's destructor method if it falls out of scope of the current PHP script. + +.. index:: Other ORM Methods + +.. _Other ORM Methods: + +Other ORM Methods +----------------- + +The ORM comes with the following helper methods: + + * ``get_maintable_settings()``, which gives access to the main read-only table's data, based on the given ``id``, + * ``get_classmap()``, which makes it possible to get the ORM's currently used classmap, + * ``get_registry()``, which will return an array containing all of the data, including Moodle entities, that were initially read from the database, + * ``get_dirty()``, which will return an array of all of the keys of the new data that require a CRUD action in order to save them to the database, + * ``get_stage()``, which will return an array of all of the data, including Moodle entities, that were included in the commit (database transaction), + * ``is_committed()``, which will return true if a database transaction was attempted, and false if no persistence action was taken, and + * ``is_dirty()``, which will return true if the ``save()`` method was invoked with new data. + +There are also two helper methods for adding or removing foreign key constraints on the foreign keys. These methods +are: + + * ``try_add_cascade_delete_check()``, and + * ``try_remove_cascade_delete_check()``. + +.. NOTE:: For a working example on how to start building a Moodle Form with MoodleORM on the back end (without the templates, the CSS, or the JS), please see the 'dist' folder included with each release of MoodleORM. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..10c4fa1 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx == 5.3.0 +urllib3 == 1.26.15 \ No newline at end of file diff --git a/docs/whats_new.rst b/docs/whats_new.rst new file mode 100755 index 0000000..4dea70f --- /dev/null +++ b/docs/whats_new.rst @@ -0,0 +1,6 @@ +.. _WhatsNewAnchor: + +What's new in version 1.0.0 (2023-02-27) +======================================== + +* Initial version of the MoodleORM local plugin. diff --git a/lang/en/local_moodleorm.php b/lang/en/local_moodleorm.php new file mode 100644 index 0000000..2b624b0 --- /dev/null +++ b/lang/en/local_moodleorm.php @@ -0,0 +1,26 @@ +. + +/** + * Localized strings of the local_moodleorm plugin. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +$string['pluginname'] = 'MoodleORM'; \ No newline at end of file diff --git a/tests/circuit.php b/tests/circuit.php new file mode 100644 index 0000000..9319383 --- /dev/null +++ b/tests/circuit.php @@ -0,0 +1,85 @@ +. + +/** + * Class for circuit persistence. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm\tests; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing a circuit from the DB. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ +class circuit extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'simulation_circuit'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'waveid' => array( + 'type' => PARAM_INT, + ), + 'timestart' => array( + 'type' => PARAM_INT, + ), + 'timeend' => array( + 'type' => PARAM_INT, + ), + 'timecreated' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + 'timemodified' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + ); + } +} diff --git a/tests/cmodule.php b/tests/cmodule.php new file mode 100644 index 0000000..718ea55 --- /dev/null +++ b/tests/cmodule.php @@ -0,0 +1,129 @@ +. + +/** + * Class for entity persistence. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm\tests; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing an entity from the database. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ +class cmodule extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'course_modules'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + ], + 'course' => [ + 'type' => PARAM_INT, + ], + 'module' => [ + 'type' => PARAM_INT, + ], + 'instance' => [ + 'type' => PARAM_INT, + ], + 'section' => [ + 'type' => PARAM_INT, + ], + 'idnumber' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + ], + 'added' => [ + 'type' => PARAM_INT, + 'default' => time(), + ], + 'indent' => [ + 'type' => PARAM_INT, + ], + 'visible' => [ + 'type' => PARAM_INT, + ], + 'visiblecoursepage' => [ + 'type' => PARAM_INT, + ], + 'visibleold' => [ + 'type' => PARAM_INT, + ], + 'groupmode' => [ + 'type' => PARAM_INT, + ], + 'groupingid' => [ + 'type' => PARAM_INT, + ], + 'completion' => [ + 'type' => PARAM_INT, + ], + 'completiongradeitemnumber' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + ], + 'completionview' => [ + 'type' => PARAM_INT, + ], + 'completionexpected' => [ + 'type' => PARAM_INT, + ], + 'completionpassgrade' => [ + 'type' => PARAM_INT, + ], + 'showdescription' => [ + 'type' => PARAM_INT, + ], + 'availability' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + ], + 'deletioninprogress' => [ + 'type' => PARAM_INT, + ], + 'downloadcontent' => [ + 'type' => PARAM_INT, + ], + 'lang' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + ], + ]; + } +} diff --git a/tests/db/install.xml b/tests/db/install.xml new file mode 100644 index 0000000..6299e53 --- /dev/null +++ b/tests/db/install.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
diff --git a/tests/init.php b/tests/init.php new file mode 100644 index 0000000..e30a202 --- /dev/null +++ b/tests/init.php @@ -0,0 +1,87 @@ +. + +/** + * Unitofwork tests. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'wave.php'); + +require_once($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'circuit.php'); + +require_once($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'station.php'); + +require_once($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'substation.php'); + +require_once($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'cmodule.php'); + +require_once($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'module.php'); diff --git a/tests/module.php b/tests/module.php new file mode 100644 index 0000000..b8ad69a --- /dev/null +++ b/tests/module.php @@ -0,0 +1,73 @@ +. + +/** + * Class for entity persistence. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm\tests; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing an entity from the database. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ +class module extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'modules'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return [ + 'id' => [ + 'type' => PARAM_INT, + ], + 'name' => [ + 'type' => PARAM_TEXT, + ], + 'cron' => [ + 'type' => PARAM_INT, + ], + 'lastcron' => [ + 'type' => PARAM_INT, + ], + 'search' => [ + 'type' => PARAM_TEXT, + ], + 'visible' => [ + 'type' => PARAM_INT, + ], + ]; + } +} diff --git a/tests/station.php b/tests/station.php new file mode 100644 index 0000000..b8cda87 --- /dev/null +++ b/tests/station.php @@ -0,0 +1,94 @@ +. + +/** + * Class for station persistence. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm\tests; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing a station from the DB. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ +class station extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'simulation_station'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'circuitid' => array( + 'type' => PARAM_INT, + ), + 'groupid' => array( + 'type' => PARAM_INT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'introroom' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'addinfo' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'timecreated' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + 'timemodified' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + ); + } +} diff --git a/tests/substation.php b/tests/substation.php new file mode 100644 index 0000000..7eefc8a --- /dev/null +++ b/tests/substation.php @@ -0,0 +1,81 @@ +. + +/** + * Class for substation persistence. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm\tests; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing a substation from the DB. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ +class substation extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'simulation_substation'; + + /** Observer role */ + const SIMULATION_ROLE_OBSERVER = 1; + + /** Hot Seat role */ + const SIMULATION_ROLE_HOTSEAT = 2; + + /** Cold Seat role */ + const SIMULATION_ROLE_COLDSEAT = 3; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'stationid' => array( + 'type' => PARAM_INT, + ), + 'userid' => array( + 'type' => PARAM_INT, + ), + 'role' => array( + 'type' => PARAM_INT, + ), + ); + } +} diff --git a/tests/unitofwork_test.php b/tests/unitofwork_test.php new file mode 100644 index 0000000..4d43f03 --- /dev/null +++ b/tests/unitofwork_test.php @@ -0,0 +1,2809 @@ +. + +/** + * Unitofwork tests. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm; + +defined('MOODLE_INTERNAL') || die(); + +use \local_moodleorm\unitofwork; + +global $CFG; + +require_once($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'init.php'); + +/** + * Class of tests for the Unit of Work of persistence management. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ +class unitofwork_test extends \advanced_testcase { + + /** Constant string Name of the main test table. */ + const MAIN_TABLE_NAME = 'simulation'; + + /** @var classmap Array of arrays containing a classmap of persistent objects. */ + protected $classmap; + + /** @var db Instance of a fake moodle_database object. */ + protected $db; + + /** @var unitofwork Instance of a unitofwork object. */ + protected $unitofwork; + + /** + * Setup test data. + */ + public function setUp(): void { + parent::setUp(); + + global $CFG, $DB; + + $this->classmap = [ + 'simulation_wave' => [ + 'class_fqn' => '\local_moodleorm\tests\wave', + 'parent_class_fqn' => '', + ], + 'simulation_circuit' => [ + 'class_fqn' => '\local_moodleorm\tests\circuit', + 'parent_class_fqn' => '\local_moodleorm\tests\wave', + ], + 'simulation_station' => [ + 'class_fqn' => '\local_moodleorm\tests\station', + 'parent_class_fqn' => '\local_moodleorm\tests\circuit', + ], + 'simulation_substation' => [ + 'class_fqn' => '\local_moodleorm\tests\substation', + 'parent_class_fqn' => '\local_moodleorm\tests\station', + ], + ]; + + $this->db = $DB; + + $dbman = $this->db->get_manager(); + + // Load the XML file. + $xmldbfile = new \xmldb_file($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'db' + . DIRECTORY_SEPARATOR + . 'install.xml'); + + // Only if the file exists. + if ($xmldbfile->fileExists()) { + // Load the XML contents to structure. + $loaded = $xmldbfile->loadXMLStructure(); + + if ($loaded || $xmldbfile->isLoaded()) { + // Arriving here, everything is ok, get the XMLDB structure. + $structure = $xmldbfile->getStructure(); + + // Getting tables. + if ($xmldbtables = $structure->getTables()) { + // Foreach table, process its fields. + foreach ($xmldbtables as $xmldbtable) { + // Table processing starts here. + if ($dbman->table_exists($xmldbtable)) { + continue; + } else { + $dbman->create_table($xmldbtable); + } + + // Give the script some more time (resetting to current if exists). + if ($currenttl = @ini_get('max_execution_time')) { + @ini_set('max_execution_time', $currenttl); + } + } + } + } + } + + $this->resetAfterTest(false); + } + + /** + * Clean up after tests. + */ + public function tearDown(): void { + global $CFG; + + $this->unitofwork->try_remove_cascade_delete_check(); + + $dbman = $this->db->get_manager(); + + // Load the XML file. + $xmldbfile = new \xmldb_file($CFG->dirroot + . DIRECTORY_SEPARATOR + . 'local' + . DIRECTORY_SEPARATOR + . 'moodleorm' + . DIRECTORY_SEPARATOR + . 'tests' + . DIRECTORY_SEPARATOR + . 'db' + . DIRECTORY_SEPARATOR + . 'install.xml'); + + // Only if the file exists. + if ($xmldbfile->fileExists()) { + // Load the XML contents to structure. + $loaded = $xmldbfile->loadXMLStructure(); + + if ($loaded || $xmldbfile->isLoaded()) { + // Arriving here, everything is ok, get the XMLDB structure. + $structure = $xmldbfile->getStructure(); + + // Getting tables. + if ($xmldbtables = $structure->getTables()) { + // For each table, truncate it (delete all records). + foreach ($xmldbtables as $xmldbtable) { + // Table processing starts here. + if ($dbman->table_exists($xmldbtable) && $xmldbtable->getName() === 'simulation') { + $this->db->delete_records($xmldbtable->getName()); + } else { + continue; + } + + // Give the script some more time (resetting to current if exists). + if ($currenttl = @ini_get('max_execution_time')) { + @ini_set('max_execution_time', $currenttl); + } + } + + // For each table, drop it. + foreach ($xmldbtables as $xmldbtable) { + // Table processing starts here. + $this->db->delete_records($xmldbtable->getName()); + $dbman->drop_table($xmldbtable); + + // Give the script some more time (resetting to current if exists). + if ($currenttl = @ini_get('max_execution_time')) { + @ini_set('max_execution_time', $currenttl); + } + } + } + } + } + + $this->unitofwork = null; + + $this->db = null; + + $this->classmap = null; + + parent::tearDown(); + } + + /** + * Test unitofwork_creation method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_creation() { + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, 1); + + $this->assertTrue( + array_reverse(explode('\\', get_class($this->unitofwork)))[0] + === 'unitofwork' + ); + + $this->assertEquals($this->classmap, $this->unitofwork->get_classmap()); + } + + /** + * Test unitofwork_creation_with_childparentid method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_creation_with_childparentid() { + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, 1, 'simulationid'); + + $this->assertTrue( + array_reverse(explode('\\', get_class($this->unitofwork)))[0] + === 'unitofwork' + ); + + $this->assertEquals($this->classmap, $this->unitofwork->get_classmap()); + } + + /** + * Test unitofwork_destruction_autocommit method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_destruction_autocommit() { + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, 1, 'simulationid'); + + $this->assertTrue($this->unitofwork->__destruct()); + + $this->assertTrue($this->unitofwork->is_committed()); + } + + /** + * Test unitofwork_cannot_commit_twice method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_cannot_commit_twice() { + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, 1, 'simulationid'); + + $this->assertTrue($this->unitofwork->commit()); + + $this->assertFalse($this->unitofwork->commit()); + } + + /** + * Test unitofwork_creation_no_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_creation_no_data_in_the_database() { + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, 1, 'simulationid'); + + $this->assertEmpty($this->unitofwork->get_registry()); + + $this->assertEmpty($this->unitofwork->get_dirty()); + + $this->assertEmpty($this->unitofwork->get_stage()); + } + + /** + * Test unitofwork_get_main_settings_from_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_get_main_settings_from_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $mainsettings = $this->unitofwork->get_maintable_settings(); + + $this->assertTrue(isset($mainsettings) && !empty($mainsettings)); + + $key = key($mainsettings); + + $this->assertSame($simulation['name'], $mainsettings[$key]->name); + + $this->assertSame($simulation['scheduledelimiters'], $mainsettings[$key]->scheduledelimiters); + } + + /** + * Test test_unitofwork_get_single_parent_table_from_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_get_single_parent_table_from_the_database() { + $course = $this->getDataGenerator()->create_course(); + $cm = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $module = $this->db->get_record('course_modules', ['id' => $cm->cmid], 'module'); + + $classmap = [ + 'module' => [ + 'class_fqn' => '\local_moodleorm\tests\module', + 'parent_class_fqn' => '', + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $classmap, 'modules', $module->module, 'id'); + + $fetchedarray = $this->unitofwork->get_registry(); + + $this->assertTrue(isset($fetchedarray) && !empty($fetchedarray)); + + $this->assertSame('module', key($fetchedarray)); + + foreach ($fetchedarray['module'] as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertSame($module->module, $entity['persistent_object']->get_data()->id); + $this->assertSame('forum', $entity['persistent_object']->get_data()->name); + $this->assertSame('local_moodleorm\tests\module', get_class($entity['persistent_object'])); + $this->assertTrue(isset($entity['hash'])); + } + } + } + + /** + * Test test_unitofwork_get_single_child_table_from_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_get_single_child_table_from_the_database() { + $course = $this->getDataGenerator()->create_course(); + $cm = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + + $classmap = [ + 'module' => [ + 'class_fqn' => '\local_moodleorm\tests\cmodule', + 'parent_class_fqn' => '', + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $classmap, 'course_modules', $cm->cmid, 'id'); + + $fetchedarray = $this->unitofwork->get_registry(); + + $this->assertTrue(isset($fetchedarray) && !empty($fetchedarray)); + + $this->assertSame('module', key($fetchedarray)); + + foreach ($fetchedarray['module'] as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertSame($cm->cmid, $entity['persistent_object']->get_data()->id); + $this->assertSame($cm->timemodified, $entity['persistent_object']->get_data()->added); + $this->assertSame('local_moodleorm\tests\cmodule', get_class($entity['persistent_object'])); + $this->assertSame($course->id, $entity['persistent_object']->get_data()->course); + $this->assertTrue(isset($entity['hash'])); + } + } + } + + /** + * Test test_unitofwork_get_parent_and_child_tables_from_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_get_parent_and_child_tables_from_the_database() { + $course = $this->getDataGenerator()->create_course(); + $cm = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); + $module = $this->db->get_record('course_modules', ['id' => $cm->cmid], 'module'); + + $classmap = [ + 'module' => [ + 'class_fqn' => '\local_moodleorm\tests\cmodule', + 'parent_class_fqn' => '', + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $classmap, 'modules', $module->module, 'module'); + + $fetchedarray = $this->unitofwork->get_registry(); + + $this->assertTrue(isset($fetchedarray) && !empty($fetchedarray)); + + $this->assertSame('module', key($fetchedarray)); + + foreach ($fetchedarray['module'] as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertSame($cm->cmid, $entity['persistent_object']->get_data()->id); + $this->assertSame($cm->timemodified, $entity['persistent_object']->get_data()->added); + $this->assertSame('local_moodleorm\tests\cmodule', get_class($entity['persistent_object'])); + $this->assertSame($course->id, $entity['persistent_object']->get_data()->course); + $this->assertTrue(isset($entity['hash'])); + } + } + } + + /** + * Test unitofwork_creation_with_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_creation_with_data_in_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du premier test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid); + + $fetchedwavearray = $this->unitofwork->get_registry()['simulation_wave']['wave_' . $waveid]; + + $this->assertTrue(isset($fetchedwavearray) && !empty($fetchedwavearray)); + + $this->assertEquals($waveid, $fetchedwavearray['persistent_object']->get_data()->id); + + $this->assertEmpty($this->unitofwork->get_dirty()); + + $this->assertEmpty($this->unitofwork->get_stage()); + } + + /** + * Test unitofwork_creation_with_data_in_the_database_with_childparentid method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_creation_with_data_in_the_database_with_childparentid() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du premier test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $fetchedwavearray = $this->unitofwork->get_registry()['simulation_wave']['wave_' . $waveid]; + + $this->assertTrue(isset($fetchedwavearray) && !empty($fetchedwavearray)); + + $this->assertEquals($waveid, $fetchedwavearray['persistent_object']->get_data()->id); + + $this->assertEmpty($this->unitofwork->get_dirty()); + + $this->assertEmpty($this->unitofwork->get_stage()); + } + + /** + * Test unitofwork_creation_with_data_read_from_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_creation_with_data_read_from_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du premier test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $fetchedwavearray = $this->unitofwork->get_registry()['simulation_wave']['wave_' . $waveid]; + + $this->assertTrue(isset($fetchedwavearray) && !empty($fetchedwavearray)); + + $this->assertEquals($waveid, $fetchedwavearray['persistent_object']->get_data()->id); + + $this->assertEmpty($this->unitofwork->get_dirty()); + + $this->assertEmpty($this->unitofwork->get_stage()); + } + + /** + * Test unitofwork_creation_with_data_read_other_simulation_from_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_creation_with_data_read_other_simulation_from_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsimother', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -25\nInstructions | -10", + ]; + + $simulationidref = $this->db->insert_record('simulation', $simulation); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du premier test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationidref, 'simulationid'); + + $fetchedwavearray = $this->unitofwork->get_registry(); + + $this->assertTrue(empty($fetchedwavearray)); + + $this->assertEmpty($this->unitofwork->get_dirty()); + + $this->assertEmpty($this->unitofwork->get_stage()); + } + + /** + * Test create_wave_with_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_create_wave_with_data_in_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 2', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $dirtyregistry = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyregistry)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(2, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $records = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($records) && !empty($records)); + + $this->assertEquals(2, count($records)); + + foreach ($records as $record) { + if ($record->name === 'Vague 2') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + } + + /** + * Test create_wave_with_data_in_the_database_with_childparentid method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_create_wave_with_data_in_the_database_with_childparentid() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 2', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $dirtyregistry = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyregistry)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(2, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $records = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($records) && !empty($records)); + + $this->assertEquals(2, count($records)); + + foreach ($records as $record) { + if ($record->name === 'Vague 2') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + } + + /** + * Test create_wave_and_circuit_with_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_create_wave_and_circuit_with_data_in_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 2', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid2 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid2, + 'parentuuid' => $uuid, + 'data' => [ + 'name' => 'Circuit 1', + 'timestart' => '20220311110000', + 'timeend' => '20220311120000', + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_circuit']; + + $this->assertEquals(1, count($circuitsarray)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + foreach ($circuitsarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(3, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $records = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($records) && !empty($records)); + + $this->assertEquals(2, count($records)); + + foreach ($records as $record) { + if ($record->name === 'Vague 2') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + + $records = $this->db->get_records('simulation_circuit'); + + $this->assertTrue(isset($records) && !empty($records)); + + $this->assertEquals(1, count($records)); + + foreach ($records as $record) { + if ($record->name === 'Circuit 1') { + $this->assertEquals('20220311110000', $record->timestart); + } + } + } + + /** + * Test create_wave_and_circuit_with_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_update_wave_and_circuit_with_data_in_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $circuit = [ + 'name' => 'Circuit 1', + 'description' => 'Premier circuit de la première vague du test.', + 'waveid' => $waveid, + ]; + + $circuitid = $this->db->insert_record('simulation_circuit', $circuit); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $data[] = [ + 'entityid' => $circuitid, + 'repositoryname' => 'simulation_circuit', + 'data' => [ + 'name' => 'Circuit 1a', + 'description' => 'Premier circuit modifié de la première vague du test.', + 'waveid' => $waveid, + ], + ]; + + $uuid = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 2', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid2 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid2, + 'parentuuid' => $uuid, + 'data' => [ + 'name' => 'Circuit 1b', + 'timestart' => '20220311110000', + 'timeend' => '20220311120000', + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_circuit']; + + $this->assertEquals(2, count($circuitsarray)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + foreach ($circuitsarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(4, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $records = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($records) && !empty($records)); + + $this->assertEquals(2, count($records)); + + foreach ($records as $record) { + if ($record->name === 'Vague 2') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + + $records = $this->db->get_records('simulation_circuit'); + + $this->assertTrue(isset($records) && !empty($records)); + + $this->assertEquals(2, count($records)); + + foreach ($records as $record) { + if ($record->name === 'Circuit 1a') { + $this->assertEquals( + 'Premier circuit modifié de la première vague du test.', + $record->description + ); + } + + if ($record->name === 'Circuit 1b') { + $this->assertEquals('20220311110000', $record->timestart); + } + } + } + + /** + * Test unitofwork_export_data_cannot_export_when_dirty method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_export_data_cannot_export_when_dirty() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du premier test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 2', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $this->assertTrue($this->unitofwork->is_dirty()); + + $this->assertFalse($this->unitofwork->export_data()); + } + + /** + * Test unitofwork_export_data_cannot_export_when_committed method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_export_data_cannot_export_when_committed() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du premier test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 2', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + // Deliberately omitting to save() the data. + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $this->assertFalse($this->unitofwork->export_data()); + } + + /** + * Test unitofwork_export_data_from_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_unitofwork_export_data_from_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du premier test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 2', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid2 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid2, + 'parentuuid' => $uuid, + 'data' => [ + 'name' => 'Circuit 1', + 'timestart' => '20220311110000', + 'timeend' => '20220311120000', + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $this->unitofwork->commit(); + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $exporteddata = $this->unitofwork->export_data(); + + foreach ($exporteddata as $element) { + if (isset($element['repositoryname']) + && $element['repositoryname'] === 'simulation_wave' + ) { + $this->assertTrue( + isset($element['entityid']) + && isset($element['data']) + && isset($element['data']['id']) + && isset($element['data']['name']) + && isset($element['data']['description']) + && isset($element['data']['simulationid']) + && isset($element['data']['timemodified']) + && $element['entityid'] === $element['data']['id'] + ); + } else if (isset($element['repositoryname']) + && $element['repositoryname'] === 'simulation_circuit' + ) { + $this->assertTrue( + isset($element['entityid']) + && isset($element['data']) + && isset($element['data']['id']) + && isset($element['data']['name']) + && isset($element['data']['waveid']) + && isset($element['data']['timestart']) + && isset($element['data']['timeend']) + && isset($element['data']['timemodified']) + && $element['entityid'] === $element['data']['id'] + ); + } + } + } + + /** + * Test crud_one_without_children_all_entities_with_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_crud_one_without_children_all_entities_with_data_in_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid1 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid1, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 1b', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid2 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid2, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 1', + 'timestart' => '20220311110000', + 'timeend' => '20220311120000', + ], + ]; + + $uuid3 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid3, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 2', + 'timestart' => '20220312110000', + 'timeend' => '20220312120000', + ], + ]; + + $uuid4 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid4, + 'parentuuid' => $uuid2, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 1.', + 'groupid' => 1, + ], + ]; + + $uuid5 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid5, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid6 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid6, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 2', + 'description' => 'La station 2 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid7 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid7, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1A', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $uuid8 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid8, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1B', + 'userid' => 1, + 'role' => 2, + ], + ]; + + $uuid9 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid9, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1C', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_circuit']; + + $this->assertEquals(2, count($circuitsarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_station']; + + $this->assertEquals(3, count($circuitsarray)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + foreach ($circuitsarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + + $this->assertEquals(2, count($waverecords)); + + foreach ($waverecords as $record) { + if ($record->name === 'Vague 1a') { + $this->assertEquals('Première vague du test.', $record->description); + } else if ($record->name === 'Vague 1b') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + + $circuitrecords = $this->db->get_records('simulation_circuit'); + + $this->assertTrue(isset($circuitrecords) && !empty($circuitrecords)); + + $this->assertEquals(2, count($circuitrecords)); + + foreach ($circuitrecords as $record) { + if ($record->name === 'Circuit 1') { + $this->assertEquals('20220311110000', $record->timestart); + } + + if ($record->name === 'Circuit 2') { + $this->assertEquals('20220312110000', $record->timestart); + } + } + + $circuitrecords = array_values($circuitrecords); + + $stationrecords = $this->db->get_records('simulation_station'); + + $this->assertTrue(isset($stationrecords) && !empty($stationrecords)); + + $this->assertEquals(3, count($stationrecords)); + + foreach ($stationrecords as $record) { + if ($record->circuitid === $circuitrecords[0]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 1.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 2.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 2') { + $this->assertEquals('La station 2 du circuit 2.', $record->description); + } + } + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $data = $this->unitofwork->export_data(); + + array_shift($data); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + $circuitrecords = $this->db->get_records('simulation_circuit'); + $stationrecords = $this->db->get_records('simulation_station'); + $substationrecords = $this->db->get_records('simulation_substation'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + $this->assertEquals(1, count($waverecords)); + + $this->assertTrue(isset($circuitrecords) && !empty($circuitrecords)); + $this->assertEquals(2, count($circuitrecords)); + + $this->assertTrue(isset($stationrecords) && !empty($stationrecords)); + $this->assertEquals(3, count($stationrecords)); + + $this->assertTrue(isset($substationrecords) && !empty($substationrecords)); + $this->assertEquals(3, count($substationrecords)); + } + + /** + * Test crud_one_with_children_all_entities_with_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_crud_one_with_children_all_entities_with_data_in_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid1 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid1, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 1b', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid2 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid2, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 1', + 'timestart' => '20220311110000', + 'timeend' => '20220311120000', + ], + ]; + + $uuid3 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid3, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 2', + 'timestart' => '20220312110000', + 'timeend' => '20220312120000', + ], + ]; + + $uuid4 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid4, + 'parentuuid' => $uuid2, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 1.', + 'groupid' => 1, + ], + ]; + + $uuid5 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid5, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid6 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid6, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 2', + 'description' => 'La station 2 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid7 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid7, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1A', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $uuid8 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid8, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1B', + 'userid' => 1, + 'role' => 2, + ], + ]; + + $uuid9 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid9, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1C', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_circuit']; + + $this->assertEquals(2, count($circuitsarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_station']; + + $this->assertEquals(3, count($circuitsarray)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + foreach ($circuitsarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + + $this->assertEquals(2, count($waverecords)); + + foreach ($waverecords as $record) { + if ($record->name === 'Vague 1a') { + $this->assertEquals('Première vague du test.', $record->description); + } else if ($record->name === 'Vague 1b') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + + $circuitrecords = $this->db->get_records('simulation_circuit'); + + $this->assertTrue(isset($circuitrecords) && !empty($circuitrecords)); + + $this->assertEquals(2, count($circuitrecords)); + + foreach ($circuitrecords as $record) { + if ($record->name === 'Circuit 1') { + $this->assertEquals('20220311110000', $record->timestart); + } + + if ($record->name === 'Circuit 2') { + $this->assertEquals('20220312110000', $record->timestart); + } + } + + $circuitrecords = array_values($circuitrecords); + + $stationrecords = $this->db->get_records('simulation_station'); + + $this->assertTrue(isset($stationrecords) && !empty($stationrecords)); + + $this->assertEquals(3, count($stationrecords)); + + foreach ($stationrecords as $record) { + if ($record->circuitid === $circuitrecords[0]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 1.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 2.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 2') { + $this->assertEquals('La station 2 du circuit 2.', $record->description); + } + } + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $data = []; + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + $circuitrecords = $this->db->get_records('simulation_circuit'); + $stationrecords = $this->db->get_records('simulation_station'); + $substationrecords = $this->db->get_records('simulation_substation'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + $this->assertEquals(1, count($waverecords)); + + $this->assertFalse(isset($circuitrecords) && !empty($circuitrecords)); + $this->assertEquals(0, count($circuitrecords)); + + $this->assertFalse(isset($stationrecords) && !empty($stationrecords)); + $this->assertEquals(0, count($stationrecords)); + + $this->assertFalse(isset($substationrecords) && !empty($substationrecords)); + $this->assertEquals(0, count($substationrecords)); + } + + /** + * Test crud_one_with_children_all_entities_with_data_in_the_database_with_classmap_order_has_no_effect method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_crud_one_with_children_all_entities_with_data_in_the_database_with_classmap_order_has_no_effect() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid1 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid1, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 1b', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid2 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid2, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 1', + 'timestart' => '20220311110000', + 'timeend' => '20220311120000', + ], + ]; + + $uuid3 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid3, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 2', + 'timestart' => '20220312110000', + 'timeend' => '20220312120000', + ], + ]; + + $uuid4 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid4, + 'parentuuid' => $uuid2, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 1.', + 'groupid' => 1, + ], + ]; + + $uuid5 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid5, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid6 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid6, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 2', + 'description' => 'La station 2 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid7 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid7, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1A', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $uuid8 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid8, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1B', + 'userid' => 1, + 'role' => 2, + ], + ]; + + $uuid9 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid9, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1C', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $tempclassmap = $this->classmap; + + $tempclassmap['simulation_substation'] = $this->classmap['simulation_substation']; + $tempclassmap['simulation_circuit'] = $this->classmap['simulation_circuit']; + $tempclassmap['simulation_station'] = $this->classmap['simulation_station']; + $tempclassmap['simulation_wave'] = $this->classmap['simulation_wave']; + + $this->unitofwork = new unitofwork($this->db, $tempclassmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_circuit']; + + $this->assertEquals(2, count($circuitsarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_station']; + + $this->assertEquals(3, count($circuitsarray)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + foreach ($circuitsarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + + $this->assertEquals(2, count($waverecords)); + + foreach ($waverecords as $record) { + if ($record->name === 'Vague 1a') { + $this->assertEquals('Première vague du test.', $record->description); + } else if ($record->name === 'Vague 1b') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + + $circuitrecords = $this->db->get_records('simulation_circuit'); + + $this->assertTrue(isset($circuitrecords) && !empty($circuitrecords)); + + $this->assertEquals(2, count($circuitrecords)); + + foreach ($circuitrecords as $record) { + if ($record->name === 'Circuit 1') { + $this->assertEquals('20220311110000', $record->timestart); + } + + if ($record->name === 'Circuit 2') { + $this->assertEquals('20220312110000', $record->timestart); + } + } + + $circuitrecords = array_values($circuitrecords); + + $stationrecords = $this->db->get_records('simulation_station'); + + $this->assertTrue(isset($stationrecords) && !empty($stationrecords)); + + $this->assertEquals(3, count($stationrecords)); + + foreach ($stationrecords as $record) { + if ($record->circuitid === $circuitrecords[0]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 1.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 2.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 2') { + $this->assertEquals('La station 2 du circuit 2.', $record->description); + } + } + + $tempclassmap = $this->classmap; + + $tempclassmap['simulation_circuit'] = $this->classmap['simulation_circuit']; + $tempclassmap['simulation_substation'] = $this->classmap['simulation_substation']; + $tempclassmap['simulation_wave'] = $this->classmap['simulation_wave']; + $tempclassmap['simulation_station'] = $this->classmap['simulation_station']; + + $this->unitofwork = new unitofwork($this->db, $tempclassmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $data = []; + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + $circuitrecords = $this->db->get_records('simulation_circuit'); + $stationrecords = $this->db->get_records('simulation_station'); + $substationrecords = $this->db->get_records('simulation_substation'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + $this->assertEquals(1, count($waverecords)); + + $this->assertFalse(isset($circuitrecords) && !empty($circuitrecords)); + $this->assertEquals(0, count($circuitrecords)); + + $this->assertFalse(isset($stationrecords) && !empty($stationrecords)); + $this->assertEquals(0, count($stationrecords)); + + $this->assertFalse(isset($substationrecords) && !empty($substationrecords)); + $this->assertEquals(0, count($substationrecords)); + } + + /** + * Test crud_one_with_children_and_delete_deleted_child_all_entities_with_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_crud_one_with_children_and_delete_deleted_child_all_entities_with_data_in_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid1 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid1, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 1b', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid2 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid2, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 1', + 'timestart' => '20220311110000', + 'timeend' => '20220311120000', + ], + ]; + + $uuid3 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid3, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 2', + 'timestart' => '20220312110000', + 'timeend' => '20220312120000', + ], + ]; + + $uuid4 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid4, + 'parentuuid' => $uuid2, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 1.', + 'groupid' => 1, + ], + ]; + + $uuid5 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid5, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid6 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid6, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 2', + 'description' => 'La station 2 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid7 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid7, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1A', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $uuid8 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid8, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1B', + 'userid' => 1, + 'role' => 2, + ], + ]; + + $uuid9 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid9, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1C', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_circuit']; + + $this->assertEquals(2, count($circuitsarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_station']; + + $this->assertEquals(3, count($circuitsarray)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + foreach ($circuitsarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + + $this->assertEquals(2, count($waverecords)); + + foreach ($waverecords as $record) { + if ($record->name === 'Vague 1a') { + $this->assertEquals('Première vague du test.', $record->description); + } else if ($record->name === 'Vague 1b') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + + $circuitrecords = $this->db->get_records('simulation_circuit'); + + $this->assertTrue(isset($circuitrecords) && !empty($circuitrecords)); + + $this->assertEquals(2, count($circuitrecords)); + + foreach ($circuitrecords as $record) { + if ($record->name === 'Circuit 1') { + $this->assertEquals('20220311110000', $record->timestart); + } + + if ($record->name === 'Circuit 2') { + $this->assertEquals('20220312110000', $record->timestart); + } + } + + $circuitrecords = array_values($circuitrecords); + + $stationrecords = $this->db->get_records('simulation_station'); + + $this->assertTrue(isset($stationrecords) && !empty($stationrecords)); + + $this->assertEquals(3, count($stationrecords)); + + foreach ($stationrecords as $record) { + if ($record->circuitid === $circuitrecords[0]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 1.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 2.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 2') { + $this->assertEquals('La station 2 du circuit 2.', $record->description); + } + } + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $data = []; + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + $circuitrecords = $this->db->get_records('simulation_circuit'); + $stationrecords = $this->db->get_records('simulation_station'); + $substationrecords = $this->db->get_records('simulation_substation'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + $this->assertEquals(1, count($waverecords)); + + $this->assertFalse(isset($circuitrecords) && !empty($circuitrecords)); + $this->assertEquals(0, count($circuitrecords)); + + $this->assertFalse(isset($stationrecords) && !empty($stationrecords)); + $this->assertEquals(0, count($stationrecords)); + + $this->assertFalse(isset($substationrecords) && !empty($substationrecords)); + $this->assertEquals(0, count($substationrecords)); + } + + /** + * Test crud_one_with_children_and_one_bad_update_all_entities_with_data_in_the_database method. + * + * @covers \local_moodleorm\unitofwork + */ + public function test_crud_one_with_children_and_one_bad_update_all_entities_with_data_in_the_database() { + $course = $this->getDataGenerator()->create_course(); + + $simulation = [ + 'course' => $course->id, + 'name' => 'newsim', + 'introformat' => 0, + 'scheduledelimiters' => "Arrival | -30\nInstructions | -15", + ]; + + $simulationid = $this->db->insert_record('simulation', $simulation); + + $wave = [ + 'name' => 'Vague 1', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ]; + + $waveid = $this->db->insert_record('simulation_wave', $wave); + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid1 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_wave', + 'entityuuid' => $uuid1, + 'parentuuid' => null, + 'data' => [ + 'name' => 'Vague 1b', + 'description' => 'Deuxième vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $uuid2 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid2, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 1', + 'timestart' => '20220311110000', + 'timeend' => '20220311120000', + ], + ]; + + $uuid3 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_circuit', + 'entityuuid' => $uuid3, + 'parentuuid' => $uuid1, + 'data' => [ + 'name' => 'Circuit 2', + 'timestart' => '20220312110000', + 'timeend' => '20220312120000', + ], + ]; + + $uuid4 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid4, + 'parentuuid' => $uuid2, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 1.', + 'groupid' => 1, + ], + ]; + + $uuid5 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid5, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 1', + 'description' => 'La station 1 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid6 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_station', + 'entityuuid' => $uuid6, + 'parentuuid' => $uuid3, + 'data' => [ + 'name' => 'Station 2', + 'description' => 'La station 2 du circuit 2.', + 'groupid' => 1, + ], + ]; + + $uuid7 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid7, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1A', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $uuid8 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid8, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1B', + 'userid' => 1, + 'role' => 2, + ], + ]; + + $uuid9 = bin2hex(random_bytes(20)); + + $data[] = [ + 'repositoryname' => 'simulation_substation', + 'entityuuid' => $uuid9, + 'parentuuid' => $uuid5, + 'data' => [ + 'name' => 'Station 1C', + 'userid' => 1, + 'role' => 3, + ], + ]; + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_circuit']; + + $this->assertEquals(2, count($circuitsarray)); + + $circuitsarray = $this->unitofwork->get_registry()['simulation_station']; + + $this->assertEquals(3, count($circuitsarray)); + + foreach ($wavesarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + foreach ($circuitsarray as $entity) { + if (!isset($entity['persistent_object']->get_data()->id)) { + $this->assertFalse(isset($entity['hash'])); + } + } + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + + $this->assertEquals(2, count($waverecords)); + + foreach ($waverecords as $record) { + if ($record->name === 'Vague 1a') { + $this->assertEquals('Première vague du test.', $record->description); + } else if ($record->name === 'Vague 1b') { + $this->assertEquals('Deuxième vague du test.', $record->description); + } + } + + $circuitrecords = $this->db->get_records('simulation_circuit'); + + $this->assertTrue(isset($circuitrecords) && !empty($circuitrecords)); + + $this->assertEquals(2, count($circuitrecords)); + + foreach ($circuitrecords as $record) { + if ($record->name === 'Circuit 1') { + $this->assertEquals('20220311110000', $record->timestart); + } + + if ($record->name === 'Circuit 2') { + $this->assertEquals('20220312110000', $record->timestart); + } + } + + $circuitrecords = array_values($circuitrecords); + + $stationrecords = $this->db->get_records('simulation_station'); + + $this->assertTrue(isset($stationrecords) && !empty($stationrecords)); + + $this->assertEquals(3, count($stationrecords)); + + foreach ($stationrecords as $record) { + if ($record->circuitid === $circuitrecords[0]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 1.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 1') { + $this->assertEquals('La station 1 du circuit 2.', $record->description); + } + + if ($record->circuitid === $circuitrecords[1]->id && $record->name === 'Station 2') { + $this->assertEquals('La station 2 du circuit 2.', $record->description); + } + } + + $this->unitofwork = new unitofwork($this->db, $this->classmap, self::MAIN_TABLE_NAME, $simulationid, 'simulationid'); + + $data = []; + + $data[] = [ + 'entityid' => $waveid, + 'repositoryname' => 'simulation_wave', + 'data' => [ + 'name' => 'Vague 1a', + 'description' => 'Première vague du test.', + 'simulationid' => $simulationid, + ], + ]; + + $this->unitofwork->save($data); + + $wavesarray = $this->unitofwork->get_registry()['simulation_wave']; + + $this->assertEquals(2, count($wavesarray)); + + $totaldirtyentities = 0; + + $dirtyentities = $this->unitofwork->get_dirty(); + + $this->assertFalse(empty($dirtyentities)); + + array_walk_recursive($dirtyentities, function($element) use (&$totaldirtyentities) { + if (!empty($element)) { + $totaldirtyentities++; + } + }); + + $this->assertEquals(10, $totaldirtyentities); + + $this->unitofwork->commit(); + + $this->assertTrue($this->unitofwork->is_committed()); + + $waverecords = $this->db->get_records('simulation_wave'); + $circuitrecords = $this->db->get_records('simulation_circuit'); + $stationrecords = $this->db->get_records('simulation_station'); + $substationrecords = $this->db->get_records('simulation_substation'); + + $this->assertTrue(isset($waverecords) && !empty($waverecords)); + $this->assertEquals(1, count($waverecords)); + + $this->assertFalse(isset($circuitrecords) && !empty($circuitrecords)); + $this->assertEquals(0, count($circuitrecords)); + + $this->assertFalse(isset($stationrecords) && !empty($stationrecords)); + $this->assertEquals(0, count($stationrecords)); + + $this->assertFalse(isset($substationrecords) && !empty($substationrecords)); + $this->assertEquals(0, count($substationrecords)); + } +} diff --git a/tests/wave.php b/tests/wave.php new file mode 100644 index 0000000..546c58c --- /dev/null +++ b/tests/wave.php @@ -0,0 +1,79 @@ +. + +/** + * Class for wave persistence. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +namespace local_moodleorm\tests; + +use \core\persistent; +use \local_moodleorm\traits\unitofworkaware; + +/** + * Class for loading/storing a wave from the DB. + * + * @package local_moodleorm + * @category test + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ +class wave extends persistent { + + use unitofworkaware; + + /** Table name */ + const TABLE = 'simulation_wave'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'id' => array( + 'type' => PARAM_INT, + ), + 'name' => array( + 'type' => PARAM_TEXT, + ), + 'description' => array( + 'type' => PARAM_TEXT, + 'default' => null, + 'null' => NULL_ALLOWED, + ), + 'simulationid' => array( + 'type' => PARAM_INT, + ), + 'timecreated' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + 'timemodified' => array( + 'type' => PARAM_INT, + 'default' => time(), + ), + ); + } +} diff --git a/upgrade.php b/upgrade.php new file mode 100644 index 0000000..ad75a17 --- /dev/null +++ b/upgrade.php @@ -0,0 +1,44 @@ +. + +/** + * The upgrade script. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +/** + * The xmldb_enhancedchoice_upgrade method. + * + * @param mixed $oldversion + * + * @return bool + */ +function xmldb_enhancedchoice_upgrade($oldversion) { + // Moodle v2.9.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.0.0 release upgrade line. + // Put any upgrade step following this. + + // Moodle v3.1.0 release upgrade line. + // Put any upgrade step following this. + + return true; +} diff --git a/version.php b/version.php new file mode 100644 index 0000000..ef7cfe1 --- /dev/null +++ b/version.php @@ -0,0 +1,33 @@ +. + +/** + * Version details. + * + * @package local_moodleorm + * @copyright 2021 Andrew Caya + * @author Andrew Caya + * @license https://directory.fsf.org/wiki/License:Apache-2.0 Apache v2 + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2022121501; // The current module version. +$plugin->requires = 2015111601; +$plugin->component = 'local_moodleorm'; // Full name of the plugin (used for diagnostics). +$plugin->cron = 0; +$plugin->maturity = MATURITY_STABLE; +$plugin->release = 'MoodleORM Plugin version 1.0.0';