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'));
+ }
+}