diff --git a/Modules/IndividualAssessment/classes/Members/class.ilIndividualAssessmentMembersStorageDB.php b/Modules/IndividualAssessment/classes/Members/class.ilIndividualAssessmentMembersStorageDB.php index f668fcf96255..4298415b57ed 100644 --- a/Modules/IndividualAssessment/classes/Members/class.ilIndividualAssessmentMembersStorageDB.php +++ b/Modules/IndividualAssessment/classes/Members/class.ilIndividualAssessmentMembersStorageDB.php @@ -31,7 +31,8 @@ class ilIndividualAssessmentMembersStorageDB implements ilIndividualAssessmentMe public function __construct( protected ilDBInterface $db, protected IRSS $irss, - protected ilIndividualAssessmentGradingStakeholder $stakeholder + protected ilIndividualAssessmentGradingStakeholder $stakeholder, + protected SpecifiedFormStorage $specified_form_storage ) { } @@ -71,7 +72,7 @@ public function loadMembersAsSingleObjects( } $res = $this->db->query($sql); while ($rec = $this->db->fetchAssoc($res)) { - $usr = new ilObjUser((int)$rec["usr_id"]); + $usr = new ilObjUser((int) $rec["usr_id"]); $members[] = $this->createAssessmentMember($obj, $usr, $rec); } return $members; @@ -114,10 +115,14 @@ protected function createAssessmentMember( if (!is_null($examiner_id)) { $examiner_id = (int) $examiner_id; } + + $custom_fields = $this->specified_form_storage->getSpecifiedFormFields($obj->getId(), $usr->getId()); + return new ilIndividualAssessmentMember( $obj, $usr, - $this->createGrading($record, $usr->getFullname()), + $this->createGrading($record, $usr->getFullname()) + ->withCustomFields($custom_fields), (int) $record[ilIndividualAssessmentMembers::FIELD_NOTIFICATION_TS], $examiner_id, $changer_id, @@ -133,6 +138,7 @@ protected function createGrading(array $record, string $user_fullname): ilIndivi $event_time = new DateTimeImmutable(); $event_time = $event_time->setTimestamp((int) $event_time_db); } + return new ilIndividualAssessmentUserGrading( $user_fullname, (string) $record[ilIndividualAssessmentMembers::FIELD_RECORD], @@ -179,6 +185,7 @@ public function updateMember(ilIndividualAssessmentMember $member): void ]; $this->db->update(self::MEMBERS_TABLE, $values, $where); + $this->specified_form_storage->storeSpecifiedUserValues(...$member->getGrading()->getCustomFields()); } protected function getActualDateTime(): string @@ -191,10 +198,12 @@ protected function getActualDateTime(): string */ public function deleteMembers(ilObjIndividualAssessment $obj): void { - foreach($this->loadMembers($obj) as $member) { - if($identifier = $member[ilIndividualAssessmentMembers::FIELD_FILE_NAME]) { + foreach ($this->loadMembers($obj) as $member) { + if ($identifier = $member[ilIndividualAssessmentMembers::FIELD_FILE_NAME]) { $resource_id = $this->irss->manage()->find($identifier); - $this->irss->manage()->remove($resource_id, $this->stakeholder); + if ($resource_id) { + $this->irss->manage()->remove($resource_id, $this->stakeholder); + } } } @@ -202,6 +211,17 @@ public function deleteMembers(ilObjIndividualAssessment $obj): void $this->db->manipulate($sql); } + public function deleteCustomFieldsForObj(ilObjIndividualAssessment $obj): void + { + $this->specified_form_storage->deleteAllUserValuesAndFields( + $this->irss, + $this->stakeholder, + $obj->getId() + ); + } + + + protected function loadMemberQuery(): string { return "SELECT " @@ -352,8 +372,7 @@ public function insertMembersRecord(ilObjIndividualAssessment $iass, array $reco */ public function removeMembersRecord(ilObjIndividualAssessment $iass, array $record): void { - - if(array_key_exists(ilIndividualAssessmentMembers::FIELD_FILE_NAME, $record) + if (array_key_exists(ilIndividualAssessmentMembers::FIELD_FILE_NAME, $record) && $identifier = $record[ilIndividualAssessmentMembers::FIELD_FILE_NAME]) { $resource_id = $this->irss->manage()->find($identifier); $this->irss->manage()->remove($resource_id, $this->stakeholder); @@ -366,6 +385,12 @@ public function removeMembersRecord(ilObjIndividualAssessment $iass, array $reco ; $this->db->manipulate($sql); + $this->specified_form_storage->deleteSpecifiedUserValues( + $this->irss, + $this->stakeholder, + $iass->getId(), + $record[ilIndividualAssessmentMembers::FIELD_USR_ID] + ); } /** diff --git a/Modules/IndividualAssessment/classes/class.ilIndividualAssessmentDIC.php b/Modules/IndividualAssessment/classes/class.ilIndividualAssessmentDIC.php index 54f05a00d9c8..6f896cb531f4 100644 --- a/Modules/IndividualAssessment/classes/class.ilIndividualAssessmentDIC.php +++ b/Modules/IndividualAssessment/classes/class.ilIndividualAssessmentDIC.php @@ -102,10 +102,19 @@ public function getObjectDIC( $dic->http()->wrapper()->query(), $c['helper.dateformat'], $dic['resource_storage'], - $stakeholder = $c['irss.stakeholder'] + $stakeholder = $c['irss.stakeholder'], + $c['iafp.fieldbuilder'] ); }; + $container['iafp.fieldbuilder'] = static fn(): ILIAS\IndividualAssessmentFormPool\FieldBuilder => + new ILIAS\IndividualAssessmentFormPool\FieldBuilder( + $dic['ui.factory']->input()->field(), + $dic['refinery'], + $dic['lng'], + new \ilUIDemoFileUploadHandlerGUI(), + new \ilUIMarkdownPreviewGUI() + ); $container['ilIndividualAssessmentCommonSettingsGUI'] = function ($c) use ($object, $dic) { return new ilIndividualAssessmentCommonSettingsGUI( $object, @@ -126,8 +135,13 @@ public function getObjectDIC( new ilIndividualAssessmentMembersStorageDB( $dic['ilDB'], $dic['resource_storage'], - $stakeholder = $c['irss.stakeholder'] + $stakeholder = $c['irss.stakeholder'], + $c['iass.member.custom_storage'], ); + + $container['iass.member.custom_storage'] = static fn($c): SpecifiedFormStorage => + new SpecifiedFormStorageDB($dic['ilDB']); + $container['iass.accesshandler'] = static fn($c): ilIndividualAssessmentAccessHandler => new ilIndividualAssessmentAccessHandler( $object, diff --git a/Modules/IndividualAssessment/classes/class.ilIndividualAssessmentMemberGUI.php b/Modules/IndividualAssessment/classes/class.ilIndividualAssessmentMemberGUI.php index b43cfd895d94..d0d1c7d6202f 100644 --- a/Modules/IndividualAssessment/classes/class.ilIndividualAssessmentMemberGUI.php +++ b/Modules/IndividualAssessment/classes/class.ilIndividualAssessmentMemberGUI.php @@ -33,6 +33,7 @@ use ILIAS\Data; use ILIAS\Refinery; use ILIAS\ResourceStorage\Services as IRSS; +use ILIAS\IndividualAssessmentFormPool\FieldBuilder; class ilIndividualAssessmentMemberGUI extends AbstractCtrlAwareUploadHandler { @@ -69,6 +70,7 @@ public function __construct( protected ilIndividualAssessmentDateFormatter $date_formatter, protected IRSS $irss, protected ilIndividualAssessmentGradingStakeholder $stakeholder, + protected FieldBuilder $field_builder, ) { parent::__construct(); } @@ -177,7 +179,7 @@ protected function downloadFile(): void { $identifier = $this->getMember()->getGrading()->getFile(); $resource_id = $this->irss->manage()->find($identifier); - if($resource_id) { + if ($resource_id) { $this->irss->consume()->download($resource_id)->run(); } } @@ -220,6 +222,7 @@ protected function buildForm( $this->refinery_factory, $this, $this->user->getDateFormat(), + $this->field_builder, $this->getPossibleLPStates(), $may_be_edited, $this->getObject()->getSettings()->isEventTimePlaceRequired(), @@ -353,7 +356,7 @@ protected function getUploadResult(): HandlerResult protected function getRemoveResult(string $identifier): HandlerResult { $resource_id = $this->irss->manage()->find($identifier); - if($resource_id) { + if ($resource_id) { $this->irss->manage()->remove($resource_id, $this->stakeholder); $status = HandlerResult::STATUS_OK; $message = $this->lng->txt('iass_file_deleted'); @@ -368,7 +371,7 @@ protected function getRemoveResult(string $identifier): HandlerResult public function getInfoResult(string $identifier): ?FileInfoResult { $resource_id = $this->irss->manage()->find($identifier); - if(! $resource_id) { + if (! $resource_id) { return null; } $resource = $this->irss->manage()->getResource($resource_id); @@ -388,7 +391,7 @@ public function getInfoForExistingFiles(array $file_ids): array $file_ids = array_filter($file_ids, fn($id) => $id !== ""); return array_map(function ($id) { $resource_id = $this->irss->manage()->find($identifier); - if(! $resource_id) { + if (! $resource_id) { return null; } $resource = $this->irss->manage()->getResource($resource_id); diff --git a/Modules/IndividualAssessment/classes/class.ilObjIndividualAssessment.php b/Modules/IndividualAssessment/classes/class.ilObjIndividualAssessment.php index 621898c5cd03..0e945aa42c2f 100755 --- a/Modules/IndividualAssessment/classes/class.ilObjIndividualAssessment.php +++ b/Modules/IndividualAssessment/classes/class.ilObjIndividualAssessment.php @@ -169,6 +169,7 @@ public function delete(): bool $this->deleteMetaData(); $this->settings_storage->deleteSettings($this); $this->members_storage->deleteMembers($this); + $this->members_storage->deleteCustomFieldsForObj($this); return parent::delete(); } @@ -245,6 +246,7 @@ public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = $new_obj->settings_storage->updateSettings($new_settings); $new_obj->settings_storage->updateInfoSettings($new_info_settings); + (new IAFPCollector())->cloneFields($this->getId(), $new_obj->getId()); return $new_obj; } diff --git a/Modules/IndividualAssessment/classes/class.ilObjIndividualAssessmentGUI.php b/Modules/IndividualAssessment/classes/class.ilObjIndividualAssessmentGUI.php index ff3030a6012e..868ecd4a3aef 100644 --- a/Modules/IndividualAssessment/classes/class.ilObjIndividualAssessmentGUI.php +++ b/Modules/IndividualAssessment/classes/class.ilObjIndividualAssessmentGUI.php @@ -157,7 +157,7 @@ public function executeCommand(): void $cmd = 'members'; } } - if($cmd === 'edit' && $this->object->accessHandler()->simulateMember()) { + if ($cmd === 'edit' && $this->object->accessHandler()->simulateMember()) { $cmd = 'view'; } @@ -237,7 +237,7 @@ protected function downloadFileObject(): void ) { $identifier = $member->getGrading()->getFile(); $resource_id = $this->irss->manage()->find($identifier); - if($resource_id) { + if ($resource_id) { $this->irss->consume()->download($resource_id)->run(); } } @@ -421,8 +421,20 @@ protected function getEntryForStatus(int $status): string } } + protected function addDidacticTemplateOptions(array &$a_options): void + { + $collector = \ilObjIndividualAssessmentFormPool::getRepository(); + $a_options = $collector->getFormsSelection(); + } + protected function afterSave(ilObject $new_object): void { + if ($form_id = $this->getDidacticTemplateVar("iass")) { + $collector = \ilObjIndividualAssessmentFormPool::getRepository(); + $collector->copyFieldsToIASS($form_id, $new_object->getId()); + } + + $this->tpl->setOnScreenMessage("success", $this->txt("iass_added"), true); $this->ctrl->setParameter($this, "ref_id", $new_object->getRefId()); $this->ctrl->redirectToUrl($this->ctrl->getLinkTargetByClass( @@ -449,4 +461,5 @@ protected function txt(string $code): string { return $this->lng->txt($code); } + } diff --git a/Modules/IndividualAssessment/classes/ilIndividualAssessmentUserGrading.php b/Modules/IndividualAssessment/classes/ilIndividualAssessmentUserGrading.php index 671341e3b117..257cb1557eb2 100644 --- a/Modules/IndividualAssessment/classes/ilIndividualAssessmentUserGrading.php +++ b/Modules/IndividualAssessment/classes/ilIndividualAssessmentUserGrading.php @@ -22,6 +22,7 @@ use ILIAS\Refinery\Factory as Refinery; use ILIAS\Data\Factory as DataFactory; use ILIAS\FileUpload\Handler\AbstractCtrlAwareUploadHandler; +use ILIAS\IndividualAssessmentFormPool\FieldBuilder; class ilIndividualAssessmentUserGrading { @@ -35,6 +36,7 @@ class ilIndividualAssessmentUserGrading protected ?DateTimeImmutable $event_time; protected bool $notify; protected bool $finalized; + protected array $custom_fields = []; public function __construct( string $name, @@ -129,6 +131,18 @@ public function withFile(?string $file): ilIndividualAssessmentUserGrading return $clone; } + public function getCustomFields(): array + { + return $this->custom_fields; + } + + public function withCustomFields(array $custom_fields): self + { + $clone = clone $this; + $clone->custom_fields = $custom_fields; + return $clone; + } + public function toFormInput( Field\Factory $input, DataFactory $data_factory, @@ -136,12 +150,14 @@ public function toFormInput( Refinery $refinery, AbstractCtrlAwareUploadHandler $file_handler, \ILIAS\Data\DateFormat\DateFormat $date_format, + FieldBuilder $field_builder, array $grading_options, bool $may_be_edited = true, bool $place_required = false, bool $file_required = false, bool $amend = false ): \ILIAS\UI\Component\Input\Container\Form\FormInput { + $name = $input ->text($lng->txt('name'), '') ->withDisabled(true) @@ -206,15 +222,23 @@ public function toFormInput( ->withDisabled(!$may_be_edited) ; + + $custom = []; + $custom_fields = $this->custom_fields; + foreach ($custom_fields as $cf) { + $custom[$cf->getFieldId()] = $cf->toFormInput($input, $refinery, $file_handler, $field_builder); + } + $fields = [ 'name' => $name, 'record' => $record, 'internal_note' => $internal_note, 'file' => $file, 'file_visible' => $file_visible, - 'learning_progress' => $learning_progress, 'place' => $place, 'event_time' => $event_time, + 'custom' => $input->group($custom), + 'learning_progress' => $learning_progress, 'notify' => $notify ]; @@ -232,7 +256,7 @@ public function toFormInput( $fields, $lng->txt('iass_edit_record') )->withAdditionalTransformation( - $refinery->custom()->transformation(function ($values) use ($amend) { + $refinery->custom()->transformation(function ($values) use ($amend, $custom_fields) { $finalized = $this->isFinalized(); if (!$amend) { $finalized = $values['finalized']; @@ -246,7 +270,12 @@ public function toFormInput( $file = $values['file'][0]; } - return new ilIndividualAssessmentUserGrading( + $updated_custom = []; + foreach ($custom_fields as $cf) { + $updated_custom[] = $cf->withValue($values['custom'][$cf->getFieldId()]); + } + + return (new ilIndividualAssessmentUserGrading( $values['name'], $values['record'], $values['internal_note'], @@ -257,7 +286,8 @@ public function toFormInput( $values['event_time'], $values['notify'], $finalized - ); + )) + ->withCustomFields($updated_custom); }) ); } diff --git a/Modules/IndividualAssessment/test/Members/ilIndividualAssessmentMembersStorageDBTest.php b/Modules/IndividualAssessment/test/Members/ilIndividualAssessmentMembersStorageDBTest.php index 607af7cd33eb..8c69ced614a6 100644 --- a/Modules/IndividualAssessment/test/Members/ilIndividualAssessmentMembersStorageDBTest.php +++ b/Modules/IndividualAssessment/test/Members/ilIndividualAssessmentMembersStorageDBTest.php @@ -61,7 +61,8 @@ public function getWrapperObj(ilDBInterface $db): ilIndividualAssessmentMembersS return new ilIndividualAssessmentMembersStorageDBWrapper( $db, $irss, - $stakeholder + $stakeholder, + $this->createMock(SpecifiedFormStorage::class) ); } @@ -72,7 +73,8 @@ public function testCreateObject(): void $obj = new ilIndividualAssessmentMembersStorageDB( $db, $irss, - new ilIndividualAssessmentGradingStakeholder() + new ilIndividualAssessmentGradingStakeholder(), + $this->createMock(SpecifiedFormStorage::class) ); $this->assertInstanceOf(ilIndividualAssessmentMembersStorageDB::class, $obj); } @@ -129,7 +131,8 @@ public function test_loadMembers(): void $obj = new ilIndividualAssessmentMembersStorageDB( $db, $this->createMock(IRSS::class), - new ilIndividualAssessmentGradingStakeholder() + new ilIndividualAssessmentGradingStakeholder(), + $this->createMock(SpecifiedFormStorage::class) ); $result = $obj->loadMembers($iass); @@ -193,7 +196,12 @@ public function test_loadMembersAsSingleObjects(): void $irss = $this->createMock(IRSS::class); $stakeholder = new ilIndividualAssessmentGradingStakeholder(); - $obj = new ilIndividualAssessmentMembersStorageDB($db, $irss, $stakeholder); + $obj = new ilIndividualAssessmentMembersStorageDB( + $db, + $irss, + $stakeholder, + $this->createMock(SpecifiedFormStorage::class) + ); $result = $obj->loadMembersAsSingleObjects($iass); $this->assertIsArray($result); @@ -265,7 +273,12 @@ public function test_loadMember_exception(): void $irss = $this->createMock(IRSS::class); $stakeholder = new ilIndividualAssessmentGradingStakeholder(); - $obj = new ilIndividualAssessmentMembersStorageDB($db, $irss, $stakeholder); + $obj = new ilIndividualAssessmentMembersStorageDB( + $db, + $irss, + $stakeholder, + $this->createMock(SpecifiedFormStorage::class) + ); $this->expectException(ilIndividualAssessmentException::class); $this->expectExceptionMessage("invalid usr-obj combination"); @@ -337,7 +350,12 @@ public function test_loadMember(): void $irss = $this->createMock(IRSS::class); $stakeholder = new ilIndividualAssessmentGradingStakeholder(); - $obj = new ilIndividualAssessmentMembersStorageDB($db, $irss, $stakeholder); + $obj = new ilIndividualAssessmentMembersStorageDB( + $db, + $irss, + $stakeholder, + $this->createMock(SpecifiedFormStorage::class) + ); $this->expectException(ilIndividualAssessmentException::class); $this->expectExceptionMessage("invalid usr-obj combination"); @@ -529,7 +547,12 @@ public function test_deleteMembers(): void $irss = $this->createMock(IRSS::class); $stakeholder = new ilIndividualAssessmentGradingStakeholder(); - $obj = new ilIndividualAssessmentMembersStorageDB($db, $irss, $stakeholder); + $obj = new ilIndividualAssessmentMembersStorageDB( + $db, + $irss, + $stakeholder, + $this->createMock(SpecifiedFormStorage::class) + ); $obj->deleteMembers($iass); } @@ -595,7 +618,7 @@ public function test_removeMembersRecord(): void { $iass = $this->createMock(ilObjIndividualAssessment::class); $iass - ->expects($this->once()) + ->expects($this->exactly(2)) ->method("getId") ->willReturn(11) ; @@ -623,7 +646,12 @@ public function test_removeMembersRecord(): void $irss = $this->createMock(IRSS::class); $stakeholder = new ilIndividualAssessmentGradingStakeholder(); - $obj = new ilIndividualAssessmentMembersStorageDB($db, $irss, $stakeholder); + $obj = new ilIndividualAssessmentMembersStorageDB( + $db, + $irss, + $stakeholder, + $this->createMock(SpecifiedFormStorage::class) + ); $obj->removeMembersRecord($iass, $record); } diff --git a/Modules/IndividualAssessment/test/ilIndividualAssessmentUserGradingTest.php b/Modules/IndividualAssessment/test/ilIndividualAssessmentUserGradingTest.php index a091075a9ae0..fb28a095cc04 100644 --- a/Modules/IndividualAssessment/test/ilIndividualAssessmentUserGradingTest.php +++ b/Modules/IndividualAssessment/test/ilIndividualAssessmentUserGradingTest.php @@ -1,7 +1,5 @@ getFieldBuilder(); + $input = $grading->toFormInput( $f, $df, @@ -165,6 +172,7 @@ public function testToFormInput(): void $refinery, $file_handler, $df->dateFormat()->standard(), + $field_builder, [ ilIndividualAssessmentMembers::LP_IN_PROGRESS, ilIndividualAssessmentMembers::LP_FAILED, diff --git a/Modules/IndividualAssessmentFormPool/classes/Forms/Field.php b/Modules/IndividualAssessmentFormPool/classes/Forms/Field.php new file mode 100644 index 000000000000..2a53f433059c --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Forms/Field.php @@ -0,0 +1,150 @@ +field_id; + } + + public function withFieldId(int $field_id): self + { + $clone = clone $this; + $clone->field_id = $field_id; + return $clone; + } + + public function asNew(): self + { + $clone = clone $this; + $clone->field_id = -1; + return $clone; + } + + public function getObjId(): int + { + return $this->iafp_obj_id; + } + + public function withObjId(int $iafp_obj_id): self + { + $clone = clone $this; + $clone->iafp_obj_id = $iafp_obj_id; + return $clone; + } + + public function getName(): string + { + return $this->name; + } + + + + public function hasNotes(): bool + { + return $this->with_notes; + } + + public function isAvailableForExaminers(): bool + { + return $this->available_for_examiners; + } + + public function getConfig(): FieldConfig + { + return $this->config; + } + + public function toFormInput( + FieldFactory $factory, + \ilLanguage $lng, + Refinery $refinery, + int $iafp_obj_id, + FieldBuilder $field_builder + ): FormInput { + + $config = $this->getConfig(); + + $inputs = []; + $inputs['id'] = $factory->hidden() + ->withAdditionalTransformation($refinery->kindlyTo()->int()) + ->withValue($this->getFieldId()); + + $inputs['type'] = $factory->select( + $lng->txt('field_type'), + [$config->getType()->value => $config->getType()->name], + $lng->txt('field_type_byline') + ) + ->withValue($config->getType()->value) + ->withRequired(true); + + $inputs['name'] = $factory->text($lng->txt('field_name'), $lng->txt('field_name_byline')) + ->withRequired(true) + ->withValue($this->getName()); + + $inputs['type_config'] = $config->toFormInput( + $factory, + $lng, + $refinery, + $field_builder, + ($this->getFieldId() !== -1) + ); + + $inputs['with_notes'] = $factory->checkbox($lng->txt('field_with_notes'), $lng->txt('field_with_notes_byline')) + ->withValue($this->hasNotes()); + $inputs['available_for_examiners'] = $factory->checkbox($lng->txt('field_available_for_examiners'), $lng->txt('field_available_for_examiners_byline')) + ->withValue($this->isAvailableForExaminers()); + + return $factory->section( + $inputs, + $this->getFieldId() === -1 ? $lng->txt('field_section_create') : $lng->txt('field_section_edit'), + $lng->txt('field_section_byline') + ) + ->withAdditionalTransformation( + $refinery->custom()->transformation( + fn($v) => new self( + $v['type_config'], + $v['id'], + $iafp_obj_id, + $v['name'], + $v['with_notes'], + $v['available_for_examiners'], + ) + ) + ); + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/Forms/FieldBuilder.php b/Modules/IndividualAssessmentFormPool/classes/Forms/FieldBuilder.php new file mode 100644 index 000000000000..02eec7c3d6f4 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Forms/FieldBuilder.php @@ -0,0 +1,101 @@ +getLabel(); + $description = $config->getDescription(); + + $value = $value ?? $config->getDefaultValue(); + + $factory = $this->factory; + switch ($config->getType()) { + case FieldType::MARKDOWN: + $md_renderer = $md_renderer ?? $this->default_mdrenderer; + return $factory->markdown($md_renderer, $label, $description) + ->withValue((string) $value) + ->withMaxLimit(512); + + case FieldType::FILE: + $value = ($value === null || $value === '') ? + [] : [$value]; + + $upload_handler = $upload_handler ?? $this->default_uploadhandler; + return $factory->file($upload_handler, $label, $description) + ->withValue($value); + + case FieldType::DATETIME: + $value = ($value === null || $value === '') ? + null : \DateTimeImmutable::createFromFormat('U', $value); + + return $factory->datetime($label, $description) + ->withTimezone('UTC') + ->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn($v) => (string) $v->format('U') + ) + ) + ->withValue($value); + + case FieldType::SINGLESELECT: + $options = $config->getOptions() ?? []; + $options = array_combine($options, $options); + $value = in_array($value, $options) ? $value : null; //better: constraint + return $factory->select($label, $options, $description) + ->withValue($value); + + case FieldType::TAG: + $options = $config->getOptions() ?? []; + $value = ($value === null || $value === '') ? + null : explode(\SpecifiedFormStorageDB::VALUE_DELIMITER, $value); + return $factory->tag($label, $options, $description) + ->withUserCreatedTagsAllowed(false) + ->withValue($value); + + case FieldType::RATING: + $options = [1,2,3,4,5]; + return $factory->select($label, $options, $description) + ;//->withValue((string) $config->getDefaultValue()); + } + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/Forms/FieldConfig.php b/Modules/IndividualAssessmentFormPool/classes/Forms/FieldConfig.php new file mode 100644 index 000000000000..0aa8844a1177 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Forms/FieldConfig.php @@ -0,0 +1,141 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getOptions(): mixed + { + return $this->options; + } + + public function getDefaultValue(): mixed + { + return $this->default_value; + } + + public function toFormInput( + FieldFactory $factory, + \ilLanguage $lng, + Refinery $refinery, + FieldBuilder $field_builder, + bool $fixed + ): FormInput { + $type_options = $fixed ? + [$this->type->value => $this->type->name] : FieldType::toArray(); + + $inputs = []; + $inputs = array_filter([ + 'label' => $factory->text($lng->txt('field_label'), $lng->txt('field_label_byline')) + ->withValue($this->getLabel()), + 'description' => $factory->textarea($lng->txt('field_description'), $lng->txt('field_description_byline')) + ->withValue($this->getDescription()), + 'opts' => $this->getFormInputOptions($factory, $lng, $refinery), + 'default' => $this->getFormInputDefault($field_builder, $lng) + ]); + + return $factory->group($inputs)->withAdditionalTransformation( + $refinery->custom()->transformation( + function ($v) { + $opts = array_key_exists('opts', $v) ? $v['opts'] : null; + $default = array_key_exists('default', $v) ? $v['default'] : null; + return new self( + $this->getType(), + $v['label'], + $v['description'], + $default, + $opts, + ); + } + ) + ); + + } + + private function getFormInputOptions( + FieldFactory $factory, + \ilLanguage $lng, + Refinery $refinery, + ): ?FormInput { + switch ($this->type) { + case FieldType::SINGLESELECT: + case FieldType::TAG: + return $factory->group([ //group is needed due to exisiting transforms on tag-input + $factory->tag($lng->txt('field_options'), [], $lng->txt('field_options_byline')) + ->withUserCreatedTagsAllowed(true) + ->withRequired(true) + ->withValue($this->getOptions()) + ]) + ->withAdditionalTransformation( + $refinery->custom()->transformation( + fn($v) => array_shift($v) + ) + ); + break; + default: + return null; + } + } + + private function getFormInputDefault( + FieldBuilder $field_builder, + \ilLanguage $lng + ): ?FormInput { + + switch ($this->type) { + case FieldType::FILE: + return null; + default: + return $field_builder->build($this) + ->withLabel($lng->txt('field_default_value')) + ->withByLine($lng->txt('field_default_value_byline')); + } + } + +} diff --git a/Modules/IndividualAssessmentFormPool/classes/Forms/FieldType.php b/Modules/IndividualAssessmentFormPool/classes/Forms/FieldType.php new file mode 100644 index 000000000000..da96abfef132 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Forms/FieldType.php @@ -0,0 +1,39 @@ +ui_factory->symbol()->icon()->custom('templates/default/images/standard/icon_checked.svg', '', 'small'); + $icon_false = $this->ui_factory->symbol()->icon()->custom('templates/default/images/standard/icon_unchecked.svg', '', 'small'); + return [ + 'type' => $this->ui_factory->table()->column()->text($this->lng->txt('type')), + 'name' => $this->ui_factory->table()->column()->text($this->lng->txt('name')), + 'label' => $this->ui_factory->table()->column()->text($this->lng->txt('label')), + 'description' => $this->ui_factory->table()->column()->text($this->lng->txt('description')) + ->withIsSortable(false), + 'default_value' => $this->ui_factory->table()->column()->text($this->lng->txt('default_value')), + 'with_notes' => $this->ui_factory->table()->column()->boolean($this->lng->txt('with_notes'), $icon_true, $icon_false), + 'available_for_examiners' => $this->ui_factory->table()->column()->boolean($this->lng->txt('available_for_examiners'), $icon_true, $icon_false), + ]; + } + + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + ?array $filter_data, + ?array $additional_parameters + ): \Generator { + $fields = $this->forms_repo->getFieldsForObjId($this->obj_id, $range, $order); + $used_in_forms = $this->forms_repo->getMappedFieldIds(); + foreach ($fields as $field) { + $row_id = (string) $field->getFieldId(); + $config = $field->getConfig(); + $record = [ + 'type' => $config->getType()->name, + 'name' => $field->getName(), + 'label' => $config->getLabel(), + 'description' => $config->getDescription(), + 'default_value' => (string) $config->getDefaultValue(), + 'with_notes' => $field->hasNotes(), + 'available_for_examiners' => $field->isAvailableForExaminers(), + ]; + yield $row_builder->buildDataRow($row_id, $record) + ->withDisabledAction( + 'delete', + in_array($field->getFieldId(), $used_in_forms) + ); + } + } + + public function getTotalRowCount( + ?array $filter_data, + ?array $additional_parameters + ): ?int { + return $this->forms_repo->getFieldsCountForObjId($this->obj_id); + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/Forms/Form.php b/Modules/IndividualAssessmentFormPool/classes/Forms/Form.php new file mode 100644 index 000000000000..94077e0f8fcd --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Forms/Form.php @@ -0,0 +1,157 @@ +form_id; + } + + public function asNew(): self + { + $clone = clone $this; + $clone->form_id = -1; + return $clone; + } + + public function getObjId(): int + { + return $this->iafp_obj_id; + } + + public function withObjId(int $iafp_obj_id): self + { + $clone = clone $this; + $clone->iafp_obj_id = $iafp_obj_id; + return $clone; + } + + public function getName(): string + { + return $this->name; + } + + public function withName(string $name): self + { + $clone = clone $this; + $clone->name = $name; + return $clone; + } + + public function getDescription(): string + { + return $this->description; + } + + public function withDescription(string $description): self + { + $clone = clone $this; + $clone->description = $description; + return $clone; + } + + public function getFields(): array + { + return $this->fields; + } + + public function withFields(Field ...$fields): self + { + $clone = clone $this; + $clone->fields = $fields; + return $clone; + } + + public function toFormInput( + FieldFactory $factory, + Renderer $renderer, + \ilLanguage $lng, + Refinery $refinery, + int $iafp_obj_id, + FormsStorageDB $forms_repo, + FieldBuilder $field_builder + ): FormInput { + + $options = []; + foreach ($this->getFields() as $field) { + $options[$field->getFieldId()] = $renderer->render( + $field_builder->build($field->getConfig())->withDisabled(true) + ); + } + + return $factory->section( + [ + 'id' => $factory->hidden() + ->withValue($this->getFormId()) + ->withAdditionalTransformation($refinery->kindlyTo()->int()), + 'name' => $factory->text($lng->txt('form_name'), $lng->txt('form_name_byline')) + ->withRequired(true) + ->withValue($this->getName()), + 'desc' => $factory->textarea($lng->txt('form_description'), $lng->txt('form_description_byline')) + ->withValue($this->getDescription()), + 'fields' => $factory->multiselect($lng->txt('fields'), $options, $lng->txt('fields_byline')) + ->withValue(array_keys($options)) + ->withAdditionalTransformation( + $refinery->custom()->transformation( + fn($v) => array_map( + fn($fid) => $forms_repo->getFieldById((int) $fid), + $v + ) + ) + ) + ], + $this->getFormId() === -1 ? $lng->txt('form_section_create') : $lng->txt('form_section_edit'), + $lng->txt('form_section_byline') + ) + ->withAdditionalTransformation( + $refinery->custom()->transformation( + fn($v) => new self( + $v['id'], + $iafp_obj_id, + $v['name'], + $v['desc'], + $v['fields'] ?? [] + ) + ) + ); + + } + +} diff --git a/Modules/IndividualAssessmentFormPool/classes/Forms/FormsDataRetrieval.php b/Modules/IndividualAssessmentFormPool/classes/Forms/FormsDataRetrieval.php new file mode 100644 index 000000000000..f5db95064d3f --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Forms/FormsDataRetrieval.php @@ -0,0 +1,80 @@ + $this->ui_factory->table()->column()->text($this->lng->txt('name')), + 'description' => $this->ui_factory->table()->column()->text($this->lng->txt('description')) + ->withIsSortable(false), + 'fields_count' => $this->ui_factory->table()->column()->number($this->lng->txt('fields_count')) + ->withIsSortable(false), + ]; + } + + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + ?array $filter_data, + ?array $additional_parameters + ): \Generator { + $forms = $this->forms_repo->getFormsForObjId($this->obj_id, $range, $order); + foreach ($forms as $form) { + $row_id = (string) $form->getFormId(); + $record = [ + 'name' => $form->getName(), + 'description' => $form->getDescription(), + 'fields_count' => count($form->getFields()), + ]; + yield $row_builder->buildDataRow($row_id, $record); + } + } + + public function getTotalRowCount( + ?array $filter_data, + ?array $additional_parameters + ): ?int { + return $this->forms_repo->getFormsCountForObjId($this->obj_id); + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/Forms/FormsStorage.php b/Modules/IndividualAssessmentFormPool/classes/Forms/FormsStorage.php new file mode 100644 index 000000000000..2bfd5305cdc0 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Forms/FormsStorage.php @@ -0,0 +1,61 @@ + FieldType::SINGLESELECT, + 'iafp_cfg_tag' => FieldType::TAG, + ]; + + private const CONFIG_TABLES_COLLECTOR = [ + FieldType::SINGLESELECT->name => ['iafp_cfg_singleselect', 'iass_cfg_singleselect'], + FieldType::TAG->name => ['iafp_cfg_tag', 'iass_cfg_tag'], + ]; + + public function __construct( + private readonly \ilDBInterface $db, + private readonly \ilAccess $access + ) { + } + + public function getFormsForObjId( + int $iafp_obj_id, + ?Range $range = null, + ?Order $order = null + ): \Generator { + + $query_order = ''; + $query_range = ''; + if ($order !== null) { + $query_order = $order->join('ORDER BY', fn(...$o) => implode(' ', $o)); + } + if ($range !== null) { + $query_range = sprintf('LIMIT %2$s OFFSET %1$s', ...$range->unpack()); + } + + $query = 'SELECT obj_id, form_id, name, description' . PHP_EOL + . 'FROM iafp_forms' . PHP_EOL + . 'WHERE obj_id = ' . $this->db->quote($iafp_obj_id, 'integer') . PHP_EOL + . $query_order . PHP_EOL + . $query_range; + + $res = $this->db->query($query); + while ($row = $this->db->fetchAssoc($res)) { + $form_id = $row['form_id']; + $fields = $this->getFieldsForFormId($form_id); + yield new Form( + $form_id, + $iafp_obj_id, + $row['name'], + $row['description'], + $fields + ); + } + } + + public function getFormById(int $form_id): ?Form + { + $query = 'SELECT obj_id, form_id, name, description' . PHP_EOL + . 'FROM iafp_forms' . PHP_EOL + . 'WHERE form_id = ' . $this->db->quote($form_id, 'integer'); + + $res = $this->db->query($query); + if ($this->db->numRows($res) === 0) { + return null; + } + $row = $this->db->fetchAssoc($res); + $form_id = $row['form_id']; + $fields = $this->getFieldsForFormId($form_id); + return new Form( + $form_id, + $row['obj_id'], + $row['name'], + $row['description'], + $fields + ); + } + + + private function getSQLPartsFieldConfig(): array + { + $opts = []; + $joins = []; + foreach (self::CONFIG_TABLES as $table => $type) { + $opts[] = 'CONCAT(' . $table . '.value)'; + $joins[] = 'LEFT JOIN ' . $table . ' ON iafp_fields.field_id = ' . $table . '.field_id AND type = ' . $type->value ; + } + return [$opts, $joins]; + } + + private function getFieldFromRow(array $row): Field + { + $field_id = $row['field_id']; + $type = FieldType::from($row['type']); + $options = ($row['options'] !== null) ? explode(',', $row['options']) : null; + $default_value = $row['default_value']; + //$config = $this->field_config_factory->buildFieldConfig($type, $options, $default_value); + + $config = new FieldConfig( + $type, + $row['label'], + $row['description'], + $row['default_value'], + $options, + ); + + return new Field( + $config, + $field_id, + $row['obj_id'], + $row['name'], + (bool) $row['with_notes'], + (bool) $row['available_for_examiners'], + ); + } + + public function createField(FieldType $type, int $iafp_obj_id): Field + { + $config = new FieldConfig($type); + $field = new Field( + $config, + -1, + $iafp_obj_id, + $name = '', + $with_notes = false, + $available_for_examiners = true + ); + return $this->storeField($field); + } + + public function getFieldsForFormId(int $form_id): array + { + list($opts, $joins) = $this->getSQLPartsFieldConfig(); + $query = 'SELECT iafp_fields.field_id, obj_id, type, name, label, description, default_value, with_notes, available_for_examiners' . PHP_EOL + . ', GROUP_CONCAT(COALESCE(' . implode(',', $opts) . ')) AS options' . PHP_EOL + . 'FROM iafp_fields' . PHP_EOL + . 'INNER JOIN iafp_fieldmap ON iafp_fields.field_id = iafp_fieldmap.field_id ' + . 'AND iafp_fieldmap.form_id = ' . $this->db->quote($form_id, 'integer') . PHP_EOL + . implode(' ', $joins) . PHP_EOL + . 'GROUP BY iafp_fields.field_id ' + . 'ORDER BY iafp_fieldmap.position ASC'; + + $ret = []; + $res = $this->db->query($query); + while ($row = $this->db->fetchAssoc($res)) { + $ret[] = $this->getFieldFromRow($row); + } + + return $ret; + } + + public function getFieldsForObjId( + int $iafp_obj_id, + ?Range $range = null, + ?Order $order = null + ): \Generator { + $query_order = ''; + $query_range = ''; + if ($order !== null) { + $query_order = $order->join('ORDER BY', fn(...$o) => implode(' ', $o)); + } + if ($range !== null) { + $query_range = sprintf('LIMIT %2$s OFFSET %1$s', ...$range->unpack()); + } + + list($opts, $joins) = $this->getSQLPartsFieldConfig(); + $query = 'SELECT iafp_fields.field_id, obj_id, type, name, label, description, default_value, with_notes, available_for_examiners' . PHP_EOL + . ', GROUP_CONCAT(COALESCE(' . implode(',', $opts) . ')) AS options' . PHP_EOL + . 'FROM iafp_fields' . PHP_EOL + . implode(' ', $joins) . PHP_EOL + . 'WHERE obj_id = ' . $this->db->quote($iafp_obj_id, 'integer') . PHP_EOL + . 'GROUP BY iafp_fields.field_id' . PHP_EOL + . $query_order . PHP_EOL + . $query_range; + + $res = $this->db->query($query); + while ($row = $this->db->fetchAssoc($res)) { + yield $this->getFieldFromRow($row); + } + } + + public function getFieldById(int $field_id): ?Field + { + list($opts, $joins) = $this->getSQLPartsFieldConfig(); + $query = 'SELECT iafp_fields.field_id, obj_id, type, name, label, description, default_value, with_notes, available_for_examiners' . PHP_EOL + . ', GROUP_CONCAT(COALESCE(' . implode(',', $opts) . ')) AS options' . PHP_EOL + . 'FROM iafp_fields' . PHP_EOL + . implode(' ', $joins) . PHP_EOL + . 'WHERE iafp_fields.field_id = ' . $this->db->quote($field_id, 'integer'); + + + $res = $this->db->query($query); + if ($this->db->numRows($res) === 0) { + return null; + } + return $this->getFieldFromRow($this->db->fetchAssoc($res)); + } + + + public function getFormsCountForObjId(int $iafp_obj_id): int + { + $query = 'SELECT count(*) as cnt' . PHP_EOL + . 'FROM iafp_forms' . PHP_EOL + . 'WHERE obj_id = ' . $this->db->quote($iafp_obj_id, 'integer'); + $res = $this->db->query($query); + return (int) $this->db->fetchAssoc($res)['cnt']; + } + + public function getAllFormIdsForObjId(int $iafp_obj_id): array + { + $query = 'SELECT form_id FROM iafp_forms' . PHP_EOL + . 'WHERE obj_id = ' . $this->db->quote($iafp_obj_id, 'integer'); + $res = $this->db->query($query); + return array_map( + fn($row) => $row['form_id'], + $this->db->fetchAll($this->db->query($query)) + ); + } + + + public function getNewForm(int $iafp_obj_id): Form + { + return new Form( + -1, + $iafp_obj_id, + $name = '', + $description = '', + $fields = [] + ); + } + + public function storeForm(Form $form): int + { + $id = $form->getFormId(); + if ($form->getFormId() === -1) { + $id = $this->db->nextId('iafp_forms'); + } + + $query = 'REPLACE INTO iafp_forms (obj_id, form_id, name, description) VALUES (' . PHP_EOL + . $this->db->quote($form->getObjId(), 'integer') . ',' + . $this->db->quote($id, 'integer') . ',' + . $this->db->quote($form->getName(), 'text') . ',' + . $this->db->quote($form->getDescription(), 'text') . PHP_EOL + . ')'; + + $this->db->manipulate($query); + + $query = 'DELETE FROM iafp_fieldmap WHERE ' . PHP_EOL + . 'form_id = ' . $this->db->quote($form->getFormId(), 'integer'); + $this->db->manipulate($query); + + $position = 0; + foreach ($form->getFields() as $field) { + $position += 10; + $query = 'INSERT INTO iafp_fieldmap (form_id, field_id, position) VALUES (' . PHP_EOL + . $this->db->quote($id, 'integer') . ',' + . $this->db->quote($field->getFieldId(), 'integer') . ',' + . $this->db->quote($position, 'integer') + . ')'; + + $this->db->manipulate($query); + } + + return $id; + } + + public function deleteFormsByIds(array $form_ids): void + { + $ids = $this->db->in('form_id', $form_ids, false, 'integer'); + $query = 'DELETE FROM iafp_forms WHERE ' . $ids; + $this->db->manipulate($query); + $query = 'DELETE FROM iafp_fieldmap WHERE ' . $ids; + $this->db->manipulate($query); + } + + + public function getFieldsCountForObjId(int $iafp_obj_id): int + { + $query = 'SELECT count(*) as cnt' . PHP_EOL + . 'FROM iafp_fields' . PHP_EOL + . 'WHERE obj_id = ' . $this->db->quote($iafp_obj_id, 'integer'); + $res = $this->db->query($query); + return (int) $this->db->fetchAssoc($res)['cnt']; + } + + public function storeField(Field $field): Field + { + if ($field->getFieldId() === -1) { + $field = $field->withFieldId($this->db->nextId('iafp_fields')); + } + $id = $field->getFieldId(); + $config = $field->getConfig(); + + $default_value = $config->getDefaultValue(); + if (is_array($default_value)) { + $default_value = implode(\SpecifiedFormStorageDB::VALUE_DELIMITER, $default_value); + } + + $query = 'REPLACE INTO iafp_fields (obj_id, field_id, type, name, label, description, default_value, with_notes, available_for_examiners) VALUES (' . PHP_EOL + . $this->db->quote($field->getObjId(), 'integer') . ',' + . $this->db->quote($id, 'integer') . ',' + . $this->db->quote($field->getConfig()->getType()->value, 'integer') . ',' + . $this->db->quote($field->getName(), 'text') . ',' + . $this->db->quote($config->getLabel(), 'text') . ',' + . $this->db->quote($config->getDescription(), 'text') . ',' + . $this->db->quote($default_value, 'text') . ',' + . $this->db->quote($field->hasNotes(), 'integer') . ',' + . $this->db->quote($field->isAvailableForExaminers(), 'integer') . PHP_EOL + . ')'; + $this->db->manipulate($query); + if ($config->getOptions() !== null) { + $this->storeFieldConfig($id, $config); + } + return $field; + } + + public function getAllFieldIdsForObjId(int $iafp_obj_id): array + { + $query = 'SELECT field_id FROM iafp_fields' . PHP_EOL + . 'WHERE obj_id = ' . $this->db->quote($iafp_obj_id, 'integer'); + $res = $this->db->query($query); + return array_map( + fn($row) => $row['field_id'], + $this->db->fetchAll($this->db->query($query)) + ); + } + + public function deleteFieldsByIds(array $field_ids): void + { + $query = 'DELETE FROM iafp_fields WHERE ' + . $this->db->in('field_id', $field_ids, false, 'integer'); + $this->db->manipulate($query); + + $query = 'DELETE FROM iafp_fieldmap WHERE ' + . $this->db->in('field_id', $field_ids, false, 'integer'); + $this->db->manipulate($query); + + foreach (array_keys(self::CONFIG_TABLES) as $cfg_table) { + $query = 'DELETE FROM ' . $cfg_table . ' WHERE ' + . $this->db->in('field_id', $field_ids, false, 'integer'); + $this->db->manipulate($query); + } + } + + public function getMappedFieldIds(): array + { + $query = 'SELECT DISTINCT field_id FROM iafp_fieldmap'; + $res = $this->db->query($query); + return array_map( + fn($row) => $row['field_id'], + $this->db->fetchAll($this->db->query($query)) + ); + } + + private function storeFieldConfig(int $field_id, FieldConfig $config): void + { + $table = array_filter( + self::CONFIG_TABLES, + fn($v) => $v === $config->getType(), + ); + + if ($table === []) { + var_dump($config->getOptions()); + die(); + } + + $table = array_key_first($table); + + $delete = 'DELETE FROM ' . $table . ' WHERE field_id = ' . $this->db->quote($field_id, 'integer'); + $this->db->manipulate($delete); + + if ($config->getOptions() !== []) { + $options = []; + foreach ($config->getOptions() as $opt) { + $options[] = '(' + . implode(',', [$this->db->quote($field_id, 'integer'), $this->db->quote($opt, 'text')]) + . ')' . PHP_EOL; + } + $insert = 'INSERT INTO ' . $table . ' (field_id, value) VALUES' . PHP_EOL + . implode(', ', $options); + + $this->db->manipulate($insert); + } + } + + + // --- collector --- + + public function getFormsSelection(): array + { + $options = [-1 => ['std.', 'no form pool']]; //add default option ('standard'), form only shows for entries > 1 + foreach ($this->getFormPools() as $pool_info) { + list($ref_id, $obj_id, $title) = $pool_info; + $forms = $this->getFormsForObjId($obj_id); + foreach ($forms as $form) { + $options['iass_' . $form->getFormId()] = [$form->getName(), $form->getDescription()]; + } + } + return $options; + } + + public function copyFieldsToIASS(int $iafp_form_id, int $iass_obj_id): void + { + $fields = $this->getFieldsForFormId($iafp_form_id); + foreach ($fields as $field) { + $config = $field->getConfig(); + $query = 'REPLACE INTO iass_formfields (obj_id, field_id, type, name, label, description, default_value, with_notes, available_for_examiners) VALUES (' . PHP_EOL + . $this->db->quote($iass_obj_id, 'integer') . ',' + . $this->db->quote($field->getFieldId(), 'integer') . ',' + . $this->db->quote($config->getType()->value, 'integer') . ',' + . $this->db->quote($field->getName(), 'text') . ',' + . $this->db->quote($config->getLabel(), 'text') . ',' + . $this->db->quote($config->getDescription(), 'text') . ',' + . $this->db->quote($config->getDefaultValue(), 'text') . ',' + . $this->db->quote($field->hasNotes(), 'integer') . ',' + . $this->db->quote($field->isAvailableForExaminers(), 'integer') . PHP_EOL + . ')'; + $this->db->manipulate($query); + + if (array_key_exists($config->getType()->name, self::CONFIG_TABLES_COLLECTOR)) { + list($source_table, $target_table) = self::CONFIG_TABLES_COLLECTOR[$field->getConfig()->getType()->name]; + + $fields = $this->getFieldsFromTable($source_table); + $query = 'REPLACE INTO ' . $target_table . '(obj_id, ' . $fields . ')' . PHP_EOL + . 'SELECT ' . $this->db->quote($iass_obj_id, 'integer') . ',' . $fields . PHP_EOL + . 'FROM ' . $source_table . PHP_EOL + . 'WHERE field_id = ' . $field->getFieldId(); + + $this->db->manipulate($query); + } + } + } + + protected function getFieldsFromTable(string $table_name): string + { + $query = 'SELECT GROUP_CONCAT(COLUMN_NAME) as fields FROM INFORMATION_SCHEMA.COLUMNS' . PHP_EOL + . 'WHERE COLUMN_NAME <> "obj_id" AND TABLE_NAME = ' . $this->db->quote($table_name, 'text'); + $res = $this->db->query($query); + return $this->db->fetchAssoc($res)['fields']; + } + + public function cloneFields(int $source_iass_obj_id, int $target_iass_obj_id): void + { + $tables = [ + 'iass_formfields', + 'iass_cfg_singleselect', + 'iass_cfg_tag', + ]; + foreach ($tables as $table) { + $fields = $this->getFieldsFromTable($table); + $query = 'INSERT INTO ' . $table . ' (obj_id,' . $fields . ')' . PHP_EOL + . 'SELECT ' + . $this->db->quote($target_iass_obj_id, 'integer') . ',' . $fields . PHP_EOL + . 'FROM ' . $table . ' WHERE obj_id = ' . $this->db->quote($source_iass_obj_id, 'integer'); + $this->db->manipulate($query); + } + } + + private function getFormPools(): array + { + $query = 'SELECT ref.obj_id, ref.ref_id, od.title FROM object_data od' . PHP_EOL + . 'JOIN object_reference ref ON od.obj_id = ref.obj_id' . PHP_EOL + . 'WHERE ref.deleted IS NULL AND od.type = "iafp"'; + + $ret = []; + $res = $this->db->query($query); + while ($row = $this->db->fetchAssoc($res)) { + $ref_id = (int) $row['ref_id']; + $obj_id = (int) $row['obj_id']; + if ($this->access->checkAccess('read', '', $ref_id)) { + $ret[] = [ + $ref_id, + $obj_id, + $row['title'] + ]; + }; + } + return $ret; + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/IAFPCollector.php b/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/IAFPCollector.php new file mode 100644 index 000000000000..5042d780adf4 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/IAFPCollector.php @@ -0,0 +1,42 @@ +getFormId()] = [$form->getName(), $form->getDescription()]; + */ + public function getFormsSelection(): array; + + /** + * copy fields and their config to the given IASS object + */ + public function copyFieldsToIASS(int $iafp_form_id, int $iass_obj_id): void; + + /** + * clone fields (on copy of object) + */ + public function cloneFields(int $source_iass_obj_id, int $target_iass_obj_id): void; + +} diff --git a/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/IASSCustomField.php b/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/IASSCustomField.php new file mode 100644 index 000000000000..85ed386f7b12 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/IASSCustomField.php @@ -0,0 +1,99 @@ +config; + } + + public function getFieldId(): int + { + return $this->field_id; + } + + public function getIds(): array + { + return [ + $this->obj_id, + $this->field_id, + $this->usr_id + ]; + } + + public function hasNotes(): bool + { + return $this->has_note; + } + + public function withValue(null|string|array $value): self + { + $clone = clone $this; + $clone->value = $value; + return $clone; + } + + public function getValue(): null|string|array + { + return $this->value; + } + + public function toFormInput( + FieldFactory $factory, + Refinery $refinery, + UploadHandler $upload_handler, //USE IT + FieldBuilder $field_builder + ): FormInput { + $ui_field = $field_builder->build( + $this->getConfig(), + $this->getValue(), + $upload_handler + ); + + if ($this->hasNotes()) { + $note = $factory->textarea('', '') + ->withValue($this->note) + ->withMaxLimit(512); + $ui_field = $factory->group([$ui_field, $note]); + } + return $ui_field; + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/SpecifiedFormStorage.php b/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/SpecifiedFormStorage.php new file mode 100644 index 000000000000..54e0cade3f8e --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/IndAssFacade/SpecifiedFormStorage.php @@ -0,0 +1,61 @@ +value . PHP_EOL + . 'LEFT JOIN iass_cfg_tag cfg_t ON ff.field_id = cfg_t.field_id AND type = ' . FieldType::TAG->value . PHP_EOL + + . 'LEFT JOIN iass_cust_values val ON ff.obj_id = val.obj_id' . PHP_EOL + . 'AND ff.field_id = val.field_id' . PHP_EOL + . 'AND val.usr_id = ' . $this->db->quote($member_usr_id, 'integer') . PHP_EOL + + . 'LEFT JOIN iass_cust_notes notes ON ff.obj_id = notes.obj_id' . PHP_EOL + . 'AND ff.field_id = notes.field_id' . PHP_EOL + . 'AND notes.usr_id = ' . $this->db->quote($member_usr_id, 'integer') . PHP_EOL + + . 'WHERE ff.obj_id = ' . $this->db->quote($obj_id, 'integer') . PHP_EOL + . 'GROUP BY field_id'; + + $ret = []; + $res = $this->db->query($query); + while ($row = $this->db->fetchAssoc($res)) { + $options = []; + if ($row['options'] !== null) { + $options = explode(',', $row['options']); + } + + $config = new FieldConfig( + FieldType::from($row['type']), + $row['label'], + $row['description'], + $row['default_value'], + $options + ); + $ret[] = new IASSCustomField( + $config, + $obj_id, + $row['field_id'], + $member_usr_id, + (bool) $row['with_notes'], + (bool) $row['available_for_examiners'], + (string) $row['value'], + (string) $row['note'], + ); + } + return $ret; + } + + public function storeSpecifiedUserValues(IASSCustomField ...$fields): void + { + foreach ($fields as $field) { + list($obj_id, $field_id, $usr_id) = $field->getIds(); + $value = $field->getValue(); + + if ($field->hasNotes()) { + list($value, $note) = $value; + $query = 'REPLACE INTO iass_cust_notes (obj_id, field_id, usr_id, value)' . PHP_EOL + . 'VALUES (' . PHP_EOL + . $obj_id . ',' + . $field_id . ',' + . $usr_id . ',' + . $this->db->quote((string) $note, 'text') . PHP_EOL + . ')'; + $this->db->manipulate($query); + } + + if (is_array($value)) { + $value = implode(self::VALUE_DELIMITER, $value); + } + $query = 'REPLACE INTO iass_cust_values (obj_id, field_id, usr_id, value)' . PHP_EOL + . 'VALUES (' . PHP_EOL + . $obj_id . ',' + . $field_id . ',' + . $usr_id . ',' + . $this->db->quote($value, 'text') . PHP_EOL + . ')'; + $this->db->manipulate($query); + } + } + + public function deleteSpecifiedUserValues( + IRSS $irss, + ilIndividualAssessmentGradingStakeholder $stakeholder, + int $iass_obj_id, + int $member_usr_id, + ): void { + $this->deleteFileResources( + $irss, + $stakeholder, + ...$this->getResourceIdentifiers($iass_obj_id, $member_usr_id) + ); + + $tables = [ + 'iass_cust_values', + 'iass_cust_notes' + ]; + foreach ($tables as $table) { + $query = 'DELETE FROM ' . $table . PHP_EOL + . 'WHERE obj_id = ' . $this->db->quote($iass_obj_id, 'integer') . PHP_EOL + . 'AND usr_id = ' . $this->db->quote($member_usr_id, 'integer'); + $this->db->manipulate($query); + } + } + + public function deleteAllUserValuesAndFields( + IRSS $irss, + ilIndividualAssessmentGradingStakeholder $stakeholder, + int $iass_obj_id + ): void { + $this->deleteFileResources( + $irss, + $stakeholder, + ...$this->getResourceIdentifiers($iass_obj_id) + ); + + $tables = [ + 'iass_cfg_singleselect', + 'iass_cfg_tag', + 'iass_cust_values', + 'iass_cust_notes', + 'iass_formfields', + ]; + foreach ($tables as $table) { + $query = 'DELETE FROM ' . $table . PHP_EOL + . 'WHERE obj_id = ' . $this->db->quote($iass_obj_id, 'integer'); + $this->db->manipulate($query); + } + } + + protected function deleteFileResources( + IRSS $irss, + ilIndividualAssessmentGradingStakeholder $stakeholder, + string ...$ids + ): void { + foreach ($ids as $identifier) { + $resource_id = $irss->manage()->find($identifier); + if ($resource_id !== null) { + $irss->manage()->remove($resource_id, $stakeholder); + } + } + } + + protected function getResourceIdentifiers(int $obj_id, int ...$usr_ids): array + { + $query = 'SELECT value FROM iass_cust_values ival' . PHP_EOL + . 'JOIN iass_formfields ifield' . PHP_EOL + . 'ON ival.obj_id = ifield.obj_id' . PHP_EOL + . 'AND ival.field_id = ifield.field_id' . PHP_EOL + . 'WHERE ival.obj_id = ' . $this->db->quote($obj_id, 'integer') . PHP_EOL + . 'AND ifield.type = ' . FieldType::FILE->value; + + if ($usr_ids !== []) { + $query .= ' AND ' . $this->db->in('ival.usr_id', $usr_ids, false, 'integer'); + } + + $ret = []; + $res = $this->db->query($query); + while ($row = $this->db->fetchAssoc($res)) { + $ret[] = $row['value']; + } + return $ret; + } + +} diff --git a/Modules/IndividualAssessmentFormPool/classes/Setup/class.IAFPTablesDBUpdateSteps.php b/Modules/IndividualAssessmentFormPool/classes/Setup/class.IAFPTablesDBUpdateSteps.php new file mode 100644 index 000000000000..268df140c326 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Setup/class.IAFPTablesDBUpdateSteps.php @@ -0,0 +1,369 @@ +db = $db; + } + + public function step_1(): void + { + $this->db->createTable( + 'iafp_forms', + [ + 'obj_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'form_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'name' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ], + 'description' => [ + 'type' => 'text', + 'length' => 255, + 'notnull' => true, + 'default' => '', + ], + ] + ); + + $this->db->addPrimaryKey('iafp_forms', ['obj_id', 'form_id']); + $this->db->addIndex('iafp_forms', ['obj_id'], 'oid'); + $this->db->addIndex('iafp_forms', ['form_id'], 'fid'); + + $this->db->createSequence('iafp_forms'); + } + + public function step_2(): void + { + $this->db->createTable( + 'iafp_fields', + [ + 'obj_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'type' => [ + 'type' => 'integer', + 'length' => 1, + 'notnull' => true + ], + 'name' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ], + 'label' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ], + 'description' => [ + 'type' => 'text', + 'length' => 255, + 'notnull' => true, + 'default' => '', + ], + 'default_value' => [ + 'type' => 'text', + 'length' => 512, + 'notnull' => false, + ], + 'with_notes' => [ + 'type' => 'integer', + 'length' => 1, + 'notnull' => true, + 'default' => 0, + ], + 'available_for_examiners' => [ + 'type' => 'integer', + 'length' => 1, + 'notnull' => true, + 'default' => 1, + ], + ] + ); + + $this->db->addPrimaryKey('iafp_fields', ['obj_id', 'field_id']); + $this->db->addIndex('iafp_fields', ['obj_id'], 'oid'); + $this->db->addIndex('iafp_fields', ['field_id'], 'fid'); + + $this->db->createSequence('iafp_fields'); + } + + public function step_3(): void + { + $this->db->createTable( + 'iafp_fieldmap', + [ + 'form_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'position' => [ + 'type' => 'integer', + 'length' => 4, + 'notnull' => true + ], + ] + ); + + $this->db->addPrimaryKey('iafp_fieldmap', ['form_id', 'field_id']); + $this->db->addIndex('iafp_fieldmap', ['form_id'], 'fo'); + $this->db->addIndex('iafp_fieldmap', ['field_id'], 'fi'); + } + + public function step_4(): void + { + $this->db->createTable( + 'iafp_cfg_singleselect', + [ + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'value' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ], + ] + ); + } + + public function step_5(): void + { + $this->db->createTable( + 'iafp_cfg_tag', + [ + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'value' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ] + ] + ); + } + + public function step_6(): void + { + $this->db->createTable( + 'iass_formfields', + [ + 'obj_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'type' => [ + 'type' => 'integer', + 'length' => 1, + 'notnull' => true + ], + 'name' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ], + 'label' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ], + 'description' => [ + 'type' => 'text', + 'length' => 255, + 'notnull' => true, + 'default' => '', + ], + 'default_value' => [ + 'type' => 'text', + 'length' => 255, + 'notnull' => false, + ], + 'with_notes' => [ + 'type' => 'integer', + 'length' => 1, + 'notnull' => true, + 'default' => 0, + ], + 'available_for_examiners' => [ + 'type' => 'integer', + 'length' => 1, + 'notnull' => true, + 'default' => 1, + ], + ] + ); + $this->db->addPrimaryKey('iass_formfields', ['obj_id', 'field_id']); + $this->db->addIndex('iass_formfields', ['obj_id'], 'oid'); + $this->db->addIndex('iass_formfields', ['field_id'], 'fid'); + } + + public function step_7(): void + { + $this->db->createTable( + 'iass_cfg_singleselect', + [ + 'obj_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'value' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ], + ] + ); + } + + public function step_8(): void + { + $this->db->createTable( + 'iass_cfg_tag', + [ + 'obj_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'value' => [ + 'type' => 'text', + 'length' => 128, + 'notnull' => true + ] + ] + ); + } + + public function step_9(): void + { + $this->db->createTable( + 'iass_cust_values', + [ + 'obj_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'usr_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'value' => [ + 'type' => 'text', + 'length' => 512, + 'notnull' => false + ] + ] + ); + $this->db->addPrimaryKey('iass_cust_values', ['obj_id', 'field_id', 'usr_id']); + $this->db->addIndex('iass_cust_values', ['obj_id'], 'oid'); + $this->db->addIndex('iass_cust_values', ['field_id'], 'fid'); + $this->db->addIndex('iass_cust_values', ['usr_id'], 'uid'); + } + + public function step_10(): void + { + $this->db->createTable( + 'iass_cust_notes', + [ + 'obj_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'field_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'usr_id' => [ + 'type' => 'integer', + 'length' => 8, + 'notnull' => true + ], + 'value' => [ + 'type' => 'text', + 'length' => 512, + 'notnull' => true + ] + ] + ); + $this->db->addPrimaryKey('iass_cust_notes', ['obj_id', 'field_id', 'usr_id']); + $this->db->addIndex('iass_cust_notes', ['obj_id'], 'oid'); + $this->db->addIndex('iass_cust_notes', ['field_id'], 'fid'); + $this->db->addIndex('iass_cust_notes', ['usr_id'], 'uid'); + } + +} diff --git a/Modules/IndividualAssessmentFormPool/classes/Setup/class.ilIndividualAssessmentFormPoolSetupAgent.php b/Modules/IndividualAssessmentFormPool/classes/Setup/class.ilIndividualAssessmentFormPoolSetupAgent.php new file mode 100644 index 000000000000..fff291e45462 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/Setup/class.ilIndividualAssessmentFormPoolSetupAgent.php @@ -0,0 +1,123 @@ +getObjectives() + ); + } + + /** + * @inheritdoc + */ + public function getUpdateObjective(Setup\Config $config = null): Setup\Objective + { + return new Setup\ObjectiveCollection( + 'Type is registered and database is updated for Module/IndividualAssessmentFormPool', + false, + ...$this->getObjectives() + ); + } + + /** + * @inheritdoc + */ + public function getBuildArtifactObjective(): Setup\Objective + { + return new Setup\Objective\NullObjective(); + } + + /** + * @inheritdoc + */ + public function getStatusObjective(Setup\Metrics\Storage $storage): Setup\Objective + { + return new Setup\ObjectiveCollection( + 'Component IndividualAssessmentFormPool', + true, + new ilObjectNewTypeAddedObjective( + self::TYPE, + self::TYPE_TITLE, + ), + new ilDatabaseUpdateStepsMetricsCollectedObjective($storage, new IAFPTablesDBUpdateSteps()), + //new ilDatabaseUpdateStepsMetricsCollectedObjective($storage, new ilLearningSequenceRegisterNotificationType()) + ); + } + + /** + * @inheritDoc + */ + public function getMigrations(): array + { + return []; + } + + private function getObjectives(): array + { + return [ + new ilObjectNewTypeAddedObjective( + self::TYPE, + self::TYPE_TITLE, + ), + new ilDatabaseUpdateStepsExecutedObjective( + new IAFPTablesDBUpdateSteps() + ), + new ilAccessRbacStandardOperationsAddedObjective(self::TYPE), + + /* + new ilDatabaseUpdateStepsExecutedObjective( + new xxxxTableDBUpdateSteps() + ) + */ + ]; + } + +} diff --git a/Modules/IndividualAssessmentFormPool/classes/class.IAFPAccessHandler.php b/Modules/IndividualAssessmentFormPool/classes/class.IAFPAccessHandler.php new file mode 100644 index 000000000000..ee5e99148cf3 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/class.IAFPAccessHandler.php @@ -0,0 +1,74 @@ +isSystemAdmin() || + $this->access->checkAccess('visible', '', $this->iafp_ref_id); + } + + public function mayRead(): bool + { + return $this->isSystemAdmin() || + $this->access->checkAccess('read', '', $this->iafp_ref_id); + + } + public function mayEdit(): bool + { + return $this->isSystemAdmin() || + $this->access->checkAccess('write', '', $this->iafp_ref_id); + } + + protected function isSystemAdmin(): bool + { + return $this->review->isAssigned($this->current_usr_id, SYSTEM_ROLE_ID); + } + +} diff --git a/Modules/IndividualAssessmentFormPool/classes/class.IAFPFieldsGUI.php b/Modules/IndividualAssessmentFormPool/classes/class.IAFPFieldsGUI.php new file mode 100644 index 000000000000..9be8df394a29 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/class.IAFPFieldsGUI.php @@ -0,0 +1,317 @@ +url_builder, + $this->action_token, + $this->rowid_token + ) = $url_builder->acquireParameters($namespace, 'act', 'fid'); + } + + public function executeCommand(): void + { + $next_class = $this->ctrl->getNextClass($this); + $cmd = $this->ctrl->getCmd() ?? self::CMD_VIEW; + + //$this->addToNavigationHistory(); + //$this->prepareOutput(); + + switch ($next_class) { + default: + + if ($this->query->has($this->action_token->getName())) { + $cmd = $this->query->retrieve( + $this->action_token->getName(), + $this->refinery->to()->string() + ); + $ids = $this->query->retrieve( + $this->rowid_token->getName(), + $this->refinery->byTrying([ + $this->refinery->to()->listOf($this->refinery->kindlyTo()->int()), + $this->refinery->custom()->transformation($this->getAllIds()), + $this->refinery->always([]) + ]) + ); + } + + switch ($cmd) { + case self::CMD_LIST: + $list = $this->listFields(); + //if ($list !== '') { + $this->tpl->setContent($list); + break; + //} + //$this->ctrl->redirect($this, self:CMD_EDIT); + case self::CMD_CREATE: + $type = $this->getFieldCreationModal()->withRequest($this->request)->getData(); + $field_id = $this->forms_repo->createField($type, $this->iafp_obj_id)->getFieldId(); + $url = $this->getUrlString(self::CMD_EDIT, $field_id); + $this->ctrl->redirectToURL($url); + break; + + case self::CMD_EDIT: + $field_id = array_shift($ids); + $frm = $this->getEditForm($this->forms_repo->getFieldById($field_id)); + $this->tpl->setContent($this->ui_renderer->render($frm)); + break; + + case self::CMD_SAVE: + $field_id = array_shift($ids); + $field = $this->forms_repo->getFieldById($field_id); + $ui_form = $this->getEditForm($field); + $this->tpl->setContent($this->ui_renderer->render($this->save($ui_form))); + break; + + case self::CMD_DELETE: + echo $this->ui_renderer->renderAsync( + $this->getDeleteConfirmation($ids) + ); + exit(); + case self::CMD_DELETE_CONFIRMED: + $used_ids = $this->forms_repo->getMappedFieldIds(); + $ids = array_filter($ids, static fn(int $id): bool => !in_array($id, $used_ids)); + $this->forms_repo->deleteFieldsByIds($ids); + $this->ctrl->redirect($this, self::CMD_LIST); + + // no break + default: + throw new \Exception('no such command: ' . $cmd); + } + } + //$this->addHeaderAction(); + } + + protected function getTableActions(): array + { + $f = $this->ui_factory->table()->action(); + return [ + 'edit' => $f->single( + $this->txt('edit'), + $this->url_builder->withParameter($this->action_token, self::CMD_EDIT), + $this->rowid_token + ), + 'delete' => $f->standard( + $this->txt('delete'), + $this->url_builder->withParameter($this->action_token, 'delete'), + $this->rowid_token + )->withAsync(), + ]; + } + + protected function getUrlString(string $cmd, int|array $row_ids): string + { + $row_ids = is_array($row_ids) ? $row_ids : [$row_ids]; + return $this->url_builder + ->withParameter($this->action_token, $cmd) + ->withParameter($this->rowid_token, $row_ids) + ->buildURI() + ->__toString(); + } + + + protected function getFieldCreationModal(): RoundTrip + { + return $this->ui_factory->modal()->roundtrip( + $this->txt('new_field'), + null, + [ + $this->ui_factory->input()->field()->select( + $this->lng->txt('field_type'), + FieldType::toArray(), + $this->lng->txt('field_type_byline') + ) + ], + $this->ctrl->getLinkTarget($this, self::CMD_CREATE) + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn($v) => FieldType::from((int) array_shift($v)) + ) + ); + } + + protected function listFields(): string + { + $modal = $this->getFieldCreationModal(); + $new_entry = $this->ui_factory->button()->primary( + $this->txt('new_field'), + //$this->ctrl->getLinkTarget($this, self::CMD_CREATE) + $modal->getShowSignal() + ); + + $table = $this->ui_factory->table() + ->data( + $this->txt('iafp_fields'), + $this->data_retrieval->getColumns(), + $this->data_retrieval + ) + ->withId('iafp_fields_table_' . $this->iafp_obj_id) + ->withActions($this->getTableActions()) + ->withRequest($this->request); + + return $this->ui_renderer->render([ + $modal, + $new_entry, + $table + ]); + } + + protected function getEditForm(Field $field): UIForm + { + return $this->ui_factory->input()->container()->form()->standard( + $this->getUrlString(self::CMD_SAVE, $field->getFieldId()), + [ + $field->toFormInput( + $this->ui_factory->input()->field(), + $this->lng, + $this->refinery, + $this->iafp_obj_id, + $this->field_builder + ) + ] + ) + ->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn($v) => array_shift($v) + ) + ); + } + + protected function save(UIForm $ui_form): UIForm + { + $ui_form = $ui_form->withRequest($this->request); + $field = $ui_form->getData(); + if ($field === null) { + return $ui_form; + } + + $field_id = $this->forms_repo->storeField($field)->getFieldId(); + $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_saved'), true); + + $url = $this->getUrlString(self::CMD_EDIT, $field_id); + $this->ctrl->redirectToURL($url); + } + + + protected function getDeleteConfirmation(array $ids): MessageBox + { + $msg = ''; + + $used_ids = $this->forms_repo->getMappedFieldIds(); + $delete_ids = array_filter( + $ids, + static fn(int $id): bool => !in_array($id, $used_ids) + ); + $nodelete_ids = array_diff($ids, $delete_ids); + if ($nodelete_ids !== []) { + $msg .= 'Cannot delete these (used):' + . $this->ui_renderer->render( + $this->ui_factory->listing()->unordered( + array_map('strval', $nodelete_ids) + ) + ); + } + + $msg .= '
Delete?' + . $this->ui_renderer->render( + $this->ui_factory->listing()->unordered( + array_map('strval', $delete_ids) + ) + ); + + $buttons = [ + $this->ui_factory->button()->standard( + $this->lng->txt('confirm_delete'), + $this->getUrlString(self::CMD_DELETE_CONFIRMED, $ids) + ), + $this->ui_factory->button()->standard( + $this->lng->txt('confirm_cancel'), + $this->ctrl->getLinkTarget($this, self::CMD_LIST) + ), + ]; + return $this->ui_factory->messageBox()->confirmation($msg) + ->withButtons($buttons); + } + + protected function getAllIds(): Closure + { + $repo = $this->forms_repo; + $obj_id = $this->iafp_obj_id; + return function ($v) use ($repo, $obj_id): array { + if (array_shift($v) === 'ALL_OBJECTS') { + return $repo->getAllFieldIdsForObjId($obj_id); + } + throw new \InvalidArgumentException('no ids'); + }; + } + + protected function txt(string $code): string + { + return $this->lng->txt($code); + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/class.IAFPFormsGUI.php b/Modules/IndividualAssessmentFormPool/classes/class.IAFPFormsGUI.php new file mode 100644 index 000000000000..05d9490760bc --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/class.IAFPFormsGUI.php @@ -0,0 +1,377 @@ +url_builder, + $this->action_token, + $this->rowid_token + ) = $url_builder->acquireParameters($namespace, 'act', 'fid'); + } + + public function executeCommand(): void + { + $next_class = $this->ctrl->getNextClass($this); + $cmd = $this->ctrl->getCmd() ?? self::CMD_VIEW; + + //$this->addToNavigationHistory(); + //$this->prepareOutput(); + + switch ($next_class) { + /* + case 'ilinfoscreengui': + $this->tabs_gui->activateTab(self::TAB_INFO); + $this->ctrl->forwardCommand(new ilInfoScreenGUI($this)); + break; + */ + default: + + if ($this->query->has($this->action_token->getName())) { + $cmd = $this->query->retrieve( + $this->action_token->getName(), + $this->refinery->to()->string() + ); + $ids = $this->query->retrieve( + $this->rowid_token->getName(), + $this->refinery->byTrying([ + $this->refinery->to()->listOf($this->refinery->kindlyTo()->int()), + $this->refinery->custom()->transformation($this->getAllIds()), + $this->refinery->always([]) + ]) + ); + } + + switch ($cmd) { + case self::CMD_LIST: + //access $this->object->accessHandler()->simulateMember() + $list = $this->listForms(); + if ($list !== '') { + $this->tpl->setContent($list); + break; + } + //$this->ctrl->redirect($this, self:CMD_EDIT); + // no break + case self::CMD_CREATE: + $frm = $this->getEditForm($this->forms_repo->getNewForm($this->iafp_obj_id)); + $this->tpl->setContent($this->ui_renderer->render($frm)); + break; + + case self::CMD_EDIT: + //access $this->object->accessHandler()->simulateMember() + $form_id = array_shift($ids); + $ui_form = $this->getEditForm($this->forms_repo->getFormById($form_id)); + + $out = $this->getFieldSelection($form_id); + $out[] = $ui_form; + + $this->tpl->setContent($this->ui_renderer->render($out)); + break; + + case self::CMD_SAVE: + $form_id = array_shift($ids); + $form = $form_id === -1 ? + $this->forms_repo->getNewForm($this->iafp_obj_id) : $this->forms_repo->getFormById($form_id); + + //access $this->object->accessHandler()->simulateMember() + $ui_form = $this->getEditForm($form); + $this->tpl->setContent($this->ui_renderer->render($this->save($ui_form))); + break; + + case self::CMD_ADDFIELDS: + $form_id = array_shift($ids); + $this->addFields($form_id); + $url = $this->getUrlString(self::CMD_EDIT, $form_id); + $this->ctrl->redirectToURL($url); + + // no break + case self::CMD_PREVIEW: + $form_id = array_shift($ids); + $form = $this->forms_repo->getFormById($form_id); + /* + $fields = array_map( + fn($f) => $f->build($this->ui_factory->input()->field()), + $form->getFields() + ); + */ + $fields = array_map( + fn($f) => $this->field_builder->build($f), + $form->getFields() + ); + + + echo $this->ui_renderer->render( + $this->ui_factory->input()->field()->section( + $fields, + $this->lng->txt('preview') + )->withDisabled(true) + ); + exit(); + + case self::CMD_DELETE: + echo $this->ui_renderer->renderAsync( + $this->getDeleteConfirmation($ids) + ); + exit(); + case self::CMD_DELETE_CONFIRMED: + $this->forms_repo->deleteFormsByIds($ids); + $this->ctrl->redirect($this, self::CMD_LIST); + + // no break + default: + throw new \Exception('no such command: ' . $cmd); + } + } + //$this->addHeaderAction(); + } + + protected function getTableActions(): array + { + $f = $this->ui_factory->table()->action(); + return [ + 'edit' => $f->single( + $this->txt('edit'), + $this->url_builder->withParameter($this->action_token, self::CMD_EDIT), + $this->rowid_token + ), + 'preview' => $f->single( + $this->txt('preview'), + $this->url_builder->withParameter($this->action_token, self::CMD_PREVIEW), + $this->rowid_token + )->withAsync(), + 'delete' => $f->standard( + $this->txt('delete'), + $this->url_builder->withParameter($this->action_token, 'delete'), + $this->rowid_token + )->withAsync(), + ]; + } + + protected function getUrlString(string $cmd, int|array $row_ids): string + { + $row_ids = is_array($row_ids) ? $row_ids : [$row_ids]; + return $this->url_builder + ->withParameter($this->action_token, $cmd) + ->withParameter($this->rowid_token, $row_ids) + ->buildURI() + ->__toString(); + } + + protected function listForms(): string + { + $new_entry = $this->ui_factory->button()->primary( + $this->txt('new_form'), + $this->ctrl->getLinkTarget($this, self::CMD_CREATE) + ); + + $table = $this->ui_factory->table() + ->data( + $this->txt('iafp_forms'), + $this->data_retrieval->getColumns(), + $this->data_retrieval + ) + ->withId('iafp_forms_table_' . $this->iafp_obj_id) + ->withActions($this->getTableActions()) + ->withRequest($this->request); + + return $this->ui_renderer->render([ + $new_entry, + $table + ]); + } + + protected function getEditForm(Form $form): UIForm + { + return $this->ui_factory->input()->container()->form()->standard( + $this->getUrlString(self::CMD_SAVE, $form->getFormId()), + [ + $form->toFormInput( + $this->ui_factory->input()->field(), + $this->ui_renderer, + $this->lng, + $this->refinery, + $this->iafp_obj_id, + $this->forms_repo, + $this->field_builder + ) + ] + ) + ->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn($v) => array_shift($v) + ) + ); + } + + protected function getFieldSelection(int $form_id): array + { + $options = []; + $available_fields = $this->forms_repo->getFieldsForObjId($this->iafp_obj_id); + foreach ($available_fields as $field) { + $options[$field->getFieldId()] = $field->getName(); + } + + $modal = $this->ui_factory->modal()->roundtrip( + $this->lng->txt('add_fields'), + null, + [ + $this->ui_factory->input()->field()->multiselect( + $this->lng->txt('pick_fields'), + $options + ) + ], + $this->getUrlString(self::CMD_ADDFIELDS, $form_id) + ) + ->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn($v) => array_shift($v) + ) + ); + + $button = $this->ui_factory->button()->primary( + $this->lng->txt('add_fields'), + $modal->getShowSignal() + ); + return [$button, $modal]; + } + + + protected function addFields(int $form_id): void + { + list($button, $modal) = $this->getFieldSelection($form_id); + $data = $modal->withRequest($this->request)->getData(); + if ($data !== null) { + $form = $this->forms_repo->getFormById($form_id); + $fields = $form->getFields(); + $field_ids = array_map(fn($f) => $f->getFieldId(), $fields); + foreach ($data as $field_id) { + if (! in_array((int) $field_id, $field_ids)) { + $fields[] = $this->forms_repo->getFieldById((int) $field_id); + } + } + $this->forms_repo->storeForm($form->withFields(...$fields)); + $this->tpl->setOnScreenMessage('success', $this->lng->txt('fields_added'), true); + } + } + + protected function save(UIForm $ui_form): UIForm + { + $ui_form = $ui_form->withRequest($this->request); + $form = $ui_form->getData(); + if ($form === null) { + return $ui_form; + } + $form_id = $this->forms_repo->storeForm($form); + $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_saved'), true); + + $url = $this->getUrlString(self::CMD_EDIT, $form_id); + $this->ctrl->redirectToURL($url); + } + + protected function getDeleteConfirmation(array $ids): MessageBox + { + $msg = 'Delete?' + . $this->ui_renderer->render( + $this->ui_factory->listing()->unordered( + array_map('strval', $ids) + ) + ); + + $buttons = [ + $this->ui_factory->button()->standard( + $this->lng->txt('confirm_delete'), + $this->getUrlString(self::CMD_DELETE_CONFIRMED, $ids) + ), + $this->ui_factory->button()->standard( + $this->lng->txt('confirm_cancel'), + $this->ctrl->getLinkTarget($this, self::CMD_LIST) + ), + ]; + return $this->ui_factory->messageBox()->confirmation($msg) + ->withButtons($buttons); + } + + protected function getAllIds(): Closure + { + $repo = $this->forms_repo; + $obj_id = $this->iafp_obj_id; + return function ($v) use ($repo, $obj_id): array { + if (array_shift($v) === 'ALL_OBJECTS') { + return $repo->getAllFormIdsForObjId($obj_id); + } + throw new \InvalidArgumentException('no ids'); + }; + } + + protected function txt(string $code): string + { + return $this->lng->txt($code); + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/class.ilIndividualAssessmentFormPoolDIC.php b/Modules/IndividualAssessmentFormPool/classes/class.ilIndividualAssessmentFormPoolDIC.php new file mode 100644 index 000000000000..c16009259bda --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/class.ilIndividualAssessmentFormPoolDIC.php @@ -0,0 +1,131 @@ + + new URLBuilder( + (new DataFactory())->uri($DIC['http']->request()->getUri()->__toString()) + ); + + $container['fieldbuilder'] = static fn(): FieldBuilder => + new FieldBuilder( + $DIC['ui.factory']->input()->field(), + $DIC['refinery'], + $DIC['lng'], + new \ilUIDemoFileUploadHandlerGUI(), + new \ilUIMarkdownPreviewGUI() + ); + + $container['repo.forms'] = static fn(): FormsStorageDB => + new FormsStorageDB( + $DIC['ilDB'], + $DIC['ilAccess'] + ); + return $container; + } + + public function getObjectDIC( + ilObjIndividualAssessmentFormPool $object, + ArrayAccess $DIC + ): Container { + $container = new Container(); + $general_dic = self::getGeneralDIC($DIC); + + $container['repo.forms'] = static fn(): FormsStorageDB => $general_dic['repo.forms']; + $container['urlbuilder'] = static fn($c): URLBuilder => $general_dic['urlbuilder']; + $container['fieldbuilder'] = static fn($c): FieldBuilder => $general_dic['fieldbuilder']; + + $container['gui.forms'] = static fn($c): IAFPFormsGUI => + new IAFPFormsGUI( + $c['access'], + $DIC['tpl'], + $DIC['ilCtrl'], + $DIC['ui.factory'], + $DIC['ui.renderer'], + $DIC['refinery'], + $DIC['http']->request(), + $DIC['http']->wrapper()->query(), + $DIC['lng'], + $c['repo.forms'], + $c['dataretrieval.forms'], + $c['urlbuilder'], + $c['fieldbuilder'], + $object->getId(), + ); + + $container['gui.fields'] = static fn($c): IAFPFieldsGUI => + new IAFPFieldsGUI( + $c['access'], + $DIC['tpl'], + $DIC['ilCtrl'], + $DIC['ui.factory'], + $DIC['ui.renderer'], + $DIC['refinery'], + $DIC['http']->request(), + $DIC['http']->wrapper()->query(), + $DIC['lng'], + $c['repo.forms'], + $c['dataretrieval.fields'], + $c['urlbuilder'], + $c['fieldbuilder'], + $object->getId(), + ); + + $container['access'] = static fn(): IAFPAccessHandler => + new IAFPAccessHandler( + $DIC['ilAccess'], + $DIC['rbacreview'], + $DIC['ilUser']->getId(), + $object->getRefId() + ); + + $container['dataretrieval.forms'] = static fn($c): FormsDataRetrieval => + new FormsDataRetrieval( + $c['repo.forms'], + $DIC['ui.factory'], + $DIC['lng'], + $object->getId(), + ); + $container['dataretrieval.fields'] = static fn($c): FieldsDataRetrieval => + new FieldsDataRetrieval( + $c['repo.forms'], + $DIC['ui.factory'], + $DIC['lng'], + $object->getId(), + ); + + return $container; + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPool.php b/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPool.php new file mode 100644 index 000000000000..622d90d24580 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPool.php @@ -0,0 +1,149 @@ +type = self::OBJ_TYPE; + parent::__construct($id, $call_by_reference); + + global $DIC; + $this->dic = $this->getObjectDIC($this, $DIC); + $this->repo = $this->dic['repo.forms']; + $this->access = $this->dic['access']; + + } + + public static function getRepository(): FormsStorageDB + { + global $DIC; + return self::getGeneralDIC($DIC)['repo.forms']; + } + + /** + * @inheritdoc + */ + public function create(): int + { + $id = parent::create(); + return $id; + } + + /** + * @inheritdoc + */ + public function read(): void + { + parent::read(); + } + + /** + * @inheritdoc + */ + public function delete(): bool + { + $form_ids = $this->repo->getAllFormIdsForObjId($this->getId()); + $fields = $this->repo->getFieldsForObjId($this->getId()); + $field_ids = array_map( + static fn(Field $field): int => $field->getFieldId(), + iterator_to_array($fields) + ); + $this->repo->deleteFormsByIds($form_ids); + $this->repo->deleteFieldsByIds($field_ids); + return parent::delete(); + } + + /** + * @inheritdoc + */ + public function update(): bool + { + parent::update(); + return true; + } + + /** + * @inheritdoc + */ + public function initDefaultRoles(): void + { + //$this->access_handler->initDefaultRolesForObject($this); + } + + /** + * @inheritdoc + */ + public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = false): ?ilObject + { + $new_obj = parent::cloneObject($target_id, $copy_id, $omit_tree); + $forms = $this->repo->getFormsForObjId($this->getId()); + $fields = $this->repo->getFieldsForObjId($this->getId()); + + $field_mapping = []; + foreach ($fields as $field) { + $nu_field = $this->repo->storeField( + $field + ->asNew() + ->withObjId($new_obj->getId()) + ); + $field_mapping[$field->getFieldId()] = $nu_field->getFieldId(); + } + + + $nu_forms = []; + foreach ($forms as $form) { + $fields = array_map( + static fn(Field $field): Field => $field + ->withFieldId($field_mapping[$field->getFieldId()]) + ->withObjId($new_obj->getId()), + $form->getFields() + ); + + $this->repo->storeForm( + $form + ->asNew() + ->withObjId($new_obj->getId()) + ->withFields(...$fields) + ); + } + + return $new_obj; + } + + + public function getDic(): Pimple\Container + { + return $this->dic; + } + +} diff --git a/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolAccess.php b/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolAccess.php new file mode 100644 index 000000000000..9aa565decbb4 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolAccess.php @@ -0,0 +1,34 @@ + "read", "cmd" => "", "lang_var" => "show", "default" => true], + ["permission" => "write", "cmd" => "edit", "lang_var" => "settings", "default" => false] + ]; + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolGUI.php b/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolGUI.php new file mode 100644 index 000000000000..7ab5a1519961 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolGUI.php @@ -0,0 +1,343 @@ +type = 'iafp'; + + global $DIC; + $this->navigation_history = $DIC['ilNavigationHistory']; + $this->tpl = $DIC['tpl']; + $this->ctrl = $DIC['ilCtrl']; + $this->usr = $DIC['ilUser']; + $this->error_object = $DIC['ilErr']; + $this->lng = $DIC['lng']; + $this->lng->loadLanguageModule('iafp'); + $this->tpl->loadStandardTemplate(); + $this->refinery = $DIC->refinery(); + $this->request_wrapper = $DIC->http()->wrapper()->query(); + + parent::__construct($data, $id, $call_by_reference, $prepare_output); + } + + public function executeCommand(): void + { + $next_class = $this->ctrl->getNextClass($this); + $cmd = $this->ctrl->getCmd(self::CMD_VIEW); + + $this->addToNavigationHistory(); + $this->prepareOutput(); + + switch ($next_class) { + case 'ilinfoscreengui': + $this->tabs_gui->activateTab(self::TAB_INFO); + $this->ctrl->forwardCommand(new ilInfoScreenGUI($this)); + break; + case "ilexportgui": + $this->tabs_gui->activateTab(self::TAB_EXPORT); + $exp_gui = new ilExportGUI($this); // $this is the ilObj...GUI class of the resource + $exp_gui->addFormat("xml"); + $this->ctrl->forwardCommand($exp_gui); + break; + case 'ilpermissiongui': + $this->tabs_gui->activateTab(self::TAB_PERMISSION); + $this->ctrl->forwardCommand(new ilPermissionGUI($this)); + break; + case "ilcommonactiondispatchergui": + $gui = ilCommonActionDispatcherGUI::getInstanceFromAjaxCall(); + $this->ctrl->forwardCommand($gui); + break; + case 'ilobjectcopygui': + $cp = new ilObjectCopyGUI($this); + $this->ctrl->forwardCommand(new ilObjectCopyGUI($this)); + break; + + case 'iafpformsgui': + $this->tabs_gui->activateTab(self::TAB_FORMS); + $gui = $this->object->getDic()['gui.forms']; + $this->ctrl->forwardCommand($gui); + break; + case 'iafpfieldsgui': + $this->tabs_gui->activateTab(self::TAB_FIELDS); + $gui = $this->object->getDic()['gui.fields']; + $this->ctrl->forwardCommand($gui); + break; + + default: + switch ($cmd) { + case 'create': + parent::createObject(); + break; + case 'cancel': + parent::cancelObject(); + break; + case self::CMD_VIEW: + case self::CMD_INFO: + $this->checkPermission('visible'); + $this->ctrl->redirectByClass('ilinfoscreengui', 'showSummary'); + break; + case self::CMD_EDIT: + $this->checkPermission('write'); + $this->tabs_gui->activateTab(self::TAB_SETTINGS); + $this->edit(); + break; + case self::CMD_SAVE: + if ($this->getCreationMode()) { + parent::saveObject(); + $this->ctrl->redirectToURL( + $this->getLinkTarget(self::CMD_EDIT) + ); + } + $this->checkPermission('write'); + $this->tabs_gui->activateTab(self::TAB_SETTINGS); + $this->save(); + break; + + default: + throw new \Exception('no such command: ' . $cmd); + } + } + $this->addHeaderAction(); + } + + public function edit(): void + { + $form = $this->initPropertiesForm(); + $this->tpl->setContent($this->ui_renderer->render($form)); + } + + public function save(): void + { + $form = $this->initPropertiesForm()->withRequest($this->request); + $data = $form->getData(); + + if ($data === null) { + $this->tpl->setOnScreenMessage('failure', $this->lng->txt("form_input_not_valid"), true); + } else { + list($title_and_desc, $online) = $data; + $this->object->getObjectProperties()->storePropertyTitleAndDescription($title_and_desc); + $this->object->getObjectProperties()->storePropertyIsOnline($online); + + $this->tpl->setOnScreenMessage('success', $this->lng->txt("msg_obj_modified"), true); + } + $this->tpl->setContent($this->ui_renderer->render($form)); + } + + protected function initPropertiesForm(): Form + { + $shift = $this->refinery->custom()->transformation( + fn($v) => array_shift($v) + ); + + $title_and_description = $this->object->getObjectProperties()->getPropertyTitleAndDescription()->toForm( + $this->lng, + $this->ui_factory->input()->field(), + $this->refinery + ); + + $online = $this->object->getObjectProperties()->getPropertyIsOnline()->toForm( + $this->lng, + $this->ui_factory->input()->field(), + $this->refinery + ); + + $settings = $this->ui_factory->input()->field()->section( + [$title_and_description], + $this->lng->txt('iafp_settings') + )->withAdditionalTransformation( + $shift + ); + + $availability = $this->ui_factory->input()->field()->section( + [$online], + $this->lng->txt('iafp_settings_availability') + )->withAdditionalTransformation( + $shift + ); + + return $this->ui_factory->input()->container()->form()->standard( + $this->ctrl->getLinkTargetByClass(self::class, self::CMD_SAVE), + [$settings, $availability] + ); + } + + protected function getTabs(): void + { + $this->tabs_gui->addTab( + self::TAB_INFO, + $this->txt('info_short'), + $this->ctrl->getLinkTargetByClass('ilinfoscreengui', 'showSummary'), + ); + + if ($this->object->getDic()['access']->mayEdit()) { + $this->tabs_gui->addTab( + self::TAB_SETTINGS, + $this->txt('settings'), + $this->ctrl->getLinkTarget($this, self::CMD_EDIT) + ); + $this->tabs_gui->addTab( + self::TAB_FORMS, + $this->txt(self::TAB_FORMS), + $this->ctrl->getLinkTargetByClass('iafpformsgui', IAFPFormsGUI::CMD_LIST) + ); + $this->tabs_gui->addTab( + self::TAB_FIELDS, + $this->txt(self::TAB_FIELDS), + $this->ctrl->getLinkTargetByClass('iafpfieldsgui', IAFPFieldsGUI::CMD_LIST) + ); + } + + /* + $this->tabs_gui->addTab( + self::TAB_EXPORT, + $this->txt('export'), + $this->ctrl->getLinkTargetByClass('ilexportgui', ''), + ); + */ + $this->tabs_gui->addTab( + self::TAB_PERMISSION, + $this->txt('perm_settings'), + $this->ctrl->getLinkTargetByClass('ilpermissiongui', 'perm'), + ); + } + + public function handleAccessViolation(): void + { + $this->error_object->raiseError($this->txt("msg_no_perm_read"), $this->error_object->WARNING); + } + + public static function _goto(string $a_target, string $a_add = ''): void + { + global $DIC; + $a_target = (int) $a_target; + if ($DIC['ilAccess']->checkAccess('write', '', $a_target)) { + ilObjectGUI::_gotoRepositoryNode($a_target, 'edit'); + } + if ($DIC['ilAccess']->checkAccess('read', '', $a_target)) { + ilObjectGUI::_gotoRepositoryNode($a_target); + } + } + + protected function addLocatorItems(): void + { + if (is_object($this->object)) { + $this->locator->addItem( + $this->object->getTitle(), + $this->ctrl->getLinkTarget($this, "view"), + "", + $this->object->getRefId() + ); + } + } + + public function viewObject(): void + { + $this->tabs_gui->activateTab(self::TAB_INFO); + $this->ctrl->setCmd('showSummary'); + $this->ctrl->setCmdClass('ilinfoscreengui'); + $info = $this->buildInfoScreen(); + $this->ctrl->forwardCommand($info); + $this->recordIndividualAssessmentRead(); + } + + public function membersObject(): void + { + $this->tabs_gui->activateTab(self::TAB_MEMBERS); + $gui = $this->object->getMembersGUI(); + $this->ctrl->forwardCommand($gui); + } + + protected function getLinkTarget(string $cmd): string + { + if ($cmd == 'settings') { + return $this->ctrl->getLinkTargetByClass('ilindividualassessmentsettingsgui', 'edit'); + } + if ($cmd == 'info') { + return $this->ctrl->getLinkTarget($this, 'view'); + } + if ($cmd == 'members') { + return $this->ctrl->getLinkTargetByClass('ilindividualassessmentmembersgui', 'view'); + } + return $this->ctrl->getLinkTarget($this, $cmd); + } + + public function editObject(): void + { + $link = $this->getLinkTarget('settings'); + $this->ctrl->redirectToURL($link); + } + + public function getBaseEditForm(): ilPropertyFormGUI + { + return $this->initEditForm(); + } + + protected function afterSave(ilObject $new_object): void + { + $this->tpl->setOnScreenMessage("success", $this->txt("iafp_added"), true); + $this->ctrl->setParameter($this, "ref_id", $new_object->getRefId()); + } + + private function addToNavigationHistory(): void + { + if (!$this->getCreationMode()) { + $ref_id = $this->request_wrapper->retrieve("ref_id", $this->refinery->kindlyTo()->int()); + $link = ilLink::_getLink($ref_id, ilObjIndividualAssessmentFormPool::OBJ_TYPE); + $this->navigation_history->addItem($ref_id, $link, ilObjIndividualAssessmentFormPool::OBJ_TYPE); + } + } + + protected function txt(string $code): string + { + return $this->lng->txt($code); + } +} diff --git a/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolListGUI.php b/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolListGUI.php new file mode 100644 index 000000000000..0cd1041a8520 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/classes/class.ilObjIndividualAssessmentFormPoolListGUI.php @@ -0,0 +1,67 @@ +static_link_enabled = true; + $this->delete_enabled = true; + $this->cut_enabled = true; + $this->copy_enabled = true; + $this->subscribe_enabled = true; + $this->link_enabled = true; + $this->info_screen_enabled = true; + $this->type = "iass"; + $this->gui_class_name = "ilObjIndividualAssessmentFormPoolGUI"; + + + // $this->substitutions = ilAdvancedMDSubstitution::_getInstanceByObjectType($this->type); + // $this->enableSubstitutions($this->substitutions->isActive()); + + // general commands array + $this->commands = ilObjIndividualAssessmentFormPoolAccess::_getCommands(); + } + + /** + * Get command target frame + * + * @param string $cmd command + * + * @return string command target frame + */ + public function getCommandFrame(string $cmd): string + { + return ilFrameTargetInfo::_getFrame("MainContent"); + } + + public function getCommandLink(string $cmd): string + { + switch ($cmd) { + case 'infoScreen': + $return = $this->ctrl->getLinkTargetByClass($this->gui_class_name, "view"); + break; + default: + $return = parent::getCommandLink($cmd); + } + + return $return; + } +} diff --git a/Modules/IndividualAssessmentFormPool/maintenance.json b/Modules/IndividualAssessmentFormPool/maintenance.json new file mode 100644 index 000000000000..f5d64fc18270 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/maintenance.json @@ -0,0 +1,14 @@ +{ + "maintenance_model": "Classic", + "first_maintainer": "rklees(34047)", + "second_maintainer": "", + "implicit_maintainers": [], + "coordinator": [ + "" + ], + "tester": "kunkel(115)", + "testcase_writer": "", + "path": "Modules/IndividualAssessment", + "belong_to_component": "IndividualAssessment", + "used_in_components": [] +} \ No newline at end of file diff --git a/Modules/IndividualAssessmentFormPool/module.xml b/Modules/IndividualAssessmentFormPool/module.xml new file mode 100644 index 000000000000..55cc2bd7bc1f --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/module.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + cat + crs + fold + root + grp + + + diff --git a/Modules/IndividualAssessmentFormPool/test/FieldBuilderMockFactory.php b/Modules/IndividualAssessmentFormPool/test/FieldBuilderMockFactory.php new file mode 100644 index 000000000000..01b9a95793f8 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/test/FieldBuilderMockFactory.php @@ -0,0 +1,73 @@ +createMock(UploadLimitResolver::class), + new \IncrementalSignalGenerator(), + new \ILIAS\Data\Factory(), + $this->getRefinery(), + $this->getLanguage() + ); + } + + protected function getRefinery(): Refinery + { + return new Refinery( + new \ILIAS\Data\Factory(), + $this->getLanguage() + ); + } + + protected function getLanguage(): \ilLanguage + { + return new class () extends \ilLanguage { + public function __construct() + { + } + + public function loadLanguageModule(string $a_module): void + { + } + }; + } + + protected function getFieldBuilder(): FieldBuilder + { + return new FieldBuilder( + $this->getFieldFactory(), + $this->getRefinery(), + $this->getLanguage(), + $this->createMock(UploadHandler::class), + $this->createMock(\ilUIMarkdownPreviewGUI::class), + ); + } +} diff --git a/Modules/IndividualAssessmentFormPool/test/FieldBuilderTest.php b/Modules/IndividualAssessmentFormPool/test/FieldBuilderTest.php new file mode 100644 index 000000000000..9d96756d68c6 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/test/FieldBuilderTest.php @@ -0,0 +1,92 @@ +builder = $this->getFieldBuilder(); + } + + public function fieldBuilderData(): array + { + return [ + [new FieldConfig(FieldType::MARKDOWN), + 'some value', + Field\Markdown::class, + 'some value' + ], + [new FieldConfig(FieldType::FILE), + 'some_id', + Field\File::class, + ['some_id' => 'some_id'] + ], + [new FieldConfig(FieldType::DATETIME), + '1742784914', + Field\DateTime::class, + '2025-03-24 02:55' + ], + [new FieldConfig(FieldType::SINGLESELECT, '', '', null, ['AA', 'BB', 'CC']), + 'BB', + Field\Select::class, + 'BB' + ], + [new FieldConfig(FieldType::TAG, '', '', null, ['a', 'b', 'c']), + implode(\SpecifiedFormStorageDB::VALUE_DELIMITER, ['c' , 'a']), + Field\Tag::class, + ['c' , 'a'] + ], + [new FieldConfig(FieldType::MARKDOWN),null,Field\Markdown::class,''], + [new FieldConfig(FieldType::FILE),null,Field\File::class,[]], + [new FieldConfig(FieldType::DATETIME),null,Field\DateTime::class,''], + [new FieldConfig(FieldType::SINGLESELECT),null,Field\Select::class,''], + [new FieldConfig(FieldType::TAG),null,Field\Tag::class,null], + + ]; + } + + /** + * @dataProvider fieldBuilderData + */ + public function testBuildField( + FieldConfig $cfg, + mixed $value, + string $expected_class, + mixed $expected_value + ): void { + $f = $this->builder->build($cfg, $value); + $this->assertInstanceOf($expected_class, $f); + $this->assertEquals($expected_value, $f->getValue()); + } + +} diff --git a/Modules/IndividualAssessmentFormPool/test/IAFPFormsStorageTest.php b/Modules/IndividualAssessmentFormPool/test/IAFPFormsStorageTest.php new file mode 100644 index 000000000000..242ba0b02230 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/test/IAFPFormsStorageTest.php @@ -0,0 +1,71 @@ +createMock(\ilDBInterface::class); + $ildb->method('nextId') + ->willReturn(-2); + $this->db = new FormsStorageDB( + $ildb, + $this->createMock(\ilAccess::class) + ); + } + + public function testIAFPGetNewForm(): void + { + $form = $this->db->getNewForm(777); + $this->assertInstanceOf(Form::class, $form); + $this->assertEquals(777, $form->getObjId()); + $this->assertEquals(-1, $form->getFormId()); + } + + public function getFieldTypes(): array + { + return array_map( + fn($t) => [$t], + FieldType::cases() + ); + } + + /** + * @dataProvider getFieldTypes + */ + public function testIAFPCreateField(FieldType $type): void + { + $field = $this->db->createField($type, 777); + $this->assertInstanceOf(FieldConfig::class, $field->getConfig()); + $this->assertEquals(-2, $field->getFieldId()); //next id + $this->assertEquals(777, $field->getObjId()); + $this->assertEquals($type, $field->getConfig()->getType()); + } + +} diff --git a/Modules/IndividualAssessmentFormPool/test/ilModulesIndividualAssessmentFormPoolSuite.php b/Modules/IndividualAssessmentFormPool/test/ilModulesIndividualAssessmentFormPoolSuite.php new file mode 100644 index 000000000000..970a7803c8a4 --- /dev/null +++ b/Modules/IndividualAssessmentFormPool/test/ilModulesIndividualAssessmentFormPoolSuite.php @@ -0,0 +1,36 @@ +addTestSuite('FieldBuilderTest'); + $suite->addTestSuite('IAFPFormsStorageTest'); + return $suite; + } +} diff --git a/Services/Repository/Administration/class.ilObjRepositorySettings.php b/Services/Repository/Administration/class.ilObjRepositorySettings.php index 2cf866f6bec5..9efdb4e85327 100644 --- a/Services/Repository/Administration/class.ilObjRepositorySettings.php +++ b/Services/Repository/Administration/class.ilObjRepositorySettings.php @@ -250,7 +250,7 @@ public static function getDefaultNewItemGrouping(): array "breaker1" => null, "content" => ["file", "webr", "feed", "copa", "wiki", "blog", "lm", "htlm", "sahs", 'cmix', 'lti', "lso", "glo", "dcl", "bibl", "mcst", "mep"], "breaker2" => null, - "assessment" => ["exc", "tst", "qpl", "iass"], + "assessment" => ["exc", "tst", "qpl", "iass", 'iafp'], "feedback" => ["poll", "svy", "spl"], "templates" => ["prtt"] ]; diff --git a/Services/Repository/classes/class.ilRepositoryGUI.php b/Services/Repository/classes/class.ilRepositoryGUI.php index 52c96ffc960b..2d65a88ffd1a 100755 --- a/Services/Repository/classes/class.ilRepositoryGUI.php +++ b/Services/Repository/classes/class.ilRepositoryGUI.php @@ -41,6 +41,7 @@ * @ilCtrl_Calls ilRepositoryGUI: ilObjLTIConsumerGUI * @ilCtrl_Calls ilRepositoryGUI: ilObjCmiXapiGUI * @ilCtrl_Calls ilRepositoryGUI: ilPermissionGUI + * @ilCtrl_Calls ilRepositoryGUI: ilObjIndividualAssessmentFormPoolGUI * */ class ilRepositoryGUI implements ilCtrlBaseClassInterface diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 629f6f4da4ba..7378b9444af5 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -4301,6 +4301,7 @@ common#:#http_not_possible#:#Dieser Server unterstützt keine HTTP-Anfragen. common#:#http_path#:#HTTP-Pfad common#:#https_not_possible#:#Dieser Server unterstützt keine HTTPS-Verbindungen. common#:#i2passwd#:#ILIAS_2-Passwort +common#:#iafp_new#:#Add Individual Assessment Form Pool common#:#iass_import#:#Individuelle Bewertung importieren common#:#iass_new#:#Neue Individuelle Bewertung anlegen common#:#icon#:#Symbol @@ -4930,6 +4931,7 @@ common#:#obj_hlps#:#Online-Hilfe common#:#obj_hlps_desc#:#Einstellungen der Online-Hilfe common#:#obj_htlm#:#Lernmodul HTML common#:#obj_htlm_duplicate#:#HTML Lernmodul kopieren +common#:#obj_iafp#:#Individual Assessment Form Pool common#:#obj_iass#:#Individuelle Bewertung common#:#obj_iass_duplicate#:#Individuelle Bewertung kopieren common#:#obj_iass_select#:#-- Wählen Sie eine Individuelle Bewertung aus -- @@ -5138,6 +5140,7 @@ common#:#objs_glo#:#Glossare common#:#objs_grp#:#Gruppen common#:#objs_grpr#:#Gruppenlinks common#:#objs_htlm#:#HTML-Lernmodule +common#:#objs_iafp#:#Individual Assessment Form Pool common#:#objs_iass#:#Individuelle Bewertungen common#:#objs_itgr#:#Objekteblöcke common#:#objs_lm#:#ILIAS-Lernmodule @@ -10394,6 +10397,52 @@ help#:#help_tooltips_and_help#:#Tooltips und Handlungsanweisungen help#:#help_tooltips_only#:#Nur Tooltips help#:#help_topcis#:#Themen help#:#help_tt_text#:#Text +iafp#:#_settings_saved#:#Settings saved. +iafp#:#add_fields#:#Add Fields to the Form +iafp#:#available_for_examiners#:#available for examiners +iafp#:#confirm_cancel#:#Cancel +iafp#:#confirm_delete#:#Delete +iafp#:#default_value#:#Default Value +iafp#:#field_available_for_examiners#:#Available for Examiners +iafp#:#field_available_for_examiners_byline#:#should the field be available for examiners? +iafp#:#field_default_value#:#Default Value +iafp#:#field_default_value_byline#:#default/initial value +iafp#:#field_description#:#Description +iafp#:#field_description_byline#:#description of the field +iafp#:#field_label#:#Label +iafp#:#field_label_byline#:#label for the field +iafp#:#field_name_byline#:#name for the field +iafp#:#field_options#:#Options +iafp#:#field_options_byline#:#add options the user may choose from +iafp#:#field_section_byline#:#configure the field +iafp#:#field_section_create#:#Edit new Field +iafp#:#field_section_edit#:#Edit Field +iafp#:#field_type_byline#:#type of the field +iafp#:#field_with_notes#:#With Notes +iafp#:#field_with_notes_byline#:#add a textarea for notes? +iafp#:#fields#:#Fields in this Form +iafp#:#fields_added#:#Fields added. +iafp#:#fields_byline#:#Unselect and save to remove a field +iafp#:#fields_count#:#amount of fields +iafp#:#form_description#:#Description +iafp#:#form_description_byline#:#description of the form +iafp#:#form_name#:#Name +iafp#:#form_name_byline#:#name of the form +iafp#:#form_section_byline#:#configure the form +iafp#:#form_section_create#:#Edit new Form +iafp#:#form_section_edit#:#Edit Form +iafp#:#iafp_fields#:#Fields +iafp#:#iafp_forms#:#Forms +iafp#:#iafp_settings#:#Settings +iafp#:#iafp_settings_availability#:#Availability +iafp#:#label#:#label +iafp#:#new_field#:#Add a new Field +iafp#:#new_form#:#Add a new Form +iafp#:#object_saved#:#Saved. +iafp#:#pick_fields#:#Select Fields +iafp#:#tab_fields#:#Fields +iafp#:#tab_forms#:#Forms +iafp#:#with_notes#:#Notes iass#:#download_assessment_paper#:#Prüfungsblatt herunterladen iass#:#general#:#Allgemein iass#:#grading#:#Bewertung @@ -14059,6 +14108,12 @@ rbac#:#htlm_read#:#HTML-Lernmodul lesen rbac#:#htlm_read_learning_progress#:#Benutzer kann Lernfortschritt anderer Benutzer einsehen rbac#:#htlm_visible#:#HTML-Lernmodul ist sichtbar rbac#:#htlm_write#:#HTML-Lernmodul bearbeiten +rbac#:#iafp_copy#:#Copy an Individual Assessment Form Pool +rbac#:#iafp_delete#:#Delete an Individual Assessment Form Pool +rbac#:#iafp_edit_permission#:#Edit permissions of an Individual Assessment Form Pool +rbac#:#iafp_read#:#Read content of an Individual Assessment Form Pool +rbac#:#iafp_visible#:#Individual Assessment Form Pool is visible +rbac#:#iafp_write#:#Edit an Individual Assessment Form Pool rbac#:#iass_amend_grading#:#Finalisierte Bewertung der Individuellen Bewertung ändern rbac#:#iass_copy#:#Individuelle Bewertung kopieren rbac#:#iass_delete#:#Individuelle Bewertung löschen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 334c82a11491..618a038fca73 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -4301,6 +4301,7 @@ common#:#http_not_possible#:#This server is not supporting http requests. common#:#http_path#:#HTTP Path common#:#https_not_possible#:#This server is not supporting HTTPS connections. common#:#i2passwd#:#ILIAS 2 password +common#:#iafp_new#:#Add Individual Assessment Form Pool common#:#iass_import#:#Import Individual Assessment common#:#iass_new#:#Create Individual Assessment common#:#icon#:#Icon @@ -4930,6 +4931,7 @@ common#:#obj_hlps#:#Help System common#:#obj_hlps_desc#:#Settings for the online help common#:#obj_htlm#:#Learning Module HTML common#:#obj_htlm_duplicate#:#Copy HTML Learning Module +common#:#obj_iafp#:#Individual Assessment Form Pool common#:#obj_iass#:#Individual Assessment common#:#obj_iass_duplicate#:#Copy Individual Assessment common#:#obj_iass_select#:#-- Please select one individual assessment -- @@ -5138,6 +5140,7 @@ common#:#objs_glo#:#Glossaries common#:#objs_grp#:#Groups common#:#objs_grpr#:#Group Links common#:#objs_htlm#:#HTML Learning Modules +common#:#objs_iafp#:#Individual Assessment Form Pool common#:#objs_iass#:#Individual Assessments common#:#objs_itgr#:#Item Groups common#:#objs_lm#:#ILIAS Learning Modules @@ -10394,6 +10397,52 @@ help#:#help_tooltips_and_help#:#Tooltips and Workflow Help help#:#help_tooltips_only#:#Tooltips Only help#:#help_topcis#:#Topics help#:#help_tt_text#:#Text +iafp#:#_settings_saved#:#Settings saved. +iafp#:#add_fields#:#Add Fields to the Form +iafp#:#available_for_examiners#:#available for examiners +iafp#:#confirm_cancel#:#Cancel +iafp#:#confirm_delete#:#Delete +iafp#:#default_value#:#Default Value +iafp#:#field_available_for_examiners#:#Available for Examiners +iafp#:#field_available_for_examiners_byline#:#should the field be available for examiners? +iafp#:#field_default_value#:#Default Value +iafp#:#field_default_value_byline#:#default/initial value +iafp#:#field_description#:#Description +iafp#:#field_description_byline#:#description of the field +iafp#:#field_label#:#Label +iafp#:#field_label_byline#:#label for the field +iafp#:#field_name_byline#:#name for the field +iafp#:#field_options#:#Options +iafp#:#field_options_byline#:#add options the user may choose from +iafp#:#field_section_byline#:#configure the field +iafp#:#field_section_create#:#Edit new Field +iafp#:#field_section_edit#:#Edit Field +iafp#:#field_type_byline#:#type of the field +iafp#:#field_with_notes#:#With Notes +iafp#:#field_with_notes_byline#:#add a textarea for notes? +iafp#:#fields#:#Fields in this Form +iafp#:#fields_added#:#Fields added. +iafp#:#fields_byline#:#Unselect and save to remove a field +iafp#:#fields_count#:#amount of fields +iafp#:#form_description#:#Description +iafp#:#form_description_byline#:#description of the form +iafp#:#form_name#:#Name +iafp#:#form_name_byline#:#name of the form +iafp#:#form_section_byline#:#configure the form +iafp#:#form_section_create#:#Edit new Form +iafp#:#form_section_edit#:#Edit Form +iafp#:#iafp_fields#:#Fields +iafp#:#iafp_forms#:#Forms +iafp#:#iafp_settings#:#Settings +iafp#:#iafp_settings_availability#:#Availability +iafp#:#label#:#label +iafp#:#new_field#:#Add a new Field +iafp#:#new_form#:#Add a new Form +iafp#:#object_saved#:#Saved. +iafp#:#pick_fields#:#Select Fields +iafp#:#tab_fields#:#Fields +iafp#:#tab_forms#:#Forms +iafp#:#with_notes#:#Notes iass#:#download_assessment_paper#:#Download Record file iass#:#general#:#General Information iass#:#grading#:#Grading @@ -14059,6 +14108,12 @@ rbac#:#htlm_read#:#User can read HTML learning module rbac#:#htlm_read_learning_progress#:#User can view learning progress of other users rbac#:#htlm_visible#:#HTML learning module is visible rbac#:#htlm_write#:#User can edit content and settings of HTML learning module +rbac#:#iafp_copy#:#Copy an Individual Assessment Form Pool +rbac#:#iafp_delete#:#Delete an Individual Assessment Form Pool +rbac#:#iafp_edit_permission#:#Edit permissions of an Individual Assessment Form Pool +rbac#:#iafp_read#:#Read content of an Individual Assessment Form Pool +rbac#:#iafp_visible#:#Individual Assessment Form Pool is visible +rbac#:#iafp_write#:#Edit an Individual Assessment Form Pool rbac#:#iass_amend_grading#:#Amend gradings in an Individual Assessment rbac#:#iass_copy#:#Copy an Individual Assessment rbac#:#iass_delete#:#Delete an Individual Assessment diff --git a/templates/default/images/standard/icon_iafp.svg b/templates/default/images/standard/icon_iafp.svg new file mode 100644 index 000000000000..90fdfcc67519 --- /dev/null +++ b/templates/default/images/standard/icon_iafp.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + +