diff --git a/modules/Planner/planner_add.php b/modules/Planner/planner_add.php index a27e3f800f..02e43f523f 100644 --- a/modules/Planner/planner_add.php +++ b/modules/Planner/planner_add.php @@ -155,6 +155,10 @@ $form->addHiddenValue('address', $session->get('address')); + // Posted even when Advanced Options are hidden (those inputs are disabled and omitted from POST). + $form->addHiddenValue('viewableStudents', $settingGateway->getSettingByScope('Planner', 'sharingDefaultStudents')); + $form->addHiddenValue('viewableParents', $settingGateway->getSettingByScope('Planner', 'sharingDefaultParents')); + //BASIC INFORMATION $form->addRow()->addHeading('Basic Information', __('Basic Information')); @@ -404,6 +408,9 @@ $row->addCheckbox('notify')->description(__('Notify all class participants')); $row->addSubmit(); + // Module hooks that add fields to the add-lesson form (also used for Import columns) + \Gibbon\Domain\Planner\LessonPlanAddFormBuilder::applyLessonPlannerAddHooks($container, $form); + // CUSTOM FIELDS $container->get(CustomFieldHandler::class)->addCustomFieldsToForm($form, 'Lesson Plan', [], ''); diff --git a/modules/Planner/planner_addProcess.php b/modules/Planner/planner_addProcess.php index 8330f46bba..249d6ef276 100644 --- a/modules/Planner/planner_addProcess.php +++ b/modules/Planner/planner_addProcess.php @@ -25,6 +25,7 @@ use Gibbon\Data\Validator; use Gibbon\Forms\CustomFieldHandler; use Gibbon\Forms\Builder\Storage\FormSessionStorage; +use Gibbon\Domain\System\SettingGateway; require_once __DIR__ . '/../../gibbon.php'; @@ -178,8 +179,9 @@ $gibbonSpaceID = $_POST['gibbonSpaceID'] ?? null; $gibbonTTDayRowClassID = $_POST['gibbonTTDayRowClassID'] ?? null; - $viewableParents = $_POST['viewableParents'] ?? 'Y'; - $viewableStudents = $_POST['viewableStudents'] ?? 'Y'; + $settingGateway = $container->get(SettingGateway::class); + $viewableParents = $_POST['viewableParents'] ?? $settingGateway->getSettingByScope('Planner', 'sharingDefaultParents') ?: 'N'; + $viewableStudents = $_POST['viewableStudents'] ?? $settingGateway->getSettingByScope('Planner', 'sharingDefaultStudents') ?: 'Y'; $gibbonPersonIDCreator = $session->get('gibbonPersonID'); $gibbonPersonIDLastEdit = $session->get('gibbonPersonID'); diff --git a/modules/Planner/planner_edit.php b/modules/Planner/planner_edit.php index 8d03a4f4ea..f9e3e40782 100644 --- a/modules/Planner/planner_edit.php +++ b/modules/Planner/planner_edit.php @@ -171,6 +171,11 @@ $form->addMeta()->addDefaultContent('editProcess'); $form->addHiddenValue('address', $session->get('address')); + + // Posted even when Advanced Options are hidden (those inputs are disabled and omitted from POST). + $form->addHiddenValue('viewableStudents', $values['viewableStudents'] ?? 'Y'); + $form->addHiddenValue('viewableParents', $values['viewableParents'] ?? 'N'); + $form->addHiddenValue('videoLink', $fields['videoLink'] ?? ''); if (!empty($gibbonMarkbookColumnID)) { $form->addHeaderAction('markbook', __('Linked Markbook')) @@ -372,13 +377,20 @@ $row->addYesNo('markbook')->required()->checked('N'); } + // ADVANCED OPTIONS + $form->addRow()->addHeading('Advanced Options', __('Advanced Options')); + + $form->toggleVisibilityByClass('advanced')->onCheckbox('advanced')->when('Y'); + $row = $form->addRow(); + $row->addCheckbox('advanced')->setValue('Y')->checked(true)->description(__('Show Advanced Options')); + // OUTCOMES - $form->addRow()->addHeading('Outcomes', __('Outcomes')); - $form->addRow()->addContent(__('Link this lesson to outcomes (defined in the Manage Outcomes section of the Planner), and track which outcomes are being met in which lessons.')); + $form->addRow()->addClass('advanced')->addHeading('Outcomes', __('Outcomes')); + $form->addRow()->addClass('advanced')->addContent(__('Link this lesson to outcomes (defined in the Manage Outcomes section of the Planner), and track which outcomes are being met in which lessons.')); $allowOutcomeEditing = $settingGateway->getSettingByScope('Planner', 'allowOutcomeEditing'); - $row = $form->addRow(); + $row = $form->addRow()->addClass('advanced'); $customBlocks = $row->addPlannerOutcomeBlocks('outcome', $session, $gibbonYearGroupIDList, $gibbonDepartmentID, $allowOutcomeEditing); $dataBlocks = array('gibbonPlannerEntryID' => $gibbonPlannerEntryID); @@ -397,13 +409,13 @@ //Access - $form->addRow()->addHeading('Access', __('Access')); + $form->addRow()->addClass('advanced')->addHeading('Access', __('Access')); - $row = $form->addRow(); + $row = $form->addRow()->addClass('advanced'); $row->addLabel('viewableStudents', __('Viewable by Students')); $row->addYesNo('viewableStudents')->required(); - $row = $form->addRow(); + $row = $form->addRow()->addClass('advanced'); $row->addLabel('viewableParents', __('Viewable by Parents')); $row->addYesNo('viewableParents')->required(); @@ -412,7 +424,7 @@ $row->addURL('videoLink')->setValue($fields['videoLink'] ?? ''); //Guests - $form->addRow()->addHeading('Guests', __('Current Guests')); + $form->addRow()->addClass('advanced')->addHeading('Guests', __('Current Guests')); $data = array('gibbonPlannerEntryID' => $gibbonPlannerEntryID); $sql = "SELECT title, preferredName, surname, category, gibbonPlannerEntryGuest.* FROM gibbonPlannerEntryGuest JOIN gibbonPerson ON (gibbonPlannerEntryGuest.gibbonPersonID=gibbonPerson.gibbonPersonID) JOIN gibbonRole ON (gibbonPerson.gibbonRoleIDPrimary=gibbonRole.gibbonRoleID) WHERE gibbonPlannerEntryID=:gibbonPlannerEntryID ORDER BY surname, preferredName"; @@ -420,11 +432,11 @@ $results = $pdo->executeQuery($data, $sql); if ($results->rowCount() == 0) { - $form->addRow()->addAlert(__('There are no records to display.'), 'error'); + $form->addRow()->addClass('advanced')->addAlert(__('There are no records to display.'), 'error'); } else { - $form->addRow()->addContent(''.__('Warning').': '.__('If you delete a guest, any unsaved changes to this planner entry will be lost!'))->wrap('', ''); + $form->addRow()->addClass('advanced')->addContent(''.__('Warning').': '.__('If you delete a guest, any unsaved changes to this planner entry will be lost!'))->wrap('', ''); - $table = $form->addRow()->addTable()->addClass('colorOddEven'); + $table = $form->addRow()->addClass('advanced')->addTable()->addClass('colorOddEven'); $header = $table->addHeaderRow(); $header->addContent(__('Name')); @@ -439,9 +451,9 @@ } } - $form->addRow()->addHeading('New Guests', __('New Guests')); + $form->addRow()->addClass('advanced')->addHeading('New Guests', __('New Guests')); - $row = $form->addRow(); + $row = $form->addRow()->addClass('advanced'); $row->addLabel('guests', __('Guest List')); $row->addSelectUsers('guests', $session->get('gibbonSchoolYearID'))->selectMultiple(); @@ -453,7 +465,7 @@ 'Guest Parent' => __('Guest Parent'), 'Other Guest' => __('Other Guest'), ); - $row = $form->addRow(); + $row = $form->addRow()->addClass('advanced'); $row->addLabel('role', __('Role')); $row->addSelect('role')->fromArray($roles); diff --git a/modules/Planner/planner_editProcess.php b/modules/Planner/planner_editProcess.php index 4a85dd9b59..d83d1c6ba8 100644 --- a/modules/Planner/planner_editProcess.php +++ b/modules/Planner/planner_editProcess.php @@ -70,10 +70,10 @@ try { if ($highestAction == 'Lesson Planner_viewEditAllClasses') { $data = array('gibbonPlannerEntryID' => $gibbonPlannerEntryID); - $sql = 'SELECT gibbonPlannerEntryID, gibbonUnitID, gibbonCourse.nameShort AS course, gibbonCourseClass.nameShort AS class, gibbonPlannerEntry.name, summary, gibbonPlannerEntry.fields FROM gibbonPlannerEntry JOIN gibbonCourseClass ON (gibbonPlannerEntry.gibbonCourseClassID=gibbonCourseClass.gibbonCourseClassID) JOIN gibbonCourse ON (gibbonCourse.gibbonCourseID=gibbonCourseClass.gibbonCourseID) WHERE gibbonPlannerEntryID=:gibbonPlannerEntryID'; + $sql = 'SELECT gibbonPlannerEntryID, gibbonUnitID, gibbonCourse.nameShort AS course, gibbonCourseClass.nameShort AS class, gibbonPlannerEntry.name, summary, gibbonPlannerEntry.viewableParents, gibbonPlannerEntry.viewableStudents, gibbonPlannerEntry.fields FROM gibbonPlannerEntry JOIN gibbonCourseClass ON (gibbonPlannerEntry.gibbonCourseClassID=gibbonCourseClass.gibbonCourseClassID) JOIN gibbonCourse ON (gibbonCourse.gibbonCourseID=gibbonCourseClass.gibbonCourseID) WHERE gibbonPlannerEntryID=:gibbonPlannerEntryID'; } else { $data = array('gibbonPlannerEntryID' => $gibbonPlannerEntryID, 'gibbonPersonID' => $session->get('gibbonPersonID')); - $sql = "SELECT gibbonPlannerEntryID, gibbonUnitID, gibbonCourse.nameShort AS course, gibbonCourseClass.nameShort AS class, gibbonPlannerEntry.name, summary, role, gibbonPlannerEntry.fields FROM gibbonPlannerEntry JOIN gibbonCourseClass ON (gibbonPlannerEntry.gibbonCourseClassID=gibbonCourseClass.gibbonCourseClassID) JOIN gibbonCourseClassPerson ON (gibbonCourseClass.gibbonCourseClassID=gibbonCourseClassPerson.gibbonCourseClassID) JOIN gibbonCourse ON (gibbonCourse.gibbonCourseID=gibbonCourseClass.gibbonCourseID) WHERE gibbonCourseClassPerson.gibbonPersonID=:gibbonPersonID AND role='Teacher' AND gibbonPlannerEntryID=:gibbonPlannerEntryID"; + $sql = "SELECT gibbonPlannerEntryID, gibbonUnitID, gibbonCourse.nameShort AS course, gibbonCourseClass.nameShort AS class, gibbonPlannerEntry.name, summary, role, gibbonPlannerEntry.viewableParents, gibbonPlannerEntry.viewableStudents, gibbonPlannerEntry.fields FROM gibbonPlannerEntry JOIN gibbonCourseClass ON (gibbonPlannerEntry.gibbonCourseClassID=gibbonCourseClass.gibbonCourseClassID) JOIN gibbonCourseClassPerson ON (gibbonCourseClass.gibbonCourseClassID=gibbonCourseClassPerson.gibbonCourseClassID) JOIN gibbonCourse ON (gibbonCourse.gibbonCourseID=gibbonCourseClass.gibbonCourseID) WHERE gibbonCourseClassPerson.gibbonPersonID=:gibbonPersonID AND role='Teacher' AND gibbonPlannerEntryID=:gibbonPlannerEntryID"; } $result = $connection2->prepare($sql); $result->execute($data); @@ -200,8 +200,8 @@ $gibbonSpaceID = $_POST['gibbonSpaceID'] ?? null; $gibbonTTDayRowClassID = $_POST['gibbonTTDayRowClassID'] ?? null; - $viewableParents = $_POST['viewableParents'] ?? ''; - $viewableStudents = $_POST['viewableStudents'] ?? ''; + $viewableParents = $_POST['viewableParents'] ?? ($row['viewableParents'] ?? ''); + $viewableStudents = $_POST['viewableStudents'] ?? ($row['viewableStudents'] ?? ''); $gibbonPersonIDCreator = $session->get('gibbonPersonID'); $gibbonPersonIDLastEdit = $session->get('gibbonPersonID'); @@ -209,10 +209,9 @@ $customRequireFail = false; $fields = $container->get(CustomFieldHandler::class)->getFieldDataFromPOST('Lesson Plan', [], $customRequireFail); - if (isset($_POST['videoLink'])) { - $fields = !empty($fields) ? json_decode($fields, true) : []; - $fields = json_encode(['videoLink'=> $_POST['videoLink'] ?? ''] + $fields); - } + $existingFields = !empty($row['fields']) ? json_decode($row['fields'], true) : []; + $fields = !empty($fields) ? json_decode($fields, true) : []; + $fields = json_encode(['videoLink' => $_POST['videoLink'] ?? ($existingFields['videoLink'] ?? '')] + $fields); if ($viewBy == '' or $gibbonCourseClassID == '' or $date == '' or $timeStart == '' or $timeEnd == '' or $name == '' or $homework == '' or $viewableParents == '' or $viewableStudents == '' or ($homework == 'Y' and ($homeworkDetails == '' or $homeworkDueDate == ''))) { $URL .= "&return=error3$params"; diff --git a/resources/imports/plannerLessons.yml b/resources/imports/plannerLessons.yml new file mode 100644 index 0000000000..e677d09f02 --- /dev/null +++ b/resources/imports/plannerLessons.yml @@ -0,0 +1,156 @@ +details: + type: plannerLessons + name: Lessons + table: gibbonPlannerEntry + dynamicForm: plannerLessonAdd + modes: { update: true, insert: true, export: true } +access: + module: Planner + action: Lesson Planner_viewEditAllClasses +primaryKey: + gibbonPlannerEntryID +uniqueKeys: + - [ gibbonCourseClassID, date, timeStart, name ] +table: + gibbonSchoolYearID: + name: "School Year" + desc: "School year name, as set in School Admin. Must already exist." + args: { filter: schoolyear, required: true, readonly: true, custom: true } + relationship: { table: gibbonSchoolYear, key: gibbonSchoolYearID, field: name } + gibbonCourseID: + name: "Course" + desc: "Short Name" + args: { filter: string, required: true, readonly: true, custom: true } + relationship: { table: gibbonCourse, key: gibbonCourseID, field: [ nameShort, gibbonSchoolYearID ] } + gibbonCourseClassID: + name: "Class" + desc: "Short Name" + args: { filter: string, required: true } + relationship: { table: gibbonCourseClass, key: gibbonCourseClassID, field: [ nameShort, gibbonCourseID ] } + gibbonUnitID: + name: "Unit" + desc: "Name" + args: { filter: string, custom: true } + relationship: { table: gibbonUnit, key: gibbonUnitID, field: [ name, gibbonCourseID ] } + name: + name: "Lesson Name" + desc: "" + args: { filter: string, required: true } + summary: + name: "Summary" + desc: "" + args: { filter: string } + date: + name: "Date" + desc: "" + args: { filter: date, required: true } + timeStart: + name: "Start Time" + desc: "Format: hh:mm (24hr)" + args: { filter: time, required: true, custom: true } + timeEnd: + name: "End Time" + desc: "Format: hh:mm (24hr)" + args: { filter: time, required: true, custom: true } + gibbonSpaceID: + name: "Location" + desc: "" + args: { filter: string, custom: true } + relationship: { table: gibbonSpace, key: gibbonSpaceID, field: name } + description: + name: "Lesson Details" + desc: "" + args: { filter: html } + teachersNotes: + name: "Teacher's Notes" + desc: "" + args: { filter: html } + homework: + name: "Add Homework?" + desc: "" + args: { filter: yesno, required: true, custom: true } + homeworkDueDateTime: + name: "Due Date" + desc: "Date is required, time is optional." + args: { filter: timestamp, custom: true } + homeworkTimeCap: + name: "Time Cap?" + desc: "The maximum time, in minutes, for students to work on this." + args: { filter: numeric, custom: true } + homeworkDetails: + name: "Homework Details" + desc: "" + args: { filter: html } + homeworkSubmission: + name: "Online Submission?" + desc: "" + args: { filter: yesno, custom: true } + homeworkSubmissionDateOpen: + name: "Submission Open Date" + desc: "" + args: { filter: date, custom: true } + homeworkSubmissionDrafts: + name: "Drafts" + desc: "Blank for none, or 1, 2, or 3." + args: { filter: string, custom: true } + homeworkSubmissionType: + name: "Submission Type" + desc: "Link, File, or Link/File" + args: { filter: string, custom: true } + homeworkSubmissionRequired: + name: "Submission Required" + desc: "Optional or Required" + args: { filter: string, custom: true } + homeworkCrowdAssess: + name: "Crowd Assessment?" + desc: "" + args: { filter: yesno, custom: true } + homeworkCrowdAssessClassmatesRead: + name: "Classmates" + desc: "" + args: { filter: yesno, custom: true } + homeworkCrowdAssessOtherStudentsRead: + name: "Other Students" + desc: "" + args: { filter: yesno, custom: true } + homeworkCrowdAssessOtherTeachersRead: + name: "Other Teachers" + desc: "" + args: { filter: yesno, custom: true } + homeworkCrowdAssessSubmitterParentsRead: + name: "Submitter's Parents" + desc: "" + args: { filter: yesno, custom: true } + homeworkCrowdAssessClassmatesParentsRead: + name: "Classmates's Parents" + desc: "" + args: { filter: yesno, custom: true } + homeworkCrowdAssessOtherParentsRead: + name: "Other Parents" + desc: "" + args: { filter: yesno, custom: true } + viewableStudents: + name: "Viewable by Students" + desc: "" + args: { filter: yesno, required: true, custom: true } + viewableParents: + name: "Viewable by Parents" + desc: "" + args: { filter: yesno, required: true, custom: true } + videoLink: + name: "Online Lesson" + desc: "Displays a video link for online lessons" + args: { filter: url, custom: true, readonly: true, serialize: fields, customField: videoLink } + gibbonPersonIDCreator: + name: "Created By" + desc: "Username or Email (if unique)" + args: { filter: string, required: true, custom: true } + relationship: { table: gibbonPerson, key: gibbonPersonID, field: username|email } + gibbonPersonIDLastEdit: + name: "Last Edited By" + desc: "Copied from Created By" + args: { filter: string, hidden: true, linked: gibbonPersonIDCreator, custom: true } + fields: + name: "Custom Fields" + desc: "" + args: { filter: customfield, serialize: fields, custom: true, hidden: true } diff --git a/src/Data/ImportType.php b/src/Data/ImportType.php index d447d8e81e..e3cd4dcd45 100644 --- a/src/Data/ImportType.php +++ b/src/Data/ImportType.php @@ -266,6 +266,7 @@ public static function loadImportTypeList( if (isset($fileData['details']) && isset($fileData['details']['type'])) { $fileData['details']['grouping'] = (isset($fileData['access']['module']))? $fileData['access']['module'] : 'General'; + $fileData = self::applyDynamicFormFields($fileData); $importTypes[ $fileData['details']['type'] ] = new ImportType($fileData, $passwordPolicy, $pdo, $validateStructure); } } @@ -283,6 +284,7 @@ public static function loadImportTypeList( if (isset($fileData['details']) && isset($fileData['details']['type'])) { $fileData['details']['grouping'] = '* Custom Imports'; $fileData['details']['custom'] = true; + $fileData = self::applyDynamicFormFields($fileData); $importTypes[ $fileData['details']['type'] ] = new ImportType( $fileData, $passwordPolicy, @@ -343,10 +345,36 @@ public static function loadImportType( $yaml = new Yaml(); $fileData = $yaml::parse(file_get_contents($path)); + $fileData = self::applyDynamicFormFields($fileData); return new ImportType($fileData, $passwordPolicy, $pdo); } + /** + * Merge live form fields into YAML import types that opt in via details.dynamicForm. + * + * @param array $fileData + * @return array + */ + protected static function applyDynamicFormFields(array $fileData) + { + global $container; + + if (($fileData['details']['dynamicForm'] ?? '') !== 'plannerLessonAdd') { + return $fileData; + } + + if (empty($container)) { + return $fileData; + } + + try { + return LessonPlanFormFieldParser::mergeIntoImportData($fileData, $container); + } catch (\Throwable $e) { + return $fileData; + } + } + /** * Is Import Accessible * @@ -498,16 +526,28 @@ protected function loadRelationalData(Connection $pdo) } $type = $this->customFields[ $customFieldName ]['type']; + $options = array_values(array_filter(array_map('trim', explode(',', (string) $this->customFields[ $customFieldName ]['options'])), 'strlen')); if ($type == 'varchar') { $this->setField($fieldName, 'kind', 'char'); $this->setField($fieldName, 'type', 'varchar'); $this->setField($fieldName, 'length', $this->customFields[ $customFieldName ]['options']); - } elseif ($type == 'select') { + } elseif ($type == 'yesno' || $type == 'checkbox' || ($type == 'checkboxes' && count($options) <= 1)) { + $this->setField($fieldName, 'kind', 'yesno'); + $this->setField($fieldName, 'type', 'enum'); + $this->setField($fieldName, 'elements', ['Y', 'N']); + $args = $this->getField($fieldName, 'args'); + if (is_array($args)) { + $args['filter'] = 'yesno'; + if ($type == 'checkboxes' && count($options) === 1) { + $args['checkboxOnValue'] = $options[0]; + } + $this->setField($fieldName, 'args', $args); + } + } elseif ($type == 'select' || $type == 'radio' || $type == 'checkboxes') { $this->setField($fieldName, 'kind', 'enum'); $this->setField($fieldName, 'type', 'enum'); - $elements = explode(',', $this->customFields[ $customFieldName ]['options']); - $this->setField($fieldName, 'elements', $elements); - $this->setField($fieldName, 'length', count($elements)); + $this->setField($fieldName, 'elements', $options); + $this->setField($fieldName, 'length', count($options)); } elseif ($type == 'text' || $type == 'date') { $this->setField($fieldName, 'kind', $type); $this->setField($fieldName, 'type', $type); @@ -591,9 +631,14 @@ protected function setValueTypeByFilter($fieldName) switch ($this->getField($fieldName, 'filter')) { case 'string': $type = 'text'; $kind = 'text'; break; + case 'html': $type = 'text'; $kind = 'text'; break; case 'date': $type = 'date'; $kind = 'date'; break; + case 'time': $type = 'time'; $kind = 'time'; break; case 'url': $type = 'text'; $kind = 'text'; break; case 'email': $type = 'text'; $kind = 'text'; break; + case 'yesno': $type = 'enum'; $kind = 'yesno'; break; + case 'numeric': $type = 'int'; $kind = 'integer'; break; + case 'csv': $type = 'text'; $kind = 'enum'; break; } $this->setField($fieldName, 'type', $type); @@ -975,6 +1020,24 @@ public function filterFieldValue($fieldName, $value) return $value; } + /** + * Map a validated import value onto the stored custom-field value. + * Single checkboxes use yesno (Y/N) like other imports, but persist the option text when checked. + * + * @param string $fieldName + * @param mixed $value + * @return mixed + */ + public function storedFieldValue($fieldName, $value) + { + $onValue = $this->getField($fieldName, 'checkboxOnValue'); + if ($onValue !== '' && $onValue !== false && $onValue !== null) { + return $value === 'Y' ? $onValue : ''; + } + + return $value; + } + /** * Compares the value type, legth and properties with the expected values for the table column * @@ -992,6 +1055,11 @@ public function validateFieldValue($fieldName, $value) return true; } + // Optional fields may be blank (unchecked checkboxes, unused dropdowns, etc.) + if ($value === '' || $value === null) { + return $this->isFieldRequired($fieldName) ? false : $value; + } + // Validate based on filter type (from args) $filter = $this->getField($fieldName, 'filter'); @@ -1076,8 +1144,13 @@ public function validateFieldValue($fieldName, $value) case 'enum': $elements = $this->getField($fieldName, 'elements'); $elements = array_map('trim', (array) $elements); - if (!in_array($value, $elements)) { - return false; + $values = $filter == 'csv' + ? array_filter(array_map('trim', explode(',', (string) $value)), 'strlen') + : [$value]; + foreach ($values as $item) { + if (!in_array($item, $elements)) { + return false; + } } break; } @@ -1239,8 +1312,11 @@ public function readableFieldType($fieldName) return __('True or False'); case 'enum': - $options = implode('
', (array) $this->getField($fieldName, 'elements')); - return ''.__('Options').''; + $elements = array_filter((array) $this->getField($fieldName, 'elements')); + if (!empty($elements)) { + return __('One of: {options}', ['options' => implode(', ', $elements)]); + } + return __('Options'); default: return __(ucfirst($kind)); diff --git a/src/Data/Importer.php b/src/Data/Importer.php index dd31cafb29..2d128e01cf 100644 --- a/src/Data/Importer.php +++ b/src/Data/Importer.php @@ -365,7 +365,7 @@ public function buildTableData($importType, $columnOrder, $customValues = []) if ($importType->isFieldRelational($fieldName) && !empty($this->cachedData[$rowIndex][$fieldName])) { // Grab existing cached relational data, to prevent multiple identical queries in multi-table imports $value = $this->cachedData[$rowIndex][$fieldName]; - } elseif ($importType->isFieldRelational($fieldName)) { + } elseif ($importType->isFieldRelational($fieldName) && !$importType->isFieldLinked($fieldName)) { // Otherwise build a query to grab the relational data. $join = $on = ''; extract($importType->getField($fieldName, 'relationship')); @@ -460,7 +460,7 @@ public function buildTableData($importType, $columnOrder, $customValues = []) // Otherwise collect values in an array $customField = $importType->getField($fieldName, 'customField'); if (empty($customField)) $customField = $importType->getField($fieldName, 'name'); - $this->serializeData[$serialize][$customField] = $value; + $this->serializeData[$serialize][$customField] = $importType->storedFieldValue($fieldName, $value); } } else { // Add the field to the field set for this row diff --git a/src/Data/LessonPlanFormFieldParser.php b/src/Data/LessonPlanFormFieldParser.php new file mode 100644 index 0000000000..eb16ced707 --- /dev/null +++ b/src/Data/LessonPlanFormFieldParser.php @@ -0,0 +1,486 @@ +. + 20|*/ + +namespace Gibbon\Data; + +use Gibbon\Domain\Planner\LessonPlanAddFormBuilder; +use Gibbon\Forms\Form; +use Gibbon\Forms\Input\Checkbox; +use Gibbon\Forms\Input\CustomField; +use Gibbon\Forms\Input\Date; +use Gibbon\Forms\Input\Editor; +use Gibbon\Forms\Input\FileUpload; +use Gibbon\Forms\Input\Input; +use Gibbon\Forms\Input\Number; +use Gibbon\Forms\Input\Time; +use Gibbon\Forms\Input\Toggle; +use Gibbon\Forms\Layout\Column; +use Gibbon\Forms\Layout\Label; +use Gibbon\Forms\Layout\Row; +use League\Container\Container; + +/** + * Walks the live Add Lesson Plan form and turns visible inputs into import fields. + */ +class LessonPlanFormFieldParser +{ + /** + * Form input names that are UI-only and should not become import columns. + * + * @var string[] + */ + protected static $skipNames = [ + 'address', + 'q', + 'csrftoken', + 'nonce', + 'advanced', + 'notify', + 'markbook', + 'guests', + 'role', + 'outcome', + 'courseClassName', + 'gibbonTTDayRowClassID', + 'homeworkDueDateTime', + 'homeworkCrowdAssessControl', + 'homeworkCrowdAssessClassTeacher', + 'homeworkCrowdAssessClassSubmitter', + ]; + + /** + * Map add-form input names onto gibbonPlannerEntry / import field names. + * + * @var string[] + */ + protected static $aliases = [ + 'homeworkDueDate' => 'homeworkDueDateTime', + ]; + + /** + * Merge fields discovered on the live add-lesson form into a YAML import definition. + * + * @param array $fileData + * @param Container $container + * @return array + */ + public static function mergeIntoImportData(array $fileData, Container $container): array + { + $form = LessonPlanAddFormBuilder::createForImport($container); + $liveFields = self::parseForm($form); + + $existing = $fileData['table'] ?? $fileData['fields'] ?? []; + $merged = []; + $used = []; + + foreach ($existing as $fieldName => $definition) { + $hidden = !empty($definition['args']['hidden']); + $readonly = !empty($definition['args']['readonly']); + $inLive = isset($liveFields[$fieldName]); + foreach (self::$aliases as $formName => $importName) { + if ($importName === $fieldName && isset($liveFields[$formName])) { + $inLive = true; + } + } + + if (!$inLive && $readonly && empty($definition['args']['linked'] ?? '')) { + $merged[$fieldName] = $definition; + $used[$fieldName] = true; + } + } + + foreach ($liveFields as $formName => $liveDefinition) { + $fieldName = self::$aliases[$formName] ?? $formName; + + if (isset($existing[$fieldName])) { + $merged[$fieldName] = self::overlayDefinition($existing[$fieldName], $liveDefinition); + } else { + $merged[$fieldName] = $liveDefinition; + } + $used[$fieldName] = true; + } + + foreach ($existing as $fieldName => $definition) { + if (empty($used[$fieldName])) { + $merged[$fieldName] = $definition; + } + } + + if (isset($fileData['table'])) { + $fileData['table'] = $merged; + } else { + $fileData['fields'] = $merged; + if (!empty($fileData['tables']['gibbonPlannerEntry']['fields'])) { + $fileData['tables']['gibbonPlannerEntry']['fields'] = array_keys($merged); + } + } + + return $fileData; + } + + /** + * @param Form $form + * @return array + */ + public static function parseForm(Form $form): array + { + $fields = []; + foreach ($form->getRows() as $row) { + self::collectFromRow($row, $fields, [], false); + } + + return $fields; + } + + /** + * @param Row $row + * @param array $fields + */ + protected static function collectFromRow(Row $row, array &$fields, array $inheritedLabels = [], bool $conditional = false) + { + $labelByFor = $inheritedLabels; + $conditional = $conditional || self::isConditionalRow($row); + + foreach ($row->getElements() as $element) { + if ($element instanceof Label) { + $for = $element->getAttribute('for'); + if (!empty($for)) { + $labelByFor[$for] = $element; + } + } + } + + foreach ($row->getElements() as $element) { + if ($element instanceof Column) { + self::collectFromRow($element, $fields, $labelByFor, $conditional); + continue; + } + + if ($element instanceof Label) { + continue; + } + + if (!$element instanceof Input) { + continue; + } + + $name = self::resolveName($element, $labelByFor); + $name = str_replace(['[]', 'CustomEditor'], '', $name); + + if (empty($name) || isset($fields[$name]) || in_array($name, self::$skipNames, true)) { + continue; + } + + if (method_exists($element, 'getDisabled') && $element->getDisabled()) { + continue; + } + + $label = $labelByFor[$name] ?? $labelByFor[$name.'CustomEditor'] ?? null; + $fields[$name] = self::definitionFromInput($element, $label, $conditional, $labelByFor); + } + } + + /** + * @param object $element + * @return string + */ + protected static function resolveName($element, array $labelByFor = []): string + { + if (method_exists($element, 'getName')) { + $name = (string) $element->getName(); + if (!empty($name) && $name !== 'label') { + return $name; + } + } + + if ($element instanceof CustomField && !empty($labelByFor)) { + return (string) array_key_first($labelByFor); + } + + return (string) $element->getID(); + } + + /** + * @param Row $row + * @return bool + */ + protected static function isConditionalRow(Row $row): bool + { + $class = $row->getClass() ?? ''; + return (bool) preg_match('/\b(homework|homeworkSubmission|homeworkCrowdAssess|advanced)\b/', $class); + } + + /** + * @param Input $element + * @param Label|null $label + * @param bool $conditional + * @param array $labelByFor + * @return array + */ + protected static function definitionFromInput($element, $label, bool $conditional, array $labelByFor = []): array + { + $fieldName = self::resolveName($element, $labelByFor); + $fieldName = str_replace(['[]', 'CustomEditor'], '', $fieldName); + + $name = ''; + $desc = ''; + if ($label instanceof Label) { + $name = trim(strip_tags((string) $label->getLabelText())); + $desc = trim(strip_tags((string) $label->getDescription())); + } + + if ($name === '' && $element instanceof Checkbox) { + $name = self::checkboxLabel($element); + } + + if ($name === '' || strcasecmp($name, __('Field')) === 0) { + $name = self::humanizeFieldName($fieldName); + } + + $required = false; + if (!$conditional && method_exists($element, 'getRequired')) { + $required = (bool) $element->getRequired(); + } + + $filter = self::filterFromElement($element); + $args = [ + 'filter' => $filter, + 'custom' => true, + ]; + + if ($required) { + $args['required'] = true; + } + + if ($filter === 'yesno' && $element instanceof CustomField) { + $options = self::customFieldOptions($element); + if (count($options) === 1) { + $args['checkboxOnValue'] = $options[0]; + } + } + + if (preg_match('/^custom(\d+)$/', $fieldName, $match)) { + $args['readonly'] = true; + $args['serialize'] = 'fields'; + $args['customField'] = $match[1]; + } elseif ($fieldName === 'videoLink') { + $args['readonly'] = true; + $args['serialize'] = 'fields'; + $args['customField'] = 'videoLink'; + } + + return [ + 'name' => $name, + 'desc' => $desc, + 'args' => $args, + ]; + } + + /** + * @param Checkbox $element + * @return string + */ + protected static function checkboxLabel(Checkbox $element): string + { + try { + $reflection = new \ReflectionProperty($element, 'description'); + $reflection->setAccessible(true); + $description = $reflection->getValue($element); + if (is_string($description) && $description !== '') { + return trim(strip_tags($description)); + } + } catch (\ReflectionException $e) { + } + + return ''; + } + + /** + * @param string $fieldName + * @return string + */ + protected static function humanizeFieldName(string $fieldName): string + { + $fieldName = preg_replace('/^custom\d+$/', '', $fieldName); + $spaced = preg_replace('/(?<=[a-z])(?=[A-Z])/', ' ', $fieldName); + $spaced = str_replace('_', ' ', (string) $spaced); + + return trim(ucwords($spaced)); + } + + /** + * @param object $element + * @return string + */ + protected static function filterFromElement($element): string + { + $customType = ''; + $customField = $element instanceof CustomField ? $element : null; + if ($element instanceof CustomField) { + $customType = strtolower(self::customFieldType($element)); + $inner = self::unwrapCustomField($element); + if ($inner instanceof Input) { + $element = $inner; + } + } + + switch ($customType) { + case 'date': + return 'date'; + case 'time': + return 'time'; + case 'number': + return 'numeric'; + case 'editor': + case 'code': + case 'text': + return 'html'; + case 'yesno': + case 'checkbox': + return 'yesno'; + case 'checkboxes': + if ($customField instanceof CustomField && count(self::customFieldOptions($customField)) <= 1) { + return 'yesno'; + } + return 'csv'; + case 'radio': + case 'select': + return 'csv'; + case 'url': + return 'url'; + case 'file': + case 'image': + return 'string'; + } + + if ($element instanceof Date) { + return 'date'; + } + if ($element instanceof Time) { + return 'time'; + } + if ($element instanceof Number) { + return 'numeric'; + } + if ($element instanceof Editor) { + return 'html'; + } + if ($element instanceof Toggle) { + return 'yesno'; + } + if ($element instanceof Checkbox) { + return 'yesno'; + } + if ($element instanceof FileUpload) { + return 'string'; + } + + $htmlType = method_exists($element, 'getAttribute') ? strtolower((string) $element->getAttribute('type')) : ''; + if ($htmlType === 'checkbox') { + return 'yesno'; + } + if ($htmlType === 'url') { + return 'url'; + } + + return 'string'; + } + + /** + * @param CustomField $element + * @return string[] + */ + protected static function customFieldOptions(CustomField $element): array + { + try { + $reflection = new \ReflectionProperty($element, 'fields'); + $reflection->setAccessible(true); + $fields = $reflection->getValue($element); + $options = $fields['options'] ?? ''; + } catch (\ReflectionException $e) { + return []; + } + + if (is_array($options)) { + return array_values(array_filter(array_map('trim', $options), 'strlen')); + } + + return array_values(array_filter(array_map('trim', explode(',', (string) $options)), 'strlen')); + } + + /** + * @param CustomField $element + * @return string + */ + protected static function customFieldType(CustomField $element): string + { + try { + $reflection = new \ReflectionProperty($element, 'type'); + $reflection->setAccessible(true); + return (string) $reflection->getValue($element); + } catch (\ReflectionException $e) { + return ''; + } + } + + /** + * @param CustomField $element + * @return Input|null + */ + protected static function unwrapCustomField(CustomField $element) + { + try { + $reflection = new \ReflectionProperty($element, 'customField'); + $reflection->setAccessible(true); + $inner = $reflection->getValue($element); + return $inner instanceof Input ? $inner : null; + } catch (\ReflectionException $e) { + return null; + } + } + + /** + * @param array $base + * @param array $live + * @return array + */ + protected static function overlayDefinition(array $base, array $live): array + { + $liveName = trim((string) ($live['name'] ?? '')); + if ($liveName !== '' && strcasecmp($liveName, __('Field')) !== 0) { + $base['name'] = $liveName; + } + if (!empty($live['desc'])) { + $base['desc'] = $live['desc']; + } + + $liveArgs = $live['args'] ?? []; + $baseArgs = $base['args'] ?? []; + + if (isset($liveArgs['required']) && empty($baseArgs['required'])) { + $baseArgs['required'] = $liveArgs['required']; + } + + $base['args'] = $baseArgs; + + return $base; + } +} diff --git a/src/Domain/Planner/LessonPlanAddFormBuilder.php b/src/Domain/Planner/LessonPlanAddFormBuilder.php new file mode 100644 index 0000000000..cab3042f8f --- /dev/null +++ b/src/Domain/Planner/LessonPlanAddFormBuilder.php @@ -0,0 +1,241 @@ +. + 20|*/ + +namespace Gibbon\Domain\Planner; + +use Gibbon\Contracts\Database\Connection; +use Gibbon\Contracts\Services\Session; +use Gibbon\Domain\System\HookGateway; +use Gibbon\Domain\System\SettingGateway; +use Gibbon\Forms\CustomFieldHandler; +use Gibbon\Forms\DatabaseFormFactory; +use Gibbon\Forms\Form; +use League\Container\Container; + +/** + * Builds the Add Lesson Plan form as the web UI does, including custom fields + * and Lesson Planner Add hooks from other modules. + */ +class LessonPlanAddFormBuilder +{ + /** + * Build a canonical add-lesson form (class view) for import column discovery. + * + * @param Container $container + * @return Form + */ + public static function createForImport(Container $container): Form + { + global $guid, $connection2; + + $session = $container->get(Session::class); + $pdo = $container->get(Connection::class); + $settingGateway = $container->get(SettingGateway::class); + + $homeworkNameSingular = $settingGateway->getSettingByScope('Planner', 'homeworkNameSingular'); + + $form = Form::create('plannerLessonImport', ''); + $form->setFactory(DatabaseFormFactory::create($pdo)); + $form->addHiddenValue('address', '/modules/Planner/planner_add.php'); + + $form->addRow()->addHeading('Basic Information', __('Basic Information')); + + $data = ['gibbonSchoolYearID' => $session->get('gibbonSchoolYearID')]; + $sql = 'SELECT gibbonCourseClass.gibbonCourseClassID AS value, CONCAT(gibbonCourse.nameShort,".", gibbonCourseClass.nameShort) AS name FROM gibbonCourseClass JOIN gibbonCourse ON (gibbonCourseClass.gibbonCourseID=gibbonCourse.gibbonCourseID) WHERE gibbonCourse.gibbonSchoolYearID=:gibbonSchoolYearID ORDER BY name'; + $row = $form->addRow(); + $row->addLabel('gibbonCourseClassID', __('Class')); + $row->addSelect('gibbonCourseClassID')->fromQuery($pdo, $sql, $data)->required()->placeholder(); + + $sql = "SELECT GROUP_CONCAT(gibbonCourseClassID SEPARATOR ' ') AS chainedTo, gibbonUnit.gibbonUnitID as value, name FROM gibbonUnit JOIN gibbonUnitClass ON (gibbonUnit.gibbonUnitID=gibbonUnitClass.gibbonUnitID) WHERE active='Y' AND running='Y' GROUP BY gibbonUnit.gibbonUnitID ORDER BY ordering, name"; + $row = $form->addRow(); + $row->addLabel('gibbonUnitID', __('Unit')); + $row->addSelect('gibbonUnitID')->fromQueryChained($pdo, $sql, [], 'gibbonCourseClassID')->placeholder(); + + $row = $form->addRow(); + $row->addLabel('name', __('Lesson Name')); + $row->addTextField('name')->maxLength(50)->required(); + + $row = $form->addRow(); + $row->addLabel('summary', __('Summary')); + $row->addTextField('summary')->maxLength(255); + + $row = $form->addRow(); + $row->addLabel('date', __('Date')); + $row->addDate('date')->required(); + + $row = $form->addRow(); + $row->addLabel('timeStart', __('Start Time'))->description(__("Format: hh:mm (24hr)")); + $row->addTime('timeStart')->required(); + + $row = $form->addRow(); + $row->addLabel('timeEnd', __('End Time'))->description(__("Format: hh:mm (24hr)")); + $row->addTime('timeEnd')->required(); + + $row = $form->addRow(); + $row->addLabel('gibbonSpaceID', __('Location')); + $row->addSelectSpace('gibbonSpaceID')->placeholder(); + + $form->addRow()->addHeading('Lesson Content', __('Lesson Content')); + + $description = $settingGateway->getSettingByScope('Planner', 'lessonDetailsTemplate'); + $row = $form->addRow(); + $column = $row->addColumn(); + $column->addLabel('description', __('Lesson Details')); + $column->addEditor('description', $guid)->setRows(20)->showMedia()->setValue($description); + + $teachersNotes = $settingGateway->getSettingByScope('Planner', 'teachersNotesTemplate'); + $row = $form->addRow(); + $column = $row->addColumn(); + $column->addLabel('teachersNotes', __('Teacher\'s Notes')); + $column->addEditor('teachersNotes', $guid)->setRows(5)->showMedia()->setValue($teachersNotes); + + $form->addRow()->addHeading('Homework', __($homeworkNameSingular)); + + $form->toggleVisibilityByClass('homework')->onClick('homework')->when('Y'); + $row = $form->addRow(); + $row->addLabel('homework', __('Add {homeworkName}?', ['homeworkName' => __($homeworkNameSingular)])); + $row->addYesNo('homework')->required()->checked('N'); + + $row = $form->addRow()->addClass('homework'); + $row->addLabel('homeworkDueDate', __('Due Date'))->description(__('Date is required, time is optional.')); + $col = $row->addColumn('homeworkDueDate')->addClass('homework'); + $col->addDate('homeworkDueDate')->addClass('mr-2')->required(); + $col->addTime('homeworkDueDateTime'); + + $row = $form->addRow()->addClass('homework'); + $row->addLabel('homeworkTimeCap', __('Time Cap?'))->description(__('The maximum time, in minutes, for students to work on this.')); + $row->addNumber('homeworkTimeCap'); + + $row = $form->addRow()->addClass('homework'); + $column = $row->addColumn(); + $column->addLabel('homeworkDetails', __('{homeworkName} Details', ['homeworkName' => __($homeworkNameSingular)])); + $column->addEditor('homeworkDetails', $guid)->setRows(15)->showMedia()->required(); + + $form->toggleVisibilityByClass('homeworkSubmission')->onClick('homeworkSubmission')->when('Y'); + $row = $form->addRow()->addClass('homework'); + $row->addLabel('homeworkSubmission', __('Online Submission?')); + $row->addYesNo('homeworkSubmission')->required()->checked('N'); + + $row = $form->addRow()->setClass('homeworkSubmission'); + $row->addLabel('homeworkSubmissionDateOpen', __('Submission Open Date')); + $row->addDate('homeworkSubmissionDateOpen')->required(); + + $row = $form->addRow()->setClass('homeworkSubmission'); + $row->addLabel('homeworkSubmissionDrafts', __('Drafts')); + $row->addSelect('homeworkSubmissionDrafts')->fromArray(['' => __('None'), '1' => __('1'), '2' => __('2'), '3' => __('3')]); + + $row = $form->addRow()->setClass('homeworkSubmission'); + $row->addLabel('homeworkSubmissionType', __('Submission Type')); + $row->addSelect('homeworkSubmissionType')->fromArray(['Link' => __('Link'), 'File' => __('File'), 'Link/File' => __('Link/File')])->required(); + + $row = $form->addRow()->setClass('homeworkSubmission'); + $row->addLabel('homeworkSubmissionRequired', __('Submission Required')); + $row->addSelect('homeworkSubmissionRequired')->fromArray(['Optional' => __('Optional'), 'Required' => __('Required')])->required(); + + if (isActionAccessible($guid, $connection2, '/modules/Crowd Assessment/crowdAssess.php')) { + $form->toggleVisibilityByClass('homeworkCrowdAssess')->onClick('homeworkCrowdAssess')->when('Y'); + $row = $form->addRow()->addClass('homeworkSubmission'); + $row->addLabel('homeworkCrowdAssess', __('Crowd Assessment?')); + $row->addYesNo('homeworkCrowdAssess')->required()->checked('N'); + + $row = $form->addRow()->addClass('homeworkCrowdAssess'); + $row->addLabel('homeworkCrowdAssessControl', __('Access Controls?'))->description(__('Decide who can see this {homeworkName}.', ['homeworkName' => __($homeworkNameSingular)])); + $column = $row->addColumn()->setClass('flex-col items-end'); + $column->addCheckbox('homeworkCrowdAssessClassTeacher')->checked(true)->description(__('Class Teacher'))->disabled(); + $column->addCheckbox('homeworkCrowdAssessClassSubmitter')->checked(true)->description(__('Submitter'))->disabled(); + $column->addCheckbox('homeworkCrowdAssessClassmatesRead')->description(__('Classmates')); + $column->addCheckbox('homeworkCrowdAssessOtherStudentsRead')->description(__('Other Students')); + $column->addCheckbox('homeworkCrowdAssessOtherTeachersRead')->description(__('Other Teachers')); + $column->addCheckbox('homeworkCrowdAssessSubmitterParentsRead')->description(__('Submitter\'s Parents')); + $column->addCheckbox('homeworkCrowdAssessClassmatesParentsRead')->description(__('Classmates\'s Parents')); + $column->addCheckbox('homeworkCrowdAssessOtherParentsRead')->description(__('Other Parents')); + } + + $sharingDefaultStudents = $settingGateway->getSettingByScope('Planner', 'sharingDefaultStudents'); + $sharingDefaultParents = $settingGateway->getSettingByScope('Planner', 'sharingDefaultParents'); + $form->addHiddenValue('viewableStudents', $sharingDefaultStudents); + $form->addHiddenValue('viewableParents', $sharingDefaultParents); + + $form->addRow()->addHeading('Advanced Options', __('Advanced Options')); + + $form->toggleVisibilityByClass('advanced')->onCheckbox('advanced')->when('Y'); + $row = $form->addRow(); + $row->addCheckbox('advanced')->setValue('Y')->description(__('Show Advanced Options')); + + $form->addRow()->addClass('advanced')->addHeading('Access', __('Access')); + + $row = $form->addRow()->addClass('advanced'); + $row->addLabel('viewableStudents', __('Viewable by Students')); + $row->addYesNo('viewableStudents')->required()->selected($sharingDefaultStudents); + + $row = $form->addRow()->addClass('advanced'); + $row->addLabel('viewableParents', __('Viewable by Parents')); + $row->addYesNo('viewableParents')->required()->selected($sharingDefaultParents); + + $row = $form->addRow()->addClass('advanced'); + $row->addLabel('videoLink', __('Online Lesson'))->description(__('Displays a video link for online lessons')); + $row->addURL('videoLink'); + + self::applyLessonPlannerAddHooks($container, $form); + + $container->get(CustomFieldHandler::class)->addCustomFieldsToForm($form, 'Lesson Plan', [], ''); + + return $form; + } + + /** + * Let modules add inputs to the add-lesson form (same $form object the UI uses). + * + * @param Container $container + * @param Form $form + */ + public static function applyLessonPlannerAddHooks(Container $container, Form $form) + { + global $guid, $connection2, $session, $pdo, $page; + + $session = $container->get(Session::class); + $hookGateway = $container->get(HookGateway::class); + $hooks = $hookGateway->selectHooksByType('Lesson Planner Add')->fetchGroupedUnique(); + + foreach ($hooks as $hook) { + $options = @unserialize($hook['options']); + if (empty($options) || !is_array($options)) { + continue; + } + + $hookPermission = $hookGateway->getHookPermission( + $hook['gibbonHookID'], + $session->get('gibbonRoleIDCurrent'), + $options['sourceModuleName'] ?? '', + $options['sourceModuleAction'] ?? '' + ); + + if (empty($hookPermission)) { + continue; + } + + $include = $session->get('absolutePath').'/modules/'.$options['sourceModuleName'].'/'.$options['sourceModuleInclude']; + if (is_file($include)) { + include $include; + } + } + } +} diff --git a/tests/unit/Data/ImportTypeTest.php b/tests/unit/Data/ImportTypeTest.php new file mode 100644 index 0000000000..018a97a782 --- /dev/null +++ b/tests/unit/Data/ImportTypeTest.php @@ -0,0 +1,73 @@ +. +*/ + +namespace Gibbon\Data; + +use PHPUnit\Framework\TestCase; +use ReflectionProperty; + +/** + * @covers ImportType + */ +class ImportTypeTest extends TestCase +{ + private function createImportType(array $field): ImportType + { + $importType = new ImportType([ + 'details' => [ + 'type' => 'testImport', + 'name' => 'Test Import', + 'table' => 'gibbonTest', + ], + 'table' => [ + 'custom0017' => $field, + ], + ], new PasswordPolicy(false, false, false, 0), null, false); + + $validated = new ReflectionProperty(ImportType::class, 'validated'); + $validated->setAccessible(true); + $validated->setValue($importType, true); + + return $importType; + } + + public function testSingleCheckboxUsesYesNoLikeOtherImports() + { + $importType = $this->createImportType([ + 'name' => 'Single Checkbox', + 'kind' => 'yesno', + 'type' => 'enum', + 'elements' => ['Y', 'N'], + 'args' => ['filter' => 'yesno', 'required' => false, 'checkboxOnValue' => 'Yes'], + ]); + + $this->assertSame('N', $importType->filterFieldValue('custom0017', '')); + $this->assertSame('Y', $importType->filterFieldValue('custom0017', 'Yes')); + $this->assertSame('Y', $importType->filterFieldValue('custom0017', 'Y')); + $this->assertSame('N', $importType->filterFieldValue('custom0017', 'No')); + + $this->assertSame('N', $importType->validateFieldValue('custom0017', 'N')); + $this->assertSame('Y', $importType->validateFieldValue('custom0017', 'Y')); + + $this->assertSame('', $importType->storedFieldValue('custom0017', 'N')); + $this->assertSame('Yes', $importType->storedFieldValue('custom0017', 'Y')); + } +}