refactor: split the questionnaire into teamer and bus feedback
This commit is contained in:
1 parent
2f4f9800dd
commit
a0170f5952
35 files changed
+1114
-554
No files matched your search
@@ -7,7 +7,6 @@ namespace App\Tests\Config;
|
||||
use App\Config\QuestionnaireCatalog;
|
||||
use App\Enum\ChoiceWidget;
|
||||
use App\Enum\ConditionOperator;
|
||||
use App\Enum\QuestionnaireFlag;
|
||||
use App\Enum\QuestionType;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -47,22 +46,20 @@ class QuestionnaireCatalogTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What the disposition already knows was decided not to be asked; it is snapshotted into the
|
||||
* response instead. These are the Google forms' keys for those questions.
|
||||
*/
|
||||
public function testTeamerAssignmentDoesNotAskWhatTheDispositionKnows(): void
|
||||
public function testTheTwoQuestionnairesAreTheCurrentKeys(): void
|
||||
{
|
||||
$definition = new QuestionnaireCatalog(self::SHIPPED_DIR)->get('teamer_assignment');
|
||||
|
||||
foreach (['name', 'job_profile', 'house', 'assignment_start', 'travel_date', 'destination', 'guide_name', 'first_pickup_location'] as $key) {
|
||||
$this->assertNull($definition->getQuestion($key), $key);
|
||||
}
|
||||
$this->assertSame(
|
||||
[QuestionnaireCatalog::BUS_FEEDBACK, QuestionnaireCatalog::TEAMER_FEEDBACK],
|
||||
new QuestionnaireCatalog(self::SHIPPED_DIR)->keys(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testTeamerAssignmentLowRatingReasonIsRequiredOnlyForLowRatings(): void
|
||||
public function testLowRatingReasonIsRequiredOnlyForLowRatings(): void
|
||||
{
|
||||
$question = new QuestionnaireCatalog(self::SHIPPED_DIR)->get('teamer_assignment')->getQuestion('office_briefing_low_reason');
|
||||
$question = new QuestionnaireCatalog(self::SHIPPED_DIR)
|
||||
->get(QuestionnaireCatalog::TEAMER_FEEDBACK)
|
||||
->getQuestion('office_briefing_low_reason')
|
||||
;
|
||||
|
||||
$this->assertTrue($question->required);
|
||||
$this->assertSame('office_briefing_rating', $question->showIf->question);
|
||||
@@ -74,9 +71,9 @@ class QuestionnaireCatalogTest extends TestCase
|
||||
* Google's single "Bitte ankreuzen:" checkbox list allowed "WC defekt" and "WC funktionstüchtig"
|
||||
* together; it was split into one single choice per topic.
|
||||
*/
|
||||
public function testTeamerAssignmentEquipmentIsOneSingleChoicePerTopic(): void
|
||||
public function testBusEquipmentIsOneSingleChoicePerTopic(): void
|
||||
{
|
||||
$definition = new QuestionnaireCatalog(self::SHIPPED_DIR)->get('teamer_assignment');
|
||||
$definition = new QuestionnaireCatalog(self::SHIPPED_DIR)->get(QuestionnaireCatalog::BUS_FEEDBACK);
|
||||
|
||||
foreach (['video', 'wc', 'luggage', 'smoking', 'snow_chains'] as $key) {
|
||||
$question = $definition->getQuestion($key);
|
||||
@@ -85,26 +82,12 @@ class QuestionnaireCatalogTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every teamer gets the questionnaire; only a bus escort gets the bus part, and nobody gets a
|
||||
* bus section without it.
|
||||
*/
|
||||
public function testTeamerAssignmentBusPartIsOnlyForBusEscorts(): void
|
||||
public function testArchivedVersionsAreNotKeys(): void
|
||||
{
|
||||
$definition = new QuestionnaireCatalog(self::SHIPPED_DIR)->get(QuestionnaireCatalog::TEAMER_ASSIGNMENT);
|
||||
$this->write('sample', ['version' => 2] + $this->sample());
|
||||
$this->write('sample.v1', $this->sample());
|
||||
|
||||
foreach ($definition->sections as $section) {
|
||||
$this->assertSame(
|
||||
str_starts_with($section->key, 'bus_') ? QuestionnaireFlag::BUS_ESCORT : null,
|
||||
$section->onlyFor,
|
||||
$section->key,
|
||||
);
|
||||
}
|
||||
|
||||
$withoutBus = $definition->forFlags([]);
|
||||
$this->assertNull($withoutBus->getQuestion('bus_company'));
|
||||
$this->assertNotNull($withoutBus->getQuestion('office_briefing_rating'));
|
||||
$this->assertNotNull($definition->forFlags([QuestionnaireFlag::BUS_ESCORT])->getQuestion('bus_company'));
|
||||
$this->assertSame(['sample'], new QuestionnaireCatalog($this->dir)->keys());
|
||||
}
|
||||
|
||||
public function testChoiceDefaultsToRadio(): void
|
||||
@@ -204,41 +187,12 @@ class QuestionnaireCatalogTest extends TestCase
|
||||
static fn (array $d): array => self::withQuestion(self::withQuestion($d, 0, ['type' => 'rating', 'options' => null]), 1, ['show_if' => ['question' => 'answer', 'lte' => 6]]),
|
||||
'lte needs a rating from 1 to 5',
|
||||
];
|
||||
yield 'unknown only_for' => [
|
||||
static fn (array $d): array => self::withSection($d, ['only_for' => 'ferry']),
|
||||
'unknown only_for "ferry"',
|
||||
];
|
||||
yield 'key does not match the file name' => [
|
||||
static fn (array $d): array => ['key' => 'other'] + $d,
|
||||
'does not match the file name',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A question everyone gets cannot depend on a bus-only question: without the bus part it
|
||||
* would never be shown.
|
||||
*/
|
||||
public function testQuestionCannotDependOnAQuestionOnlyAskedWithAFlag(): void
|
||||
{
|
||||
$definition = $this->sample();
|
||||
$definition['sections'][0]['only_for'] = 'bus_escort';
|
||||
$definition['sections'][] = [
|
||||
'key' => 'later',
|
||||
'questions' => [[
|
||||
'key' => 'other_reason',
|
||||
'label' => 'Warum?',
|
||||
'type' => 'text',
|
||||
'show_if' => ['question' => 'answer', 'equals' => 'yes'],
|
||||
]],
|
||||
];
|
||||
$this->write('sample', $definition);
|
||||
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
$this->expectExceptionMessage('"answer" is only asked for bus_escort');
|
||||
|
||||
new QuestionnaireCatalog($this->dir)->get('sample');
|
||||
}
|
||||
|
||||
public function testArchivedFileWithTheWrongVersionIsRejected(): void
|
||||
{
|
||||
$this->write('sample', ['version' => 3] + $this->sample());
|
||||
@@ -290,13 +244,6 @@ class QuestionnaireCatalogTest extends TestCase
|
||||
return $definition;
|
||||
}
|
||||
|
||||
private static function withSection(array $definition, array $fields): array
|
||||
{
|
||||
$definition['sections'][0] = $fields + $definition['sections'][0];
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
private function write(string $name, array $definition): void
|
||||
{
|
||||
file_put_contents(sprintf('%s/%s.yaml', $this->dir, $name), Yaml::dump($definition, 10));
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Config;
|
||||
|
||||
use App\Config\QuestionnaireCatalog;
|
||||
use App\Entity\Application;
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\Teamer;
|
||||
use App\Service\Questionnaire\DispositionContext;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Every question of the two Google forms the questionnaires replace ("RL-Feedback 25-26" and
|
||||
* "Bus-Feedback 25-26") is either asked or answered from the placement - none may get lost when
|
||||
* a definition is edited.
|
||||
*
|
||||
* The lists below are the Google forms' questions in their order, with what each became:
|
||||
* "question:<key>" is asked, "context:<field>" is taken from the disposition and snapshotted into
|
||||
* the response (DispositionContext::build()). Google's "Bitte ankreuzen:" checkbox list was split
|
||||
* into five single-choice questions, so it maps to five.
|
||||
*/
|
||||
class QuestionnaireCompletenessTest extends TestCase
|
||||
{
|
||||
private const array TEAMER_FEEDBACK = [
|
||||
'Name' => ['context:teamerName'],
|
||||
'Jobprofil' => ['context:jobProfileName'],
|
||||
'Haus' => ['context:hotel'],
|
||||
'Einsatzzeitraum (Anreisetag)' => ['context:dateFrom'],
|
||||
'Wie gut hast du dich vorab vom Büro gebrieft gefühlt?' => ['question:office_briefing_rating'],
|
||||
'Wenn 1 - 3 Sterne Bewertung, warum?' => ['question:office_briefing_low_reason'],
|
||||
'War das Wochenteam-Seminar hilfreich für dich? Warst du bzw. wurdest du gut vorbereitet?' => ['question:team_week_seminar'],
|
||||
'Wie gut lief die Reiseorganisation vor Ort?' => ['question:onsite_organisation_rating'],
|
||||
'Gab es organisatorische Probleme?' => ['question:had_organisational_problems'],
|
||||
'Wenn ja, welche?' => ['question:organisational_problems'],
|
||||
'Wie lief die Kommunikation & Zusammenarbeit mit den E&P Teamer:innen vor Ort während der Reise?' => ['question:teamer_collaboration_rating'],
|
||||
'Welche Unterstützung hättest du dir zusätzlich gewünscht?' => ['question:support_wished'],
|
||||
'Wie ist deine Hausleitung mit Problemen vor Ort umgegangen?' => ['question:house_manager_problem_handling'],
|
||||
'Gab es Momente, in denen du dich überfordert gefühlt hast?' => ['question:felt_overwhelmed'],
|
||||
'Wenn ja, wodurch?' => ['question:overwhelmed_reason'],
|
||||
'Was hättest du dir anders oder besser gewünscht?' => ['question:wished_differently'],
|
||||
'Würdest du wieder als Reiseleitung für E&P arbeiten?' => ['question:would_work_again'],
|
||||
'Wenn nein, warum nicht?' => ['question:would_not_work_again_reason'],
|
||||
'Du hast nun schon vieles beantwortet, aber nirgendwo konntest du folgende Aspekte loswerden:' => ['question:anything_else'],
|
||||
];
|
||||
|
||||
private const array BUS_FEEDBACK = [
|
||||
'(Datum, ohne Titel)' => ['context:escortPickupDate', 'context:dateTo'],
|
||||
'Hin- oder Rückfahrt' => ['question:direction'],
|
||||
'Reiseziel:' => ['context:destinationProduct'],
|
||||
'Bus-Unternehmen:' => ['question:bus_company'],
|
||||
'Name des Busunternehmens:' => ['question:bus_company_other'],
|
||||
'Busfahrer-Namen:' => ['question:driver_names'],
|
||||
'Reiseleitung:' => ['context:teamerName'],
|
||||
'1. Abfahrtsort' => ['context:escortPickup', 'context:hotel'],
|
||||
'Bus pünktlich am 1. Abfahrtsort?' => ['question:on_time_at_first_pickup'],
|
||||
'Wieviel Verspätung hatte der Bus am 1. Abfahrtsort?' => ['question:delay_at_first_pickup'],
|
||||
'Gesamteindruck des Busses' => ['question:overall_impression'],
|
||||
'Alter des Fahrzeugs' => ['question:vehicle_age'],
|
||||
'Sitzkomfort im Bus' => ['question:seating_comfort'],
|
||||
'Freundlichkeit der Fahrer' => ['question:driver_friendliness'],
|
||||
'Pausenzeiten' => ['question:breaks'],
|
||||
'Temperatur im Bus' => ['question:temperature'],
|
||||
'Verhalten der Gäste im Bus' => ['question:guest_behaviour'],
|
||||
'Waren den Fahrern alle Abfahrtsorte bekannt?' => ['question:drivers_knew_pickups'],
|
||||
'Welche Abfahrtsorte waren nicht bekannt? Gab es dadurch Probleme?' => ['question:unknown_pickups'],
|
||||
'War den Busfahrern der Zielort und die Ausladestelle bekannt?' => ['question:drivers_knew_destination'],
|
||||
'Bitte ankreuzen:' => ['question:video', 'question:wc', 'question:luggage', 'question:smoking', 'question:snow_chains'],
|
||||
'Entstanden während der Fahrt Schäden am Bus?' => ['question:bus_damage'],
|
||||
'Was ist passiert?' => ['question:damage_description'],
|
||||
'Das ging gar nicht / ist besonders hervorzuheben:' => ['question:bus_remarks'],
|
||||
];
|
||||
|
||||
public function testEveryTeamerFeedbackQuestionIsAskedOrDerived(): void
|
||||
{
|
||||
$this->assertComplete(QuestionnaireCatalog::TEAMER_FEEDBACK, self::TEAMER_FEEDBACK);
|
||||
}
|
||||
|
||||
public function testEveryBusFeedbackQuestionIsAskedOrDerived(): void
|
||||
{
|
||||
$this->assertComplete(QuestionnaireCatalog::BUS_FEEDBACK, self::BUS_FEEDBACK);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Google forms had 19 and 24 questions; with the checkbox list split there are 43 places
|
||||
* an answer comes from.
|
||||
*/
|
||||
public function testTheMappingCoversBothGoogleForms(): void
|
||||
{
|
||||
$this->assertCount(19, self::TEAMER_FEEDBACK);
|
||||
$this->assertCount(24, self::BUS_FEEDBACK);
|
||||
}
|
||||
|
||||
/**
|
||||
* A bus escort answers every question of both forms; anyone else every question of the
|
||||
* teamer form.
|
||||
*/
|
||||
public function testABusEscortGetsBothQuestionnairesAndEveryoneTheTeamerOne(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[QuestionnaireCatalog::TEAMER_FEEDBACK, QuestionnaireCatalog::BUS_FEEDBACK],
|
||||
DispositionContext::questionnaires((new Assignment())->setPickup(4711)),
|
||||
);
|
||||
$this->assertSame(
|
||||
[QuestionnaireCatalog::TEAMER_FEEDBACK],
|
||||
DispositionContext::questionnaires((new Assignment())->setPickup(0)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, list<string>> $googleQuestions
|
||||
*/
|
||||
private function assertComplete(string $questionnaire, array $googleQuestions): void
|
||||
{
|
||||
$definition = new QuestionnaireCatalog(__DIR__.'/../../config/questionnaires')->get($questionnaire);
|
||||
$contextFields = array_keys(DispositionContext::build(new Disposition(new Application(new Assignment(), new Teamer()))));
|
||||
$mapped = [];
|
||||
|
||||
foreach ($googleQuestions as $label => $targets) {
|
||||
foreach ($targets as $target) {
|
||||
[$kind, $key] = explode(':', $target);
|
||||
|
||||
if ('question' === $kind) {
|
||||
$this->assertNotNull($definition->getQuestion($key), sprintf('"%s" is not asked as "%s"', $label, $key));
|
||||
$mapped[] = $key;
|
||||
} else {
|
||||
$this->assertContains($key, $contextFields, sprintf('"%s" is not snapshotted as "%s"', $label, $key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// and nothing is asked that the Google form did not ask
|
||||
$this->assertSame([], array_values(array_diff(array_keys($definition->getQuestions()), $mapped)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user