Skip to content

fix(forms): Refactoring migration logic to skip orphan elements messages, and improve default value handling for some question types #19474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 82 additions & 19 deletions phpunit/functional/Glpi/Form/Migration/FormMigrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
use Glpi\Form\QuestionType\QuestionTypesManager;
use Glpi\Form\QuestionType\QuestionTypeUrgency;
use Glpi\Form\Section;
use Glpi\Message\MessageType;
use Glpi\Migration\PluginMigrationResult;
use Glpi\Tests\FormTesterTrait;
use Location;
Expand Down Expand Up @@ -919,15 +918,11 @@ public function testFormMigrationWithOrphanSection(): void
$this->setPrivateProperty($migration, 'result', $result);
$this->assertTrue($this->callPrivateMethod($migration, 'processMigration'));

$errors = array_filter(
$result->getMessages(),
static fn (array $entry) => $entry['type'] === MessageType::Error
);
$this->assertCount(1, $errors);
$this->assertEquals(
current($errors)['message'],
'Section "Orphan section" has no form. It will not be migrated.'
);
// Check that the section hasn't been migrated
$section = new Section();
$this->assertFalse($section->getFromDBByCrit([
'name' => 'Orphan section',
]));
}

public function testFormMigrationWithOrphanQuestion(): void
Expand All @@ -950,15 +945,11 @@ public function testFormMigrationWithOrphanQuestion(): void
$this->setPrivateProperty($migration, 'result', $result);
$this->assertTrue($this->callPrivateMethod($migration, 'processMigration'));

$errors = array_filter(
$result->getMessages(),
static fn (array $entry) => $entry['type'] === MessageType::Error
);
$this->assertCount(1, $errors);
$this->assertEquals(
current($errors)['message'],
'Question "Orphan question" has no section. It will not be migrated.'
);
// Check that the question hasn't been migrated
$question = new Question();
$this->assertFalse($question->getFromDBByCrit([
'name' => 'Orphan question',
]));
}

public function testFormMigrationUpdateHorizontalRanks(): void
Expand Down Expand Up @@ -1165,6 +1156,78 @@ public function testFormMigrationTagConversion(string $rawContent, string $expec
);
}

public function testFormMigrationWithRadioQuestion(): void
{
/**
* @var \DBmysql $DB
*/
global $DB;

// Insert a new form
$this->assertTrue($DB->insert(
'glpi_plugin_formcreator_forms',
[
'name' => 'Test form migration for radio question',
]
));

// Insert a new section
$this->assertTrue($DB->insert(
'glpi_plugin_formcreator_sections',
[
'name' => 'Test form migration for radio question - Section',
'plugin_formcreator_forms_id' => $DB->insertId()
]
));

// Insert a new question with type "radio"
$section_id = $DB->insertId();
$this->assertTrue($DB->insert(
'glpi_plugin_formcreator_questions',
[
'plugin_formcreator_sections_id' => $section_id,
'name' => 'Test form migration for radio question - Question',
'fieldtype' => 'radios',
'default_values' => '1',
'values' => json_encode(['1', '2', '3']),
]
));

$migration = new FormMigration($DB, FormAccessControlManager::getInstance());
$this->setPrivateProperty($migration, 'result', new PluginMigrationResult());
$this->assertTrue($this->callPrivateMethod($migration, 'processMigration'));

// Check that the question has been migrated
/** @var Question $question */
$question = getItemByTypeName(Question::class, 'Test form migration for radio question - Question');
$this->assertInstanceOf(Question::class, $question);

/** @var QuestionTypeRadio $question_type */
$question_type = $question->getQuestionType();
$this->assertInstanceOf(QuestionTypeRadio::class, $question_type);

$this->assertEquals(
[
[
'uuid' => 0,
'value' => '1',
'checked' => true,
],
[
'uuid' => 1,
'value' => '2',
'checked' => false,
],
[
'uuid' => 2,
'value' => '3',
'checked' => false,
]
],
$question_type->getValues($question)
);
}

public static function provideFormMigrationConditionsForQuestions(): iterable
{
yield 'QuestionTypeShortText - Always visible' => [
Expand Down
69 changes: 44 additions & 25 deletions src/Glpi/Form/Migration/FormMigration.php
Original file line number Diff line number Diff line change
Expand Up @@ -447,11 +447,10 @@ private function processMigrationOfSections(): void
'PluginFormcreatorForm',
$raw_section['plugin_formcreator_forms_id']
)['items_id'] ?? 0;

// If the form ID is 0, it means the form does not exist
if ($form_id === 0) {
$this->result->addMessage(MessageType::Error, sprintf(
'Section "%s" has no form. It will not be migrated.',
$raw_section['name']
));
// No need to warn the user, as this section was not visible in formcreator anyway.
continue;
}

Expand Down Expand Up @@ -509,11 +508,10 @@ private function processMigrationOfQuestions(): void
'PluginFormcreatorSection',
$raw_question['plugin_formcreator_sections_id']
)['items_id'] ?? 0;

// If the section ID is 0, it means the section does not exist
if ($section_id === 0) {
$this->result->addMessage(MessageType::Error, sprintf(
'Question "%s" has no section. It will not be migrated.',
$raw_question['name']
));
// No need to warn the user, as this question was not visible in formcreator anyway.
continue;
}

Expand Down Expand Up @@ -544,20 +542,34 @@ private function processMigrationOfQuestions(): void
'uuid' => $raw_question['uuid']
], fn ($value) => $value !== null);

$question = $this->importItem(
Question::class,
$data,
[
'uuid' => $raw_question['uuid'],
]
);
try {
$question = $this->importItem(
Question::class,
$data,
[
'uuid' => $raw_question['uuid'],
]
);

$this->mapItem(
'PluginFormcreatorQuestion',
$raw_question['id'],
Question::class,
$question->getID()
);
$this->mapItem(
'PluginFormcreatorQuestion',
$raw_question['id'],
Question::class,
$question->getID()
);
} catch (\Throwable $th) {
$section = Section::getById($section_id) ?: null;
$this->result->addMessage(
MessageType::Error,
sprintf(
__('Error while importing question "%s" in section "%s" and form "%s": %s'),
$raw_question['name'],
$section?->getName(),
$section?->getItem()?->getName(),
$th->getMessage()
)
);
}

$this->progress_indicator?->advance();
}
Expand Down Expand Up @@ -915,8 +927,11 @@ private function processMigrationOfDestination(
)['items_id'] ?? 0;

$form = new Form();

// If the form is invalid, skip this target
if (!$form->getFromDB($form_id)) {
throw new LogicException("Form with id {$raw_target['plugin_formcreator_forms_id']} not found");
// No need to warn the user, as this destination was not visible in formcreator anyway.
continue;
}

$fields_config = [];
Expand All @@ -934,9 +949,10 @@ private function processMigrationOfDestination(
$this->result->addMessage(
MessageType::Error,
sprintf(
__('The "%s" destination field configuration of the form "%s" cannot be imported.'),
__('The "%s" destination field configuration of the form "%s" cannot be imported : %s'),
$configurable_field->getLabel(),
$form->getName()
$form->getName(),
$th->getMessage()
)
);
}
Expand Down Expand Up @@ -1018,8 +1034,11 @@ private function processMigrationOfITILActorsFields(
$raw_target_actor['items_id']
)['items_id'] ?? 0;

// The destination ID is 0 if the target was not migrated or not existing.
// In this case, we skip the actor
if ($target_id === 0) {
throw new LogicException("Destination for target id {$raw_target_actor['items_id']} not found");
// No need to warn the user, as this actor config was not visible in formcreator anyway.
continue;
}

$targets_actors[$target_id][$raw_target_actor['actor_role']][$raw_target_actor['actor_type']][] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,15 @@ public function convertDefaultValue(array $rawData): array
return [];
}

/**
* New default values format require an array of values.
* The old system did not use an array if there was only one element.
*/
$default_values = json_decode($rawData['default_values']);
if ($default_values === null && json_last_error() !== JSON_ERROR_NONE) {
if (
($default_values === null && json_last_error() !== JSON_ERROR_NONE)
|| !is_array($default_values)
) {
$default_values = [$rawData['default_values']];
}

Expand Down
6 changes: 5 additions & 1 deletion src/Glpi/Form/QuestionType/QuestionTypeUrgency.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ public function getConditionHandlers(
#[Override]
public function convertDefaultValue(array $rawData): ?int
{
return $rawData['default_values'] ?? null;
if (!isset($rawData['default_values'])) {
return null;
}

return (int) $rawData['default_values'];
}

#[Override]
Expand Down