feat(demo): add tenant demo data generator
Add a relaunchable demo seed flow so a tenant can be populated quickly on a VPS or demo environment without manual setup.
This commit is contained in:
@@ -42,6 +42,13 @@ La configuration associee est deja prete dans :
|
||||
- `deploy/vps/generate-env.sh`
|
||||
- `deploy/vps/Caddyfile`
|
||||
- `deploy/vps/generate-jwt.sh`
|
||||
- `deploy/vps/generate-demo-data.sh`
|
||||
|
||||
Pour remplir rapidement une instance de demo une fois le VPS lance :
|
||||
|
||||
```bash
|
||||
./deploy/vps/generate-demo-data.sh
|
||||
```
|
||||
|
||||
### Commandes utiles
|
||||
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Console;
|
||||
|
||||
use App\Administration\Domain\Model\AcademicYear\PeriodType;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolZone;
|
||||
use App\Administration\Infrastructure\Service\DemoDataGenerationResult;
|
||||
use App\Administration\Infrastructure\Service\DemoDataGenerator;
|
||||
use App\Shared\Infrastructure\Tenant\TenantConfig;
|
||||
use App\Shared\Infrastructure\Tenant\TenantNotFoundException;
|
||||
use App\Shared\Infrastructure\Tenant\TenantRegistry;
|
||||
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function getenv;
|
||||
use function implode;
|
||||
|
||||
use Override;
|
||||
|
||||
use function sprintf;
|
||||
use function strtolower;
|
||||
use function strtoupper;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Throwable;
|
||||
|
||||
use function trim;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:dev:generate-demo-data',
|
||||
description: 'Génère un jeu de données complet pour un tenant de démonstration',
|
||||
)]
|
||||
final class GenerateDemoDataCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TenantRegistry $tenantRegistry,
|
||||
private readonly DemoDataGenerator $demoDataGenerator,
|
||||
#[Autowire('%kernel.project_dir%')]
|
||||
private readonly string $projectDir,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('tenant', null, InputOption::VALUE_OPTIONAL, 'Sous-domaine du tenant cible')
|
||||
->addOption('password', null, InputOption::VALUE_OPTIONAL, 'Mot de passe partagé pour tous les comptes de démo', 'DemoPassword123!')
|
||||
->addOption('school', null, InputOption::VALUE_OPTIONAL, 'Nom de l’établissement affiché sur les comptes générés')
|
||||
->addOption('zone', null, InputOption::VALUE_OPTIONAL, 'Zone scolaire (A, B ou C)', 'B')
|
||||
->addOption('period-type', null, InputOption::VALUE_OPTIONAL, 'Découpage des périodes (trimester ou semester)', PeriodType::TRIMESTER->value)
|
||||
->addOption('internal-run', null, InputOption::VALUE_NONE, 'Option interne pour exécuter la génération dans la base tenant');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
/** @var string|null $tenantOption */
|
||||
$tenantOption = $input->getOption('tenant');
|
||||
/** @var string $password */
|
||||
$password = $input->getOption('password');
|
||||
/** @var string|null $schoolOption */
|
||||
$schoolOption = $input->getOption('school');
|
||||
/** @var string $zoneOption */
|
||||
$zoneOption = $input->getOption('zone');
|
||||
/** @var string $periodTypeOption */
|
||||
$periodTypeOption = $input->getOption('period-type');
|
||||
$internalRun = $input->getOption('internal-run');
|
||||
|
||||
$tenantConfig = $this->resolveTenantConfig($tenantOption, $io);
|
||||
if ($tenantConfig === null) {
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$zone = SchoolZone::tryFrom(strtoupper(trim($zoneOption)));
|
||||
if ($zone === null) {
|
||||
$io->error('Zone invalide. Valeurs attendues: A, B, C.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$periodType = PeriodType::tryFrom(strtolower(trim($periodTypeOption)));
|
||||
if ($periodType === null) {
|
||||
$io->error('Type de période invalide. Valeurs attendues: trimester, semester.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$schoolName = trim((string) $schoolOption);
|
||||
if ($schoolName === '') {
|
||||
$schoolName = $this->demoDataGenerator->defaultSchoolNameForSubdomain($tenantConfig->subdomain);
|
||||
}
|
||||
|
||||
if ($internalRun) {
|
||||
return $this->runInTenantProcess($tenantConfig, $password, $schoolName, $zone, $periodType, $io);
|
||||
}
|
||||
|
||||
return $this->relaunchAgainstTenantDatabase($tenantConfig, $password, $schoolName, $zone, $periodType, $io);
|
||||
}
|
||||
|
||||
private function relaunchAgainstTenantDatabase(
|
||||
TenantConfig $tenantConfig,
|
||||
string $password,
|
||||
string $schoolName,
|
||||
SchoolZone $zone,
|
||||
PeriodType $periodType,
|
||||
SymfonyStyle $io,
|
||||
): int {
|
||||
$io->section(sprintf(
|
||||
'Génération du jeu de démo pour le tenant "%s" sur sa base dédiée',
|
||||
$tenantConfig->subdomain,
|
||||
));
|
||||
|
||||
$process = new Process(
|
||||
command: [
|
||||
'php',
|
||||
'bin/console',
|
||||
'app:dev:generate-demo-data',
|
||||
'--tenant=' . $tenantConfig->subdomain,
|
||||
'--password=' . $password,
|
||||
'--school=' . $schoolName,
|
||||
'--zone=' . $zone->value,
|
||||
'--period-type=' . $periodType->value,
|
||||
'--internal-run',
|
||||
],
|
||||
cwd: $this->projectDir,
|
||||
env: [
|
||||
...getenv(),
|
||||
'DATABASE_URL' => $tenantConfig->databaseUrl,
|
||||
],
|
||||
timeout: 300,
|
||||
);
|
||||
|
||||
$process->run(static function (string $type, string $buffer) use ($io): void {
|
||||
$io->write($buffer);
|
||||
});
|
||||
|
||||
if ($process->isSuccessful()) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->error(sprintf(
|
||||
'La génération a échoué pour le tenant "%s".',
|
||||
$tenantConfig->subdomain,
|
||||
));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
private function runInTenantProcess(
|
||||
TenantConfig $tenantConfig,
|
||||
string $password,
|
||||
string $schoolName,
|
||||
SchoolZone $zone,
|
||||
PeriodType $periodType,
|
||||
SymfonyStyle $io,
|
||||
): int {
|
||||
try {
|
||||
$result = $this->demoDataGenerator->generate(
|
||||
tenantConfig: $tenantConfig,
|
||||
password: $password,
|
||||
schoolName: $schoolName,
|
||||
zone: $zone,
|
||||
periodType: $periodType,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$io->error([
|
||||
sprintf('Impossible de générer les données de démo pour "%s".', $tenantConfig->subdomain),
|
||||
$e->getMessage(),
|
||||
]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->renderResult($result, $io);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function renderResult(DemoDataGenerationResult $result, SymfonyStyle $io): void
|
||||
{
|
||||
$io->success(sprintf(
|
||||
'Jeu de démo prêt pour le tenant "%s" (%s).',
|
||||
$result->tenantSubdomain,
|
||||
$result->academicYearLabel,
|
||||
));
|
||||
|
||||
$io->table(
|
||||
['Élément', 'Résultat'],
|
||||
[
|
||||
['Établissement', $result->schoolName],
|
||||
['Utilisateurs créés', (string) $result->createdUsers],
|
||||
['Matières créées', (string) $result->createdSubjects],
|
||||
['Classes créées', (string) $result->createdClasses],
|
||||
['Affectations élèves', (string) $result->createdClassAssignments],
|
||||
['Affectations enseignants', (string) $result->createdTeacherAssignments],
|
||||
['Liens parent-enfant', (string) $result->createdGuardianLinks],
|
||||
['Créneaux d’emploi du temps', (string) $result->createdScheduleSlots],
|
||||
['Périodes créées', $result->periodConfigurationCreated ? 'oui' : 'non'],
|
||||
['Calendrier créé', $result->schoolCalendarCreated ? 'oui' : 'non'],
|
||||
['Mode de notation créé', $result->gradingConfigurationCreated ? 'oui' : 'non'],
|
||||
],
|
||||
);
|
||||
|
||||
$io->writeln(sprintf('Mot de passe commun: <info>%s</info>', $result->sharedPassword));
|
||||
|
||||
$io->table(
|
||||
['Rôle', 'Nom', 'Email'],
|
||||
array_map(
|
||||
static fn (array $account): array => [
|
||||
$account['role'],
|
||||
$account['name'],
|
||||
$account['email'],
|
||||
],
|
||||
$result->accounts,
|
||||
),
|
||||
);
|
||||
|
||||
if ($result->warnings !== []) {
|
||||
$io->warning($result->warnings);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveTenantConfig(?string $tenantOption, SymfonyStyle $io): ?TenantConfig
|
||||
{
|
||||
$tenant = trim((string) $tenantOption);
|
||||
|
||||
if ($tenant !== '') {
|
||||
try {
|
||||
return $this->tenantRegistry->getBySubdomain($tenant);
|
||||
} catch (TenantNotFoundException) {
|
||||
$io->error(sprintf(
|
||||
'Tenant "%s" introuvable. Disponibles: %s',
|
||||
$tenant,
|
||||
implode(', ', $this->availableSubdomains()),
|
||||
));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$configs = $this->tenantRegistry->getAllConfigs();
|
||||
if (count($configs) === 1) {
|
||||
return $configs[0];
|
||||
}
|
||||
|
||||
$io->error(sprintf(
|
||||
'Plusieurs tenants sont configurés. Précise --tenant parmi: %s',
|
||||
implode(', ', $this->availableSubdomains()),
|
||||
));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function availableSubdomains(): array
|
||||
{
|
||||
return array_values(array_map(
|
||||
static fn (TenantConfig $config): string => $config->subdomain,
|
||||
$this->tenantRegistry->getAllConfigs(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Service;
|
||||
|
||||
final class DemoDataGenerationResult
|
||||
{
|
||||
public int $createdUsers = 0;
|
||||
public int $createdSubjects = 0;
|
||||
public int $createdClasses = 0;
|
||||
public int $createdClassAssignments = 0;
|
||||
public int $createdTeacherAssignments = 0;
|
||||
public int $createdGuardianLinks = 0;
|
||||
public int $createdScheduleSlots = 0;
|
||||
public bool $periodConfigurationCreated = false;
|
||||
public bool $schoolCalendarCreated = false;
|
||||
public bool $gradingConfigurationCreated = false;
|
||||
|
||||
/** @var list<array{role: string, name: string, email: string}> */
|
||||
public array $accounts = [];
|
||||
|
||||
/** @var list<string> */
|
||||
public array $warnings = [];
|
||||
|
||||
public function __construct(
|
||||
public readonly string $tenantSubdomain,
|
||||
public readonly string $schoolName,
|
||||
public readonly string $academicYearLabel,
|
||||
public readonly string $sharedPassword,
|
||||
) {
|
||||
}
|
||||
|
||||
public function addAccount(string $role, string $name, string $email): void
|
||||
{
|
||||
$this->accounts[] = [
|
||||
'role' => $role,
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
];
|
||||
}
|
||||
|
||||
public function addWarning(string $warning): void
|
||||
{
|
||||
$this->warnings[] = $warning;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,957 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Service;
|
||||
|
||||
use App\Administration\Application\Port\OfficialCalendarProvider;
|
||||
use App\Administration\Application\Port\PasswordHasher;
|
||||
use App\Administration\Domain\Model\AcademicYear\DefaultPeriods;
|
||||
use App\Administration\Domain\Model\AcademicYear\PeriodType;
|
||||
use App\Administration\Domain\Model\ClassAssignment\ClassAssignment;
|
||||
use App\Administration\Domain\Model\GradingConfiguration\SchoolGradingConfiguration;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolZone;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassName;
|
||||
use App\Administration\Domain\Model\SchoolClass\SchoolClass;
|
||||
use App\Administration\Domain\Model\SchoolClass\SchoolId;
|
||||
use App\Administration\Domain\Model\SchoolClass\SchoolLevel;
|
||||
use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
|
||||
use App\Administration\Domain\Model\StudentGuardian\StudentGuardian;
|
||||
use App\Administration\Domain\Model\Subject\Subject;
|
||||
use App\Administration\Domain\Model\Subject\SubjectCode;
|
||||
use App\Administration\Domain\Model\Subject\SubjectColor;
|
||||
use App\Administration\Domain\Model\Subject\SubjectName;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
|
||||
use App\Administration\Domain\Model\User\Email;
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Domain\Model\User\StatutCompte;
|
||||
use App\Administration\Domain\Model\User\User;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Domain\Repository\ClassAssignmentRepository;
|
||||
use App\Administration\Domain\Repository\ClassRepository;
|
||||
use App\Administration\Domain\Repository\GradingConfigurationRepository;
|
||||
use App\Administration\Domain\Repository\PeriodConfigurationRepository;
|
||||
use App\Administration\Domain\Repository\StudentGuardianRepository;
|
||||
use App\Administration\Domain\Repository\SubjectRepository;
|
||||
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Administration\Domain\Repository\UserRepository;
|
||||
use App\Administration\Infrastructure\School\SchoolIdResolver;
|
||||
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
|
||||
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
|
||||
use App\Scolarite\Domain\Repository\ScheduleSlotRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\Shared\Infrastructure\Tenant\TenantConfig;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use DateTimeImmutable;
|
||||
use RuntimeException;
|
||||
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
use function ucwords;
|
||||
|
||||
final readonly class DemoDataGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private ClassRepository $classRepository,
|
||||
private SubjectRepository $subjectRepository,
|
||||
private ClassAssignmentRepository $classAssignmentRepository,
|
||||
private TeacherAssignmentRepository $teacherAssignmentRepository,
|
||||
private StudentGuardianRepository $studentGuardianRepository,
|
||||
private ScheduleSlotRepository $scheduleSlotRepository,
|
||||
private PeriodConfigurationRepository $periodConfigurationRepository,
|
||||
private \App\Administration\Domain\Model\SchoolCalendar\SchoolCalendarRepository $schoolCalendarRepository,
|
||||
private GradingConfigurationRepository $gradingConfigurationRepository,
|
||||
private PasswordHasher $passwordHasher,
|
||||
private Clock $clock,
|
||||
private TenantContext $tenantContext,
|
||||
private CurrentAcademicYearResolver $currentAcademicYearResolver,
|
||||
private SchoolIdResolver $schoolIdResolver,
|
||||
private OfficialCalendarProvider $officialCalendarProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
public function generate(
|
||||
TenantConfig $tenantConfig,
|
||||
string $password,
|
||||
string $schoolName,
|
||||
SchoolZone $zone,
|
||||
PeriodType $periodType,
|
||||
): DemoDataGenerationResult {
|
||||
$this->tenantContext->setCurrentTenant($tenantConfig);
|
||||
|
||||
try {
|
||||
$tenantId = $tenantConfig->tenantId;
|
||||
$academicYearId = $this->resolveAcademicYearId();
|
||||
$academicYearStartYear = $this->resolveAcademicYearStartYear();
|
||||
$academicYearLabel = sprintf('%d-%d', $academicYearStartYear, $academicYearStartYear + 1);
|
||||
$schoolId = SchoolId::fromString($this->schoolIdResolver->resolveForTenant((string) $tenantId));
|
||||
$now = $this->clock->now();
|
||||
$hashedPassword = $this->passwordHasher->hash($password);
|
||||
|
||||
$result = new DemoDataGenerationResult(
|
||||
tenantSubdomain: $tenantConfig->subdomain,
|
||||
schoolName: $schoolName,
|
||||
academicYearLabel: $academicYearLabel,
|
||||
sharedPassword: $password,
|
||||
);
|
||||
|
||||
$this->ensurePeriodConfiguration($tenantId, $academicYearId, $academicYearStartYear, $periodType, $result);
|
||||
$this->ensureSchoolCalendar($tenantId, $academicYearId, $academicYearLabel, $zone, $now, $result);
|
||||
$this->ensureGradingConfiguration($tenantId, $schoolId, $academicYearId, $now, $result);
|
||||
|
||||
$users = $this->seedUsers($tenantConfig, $schoolName, $hashedPassword, $now, $result);
|
||||
$subjects = $this->seedSubjects($tenantId, $schoolId, $now, $result);
|
||||
$classes = $this->seedClasses($tenantId, $schoolId, $academicYearId, $now, $result);
|
||||
|
||||
$this->seedClassAssignments($tenantId, $academicYearId, $users, $classes, $now, $result);
|
||||
$this->seedTeacherAssignments($tenantId, $academicYearId, $users, $classes, $subjects, $now, $result);
|
||||
$this->seedStudentGuardianLinks($tenantId, $users, $now, $result);
|
||||
$this->seedScheduleSlots($tenantId, $classes, $subjects, $users, $academicYearStartYear, $now, $result);
|
||||
|
||||
return $result;
|
||||
} finally {
|
||||
$this->tenantContext->clear();
|
||||
}
|
||||
}
|
||||
|
||||
private function ensurePeriodConfiguration(
|
||||
TenantId $tenantId,
|
||||
AcademicYearId $academicYearId,
|
||||
int $academicYearStartYear,
|
||||
PeriodType $periodType,
|
||||
DemoDataGenerationResult $result,
|
||||
): void {
|
||||
if ($this->periodConfigurationRepository->findByAcademicYear($tenantId, $academicYearId) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$configuration = DefaultPeriods::forType($periodType, $academicYearStartYear);
|
||||
$this->periodConfigurationRepository->save($tenantId, $academicYearId, $configuration);
|
||||
$result->periodConfigurationCreated = true;
|
||||
}
|
||||
|
||||
private function ensureSchoolCalendar(
|
||||
TenantId $tenantId,
|
||||
AcademicYearId $academicYearId,
|
||||
string $academicYearLabel,
|
||||
SchoolZone $zone,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): void {
|
||||
if ($this->schoolCalendarRepository->findByTenantAndYear($tenantId, $academicYearId) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$calendar = SchoolCalendar::initialiser($tenantId, $academicYearId);
|
||||
$calendar->configurer(
|
||||
$zone,
|
||||
$this->officialCalendarProvider->toutesEntreesOfficielles($zone, $academicYearLabel),
|
||||
$now,
|
||||
);
|
||||
|
||||
$this->schoolCalendarRepository->save($calendar);
|
||||
$result->schoolCalendarCreated = true;
|
||||
}
|
||||
|
||||
private function ensureGradingConfiguration(
|
||||
TenantId $tenantId,
|
||||
SchoolId $schoolId,
|
||||
AcademicYearId $academicYearId,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): void {
|
||||
if ($this->gradingConfigurationRepository->findBySchoolAndYear($tenantId, $schoolId, $academicYearId) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$configuration = SchoolGradingConfiguration::configurer(
|
||||
tenantId: $tenantId,
|
||||
schoolId: $schoolId,
|
||||
academicYearId: $academicYearId,
|
||||
mode: SchoolGradingConfiguration::DEFAULT_MODE,
|
||||
hasExistingGrades: false,
|
||||
configuredAt: $now,
|
||||
);
|
||||
|
||||
$this->gradingConfigurationRepository->save($configuration);
|
||||
$result->gradingConfigurationCreated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, User>
|
||||
*/
|
||||
private function seedUsers(
|
||||
TenantConfig $tenantConfig,
|
||||
string $schoolName,
|
||||
string $hashedPassword,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): array {
|
||||
$users = [];
|
||||
|
||||
foreach ($this->staffBlueprints() as $blueprint) {
|
||||
$users[$blueprint['key']] = $this->ensureActiveUser(
|
||||
tenantConfig: $tenantConfig,
|
||||
schoolName: $schoolName,
|
||||
hashedPassword: $hashedPassword,
|
||||
role: $blueprint['role'],
|
||||
emailSlug: $blueprint['emailSlug'],
|
||||
firstName: $blueprint['firstName'],
|
||||
lastName: $blueprint['lastName'],
|
||||
now: $now,
|
||||
result: $result,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($this->teacherBlueprints() as $blueprint) {
|
||||
$users[$blueprint['key']] = $this->ensureActiveUser(
|
||||
tenantConfig: $tenantConfig,
|
||||
schoolName: $schoolName,
|
||||
hashedPassword: $hashedPassword,
|
||||
role: Role::PROF,
|
||||
emailSlug: $blueprint['emailSlug'],
|
||||
firstName: $blueprint['firstName'],
|
||||
lastName: $blueprint['lastName'],
|
||||
now: $now,
|
||||
result: $result,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($this->parentBlueprints() as $blueprint) {
|
||||
$users[$blueprint['key']] = $this->ensureActiveUser(
|
||||
tenantConfig: $tenantConfig,
|
||||
schoolName: $schoolName,
|
||||
hashedPassword: $hashedPassword,
|
||||
role: Role::PARENT,
|
||||
emailSlug: $blueprint['emailSlug'],
|
||||
firstName: $blueprint['firstName'],
|
||||
lastName: $blueprint['lastName'],
|
||||
now: $now,
|
||||
result: $result,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($this->studentBlueprints() as $blueprint) {
|
||||
$users[$blueprint['key']] = $this->ensureActiveUser(
|
||||
tenantConfig: $tenantConfig,
|
||||
schoolName: $schoolName,
|
||||
hashedPassword: $hashedPassword,
|
||||
role: Role::ELEVE,
|
||||
emailSlug: $blueprint['emailSlug'],
|
||||
firstName: $blueprint['firstName'],
|
||||
lastName: $blueprint['lastName'],
|
||||
now: $now,
|
||||
result: $result,
|
||||
dateOfBirth: $blueprint['dateOfBirth'],
|
||||
studentNumber: $blueprint['studentNumber'],
|
||||
);
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Subject>
|
||||
*/
|
||||
private function seedSubjects(
|
||||
TenantId $tenantId,
|
||||
SchoolId $schoolId,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): array {
|
||||
$subjects = [];
|
||||
|
||||
foreach ($this->subjectBlueprints() as $blueprint) {
|
||||
$code = new SubjectCode($blueprint['code']);
|
||||
$subject = $this->subjectRepository->findByCode($code, $tenantId, $schoolId);
|
||||
|
||||
if ($subject === null) {
|
||||
$subject = Subject::creer(
|
||||
tenantId: $tenantId,
|
||||
schoolId: $schoolId,
|
||||
name: new SubjectName($blueprint['name']),
|
||||
code: $code,
|
||||
color: new SubjectColor($blueprint['color']),
|
||||
createdAt: $now,
|
||||
);
|
||||
$subject->decrire($blueprint['description'], $now);
|
||||
$this->subjectRepository->save($subject);
|
||||
++$result->createdSubjects;
|
||||
}
|
||||
|
||||
$subjects[$blueprint['code']] = $subject;
|
||||
}
|
||||
|
||||
return $subjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, SchoolClass>
|
||||
*/
|
||||
private function seedClasses(
|
||||
TenantId $tenantId,
|
||||
SchoolId $schoolId,
|
||||
AcademicYearId $academicYearId,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): array {
|
||||
$classes = [];
|
||||
|
||||
foreach ($this->classBlueprints() as $blueprint) {
|
||||
$name = new ClassName($blueprint['name']);
|
||||
$class = $this->classRepository->findByName($name, $tenantId, $academicYearId);
|
||||
|
||||
if ($class === null) {
|
||||
$class = SchoolClass::creer(
|
||||
tenantId: $tenantId,
|
||||
schoolId: $schoolId,
|
||||
academicYearId: $academicYearId,
|
||||
name: $name,
|
||||
level: $blueprint['level'],
|
||||
capacity: $blueprint['capacity'],
|
||||
createdAt: $now,
|
||||
);
|
||||
$class->decrire($blueprint['description'], $now);
|
||||
$this->classRepository->save($class);
|
||||
++$result->createdClasses;
|
||||
}
|
||||
|
||||
$classes[$blueprint['name']] = $class;
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, User> $users
|
||||
* @param array<string, SchoolClass> $classes
|
||||
*/
|
||||
private function seedClassAssignments(
|
||||
TenantId $tenantId,
|
||||
AcademicYearId $academicYearId,
|
||||
array $users,
|
||||
array $classes,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): void {
|
||||
foreach ($this->studentBlueprints() as $blueprint) {
|
||||
$student = $users[$blueprint['key']];
|
||||
$class = $classes[$blueprint['className']];
|
||||
$assignment = $this->classAssignmentRepository->findByStudent($student->id, $academicYearId, $tenantId);
|
||||
|
||||
if ($assignment === null) {
|
||||
$assignment = ClassAssignment::affecter(
|
||||
tenantId: $tenantId,
|
||||
studentId: $student->id,
|
||||
classId: $class->id,
|
||||
academicYearId: $academicYearId,
|
||||
assignedAt: $now,
|
||||
);
|
||||
$this->classAssignmentRepository->save($assignment);
|
||||
++$result->createdClassAssignments;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$assignment->classId->equals($class->id)) {
|
||||
$assignment->changerClasse($class->id, $now);
|
||||
$this->classAssignmentRepository->save($assignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, User> $users
|
||||
* @param array<string, SchoolClass> $classes
|
||||
* @param array<string, Subject> $subjects
|
||||
*/
|
||||
private function seedTeacherAssignments(
|
||||
TenantId $tenantId,
|
||||
AcademicYearId $academicYearId,
|
||||
array $users,
|
||||
array $classes,
|
||||
array $subjects,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): void {
|
||||
$teacherKeysBySubject = $this->teacherKeysBySubject();
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
foreach ($subjects as $subjectCode => $subject) {
|
||||
$teacher = $users[$teacherKeysBySubject[$subjectCode]];
|
||||
$assignment = $this->teacherAssignmentRepository->findByTeacherClassSubject(
|
||||
teacherId: $teacher->id,
|
||||
classId: $class->id,
|
||||
subjectId: $subject->id,
|
||||
academicYearId: $academicYearId,
|
||||
tenantId: $tenantId,
|
||||
);
|
||||
|
||||
if ($assignment !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$removedAssignment = $this->teacherAssignmentRepository->findRemovedByTeacherClassSubject(
|
||||
teacherId: $teacher->id,
|
||||
classId: $class->id,
|
||||
subjectId: $subject->id,
|
||||
academicYearId: $academicYearId,
|
||||
tenantId: $tenantId,
|
||||
);
|
||||
|
||||
if ($removedAssignment !== null) {
|
||||
$removedAssignment->reactiver($now);
|
||||
$this->teacherAssignmentRepository->save($removedAssignment);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$createdAssignment = TeacherAssignment::creer(
|
||||
tenantId: $tenantId,
|
||||
teacherId: $teacher->id,
|
||||
classId: $class->id,
|
||||
subjectId: $subject->id,
|
||||
academicYearId: $academicYearId,
|
||||
createdAt: $now,
|
||||
);
|
||||
$this->teacherAssignmentRepository->save($createdAssignment);
|
||||
++$result->createdTeacherAssignments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, User> $users
|
||||
*/
|
||||
private function seedStudentGuardianLinks(
|
||||
TenantId $tenantId,
|
||||
array $users,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): void {
|
||||
$createdBy = $users['admin']->id;
|
||||
|
||||
foreach ($this->guardianLinkBlueprints() as $blueprint) {
|
||||
$student = $users[$blueprint['studentKey']];
|
||||
$guardian = $users[$blueprint['guardianKey']];
|
||||
$existingLink = $this->studentGuardianRepository->findByStudentAndGuardian(
|
||||
studentId: $student->id,
|
||||
guardianId: $guardian->id,
|
||||
tenantId: $tenantId,
|
||||
);
|
||||
|
||||
if ($existingLink !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$link = StudentGuardian::lier(
|
||||
studentId: $student->id,
|
||||
guardianId: $guardian->id,
|
||||
relationshipType: $blueprint['relationshipType'],
|
||||
tenantId: $tenantId,
|
||||
createdAt: $now,
|
||||
createdBy: $createdBy,
|
||||
);
|
||||
$this->studentGuardianRepository->save($link);
|
||||
++$result->createdGuardianLinks;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, SchoolClass> $classes
|
||||
* @param array<string, Subject> $subjects
|
||||
* @param array<string, User> $users
|
||||
*/
|
||||
private function seedScheduleSlots(
|
||||
TenantId $tenantId,
|
||||
array $classes,
|
||||
array $subjects,
|
||||
array $users,
|
||||
int $academicYearStartYear,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
): void {
|
||||
$teacherKeysBySubject = $this->teacherKeysBySubject();
|
||||
$recurrenceStart = new DateTimeImmutable(sprintf('%d-09-01', $academicYearStartYear));
|
||||
$recurrenceEnd = new DateTimeImmutable(sprintf('%d-06-30', $academicYearStartYear + 1));
|
||||
|
||||
foreach ($this->scheduleBlueprints() as $blueprint) {
|
||||
foreach ($blueprint['assignments'] as $assignmentBlueprint) {
|
||||
$class = $classes[$assignmentBlueprint['className']];
|
||||
$subject = $subjects[$assignmentBlueprint['subjectCode']];
|
||||
$teacher = $users[$teacherKeysBySubject[$assignmentBlueprint['subjectCode']]];
|
||||
|
||||
if ($this->scheduleSlotExists(
|
||||
tenantId: $tenantId,
|
||||
classId: $class->id,
|
||||
subjectId: $subject->id,
|
||||
teacherId: $teacher->id,
|
||||
dayOfWeek: $blueprint['dayOfWeek'],
|
||||
startTime: $blueprint['startTime'],
|
||||
endTime: $blueprint['endTime'],
|
||||
room: $assignmentBlueprint['room'],
|
||||
recurrenceStart: $recurrenceStart,
|
||||
recurrenceEnd: $recurrenceEnd,
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$slot = ScheduleSlot::creer(
|
||||
tenantId: $tenantId,
|
||||
classId: $class->id,
|
||||
subjectId: $subject->id,
|
||||
teacherId: $teacher->id,
|
||||
dayOfWeek: $blueprint['dayOfWeek'],
|
||||
timeSlot: new TimeSlot($blueprint['startTime'], $blueprint['endTime']),
|
||||
room: $assignmentBlueprint['room'],
|
||||
isRecurring: true,
|
||||
now: $now,
|
||||
recurrenceStart: $recurrenceStart,
|
||||
recurrenceEnd: $recurrenceEnd,
|
||||
);
|
||||
$this->scheduleSlotRepository->save($slot);
|
||||
++$result->createdScheduleSlots;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function scheduleSlotExists(
|
||||
TenantId $tenantId,
|
||||
\App\Administration\Domain\Model\SchoolClass\ClassId $classId,
|
||||
\App\Administration\Domain\Model\Subject\SubjectId $subjectId,
|
||||
UserId $teacherId,
|
||||
DayOfWeek $dayOfWeek,
|
||||
string $startTime,
|
||||
string $endTime,
|
||||
?string $room,
|
||||
DateTimeImmutable $recurrenceStart,
|
||||
DateTimeImmutable $recurrenceEnd,
|
||||
): bool {
|
||||
foreach ($this->scheduleSlotRepository->findRecurringByClass($classId, $tenantId) as $existingSlot) {
|
||||
if (!$existingSlot->subjectId->equals($subjectId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$existingSlot->teacherId->equals($teacherId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existingSlot->dayOfWeek !== $dayOfWeek) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existingSlot->timeSlot->startTime !== $startTime || $existingSlot->timeSlot->endTime !== $endTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existingSlot->room !== $room) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existingSlot->recurrenceStart?->format('Y-m-d') !== $recurrenceStart->format('Y-m-d')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existingSlot->recurrenceEnd?->format('Y-m-d') !== $recurrenceEnd->format('Y-m-d')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function ensureActiveUser(
|
||||
TenantConfig $tenantConfig,
|
||||
string $schoolName,
|
||||
string $hashedPassword,
|
||||
Role $role,
|
||||
string $emailSlug,
|
||||
string $firstName,
|
||||
string $lastName,
|
||||
DateTimeImmutable $now,
|
||||
DemoDataGenerationResult $result,
|
||||
?DateTimeImmutable $dateOfBirth = null,
|
||||
?string $studentNumber = null,
|
||||
): User {
|
||||
$email = new Email($this->emailFor($emailSlug, $tenantConfig->subdomain));
|
||||
$user = $this->userRepository->findByEmail($email, $tenantConfig->tenantId);
|
||||
|
||||
if ($user === null) {
|
||||
$user = User::reconstitute(
|
||||
id: UserId::generate(),
|
||||
email: $email,
|
||||
roles: [$role],
|
||||
tenantId: $tenantConfig->tenantId,
|
||||
schoolName: $schoolName,
|
||||
statut: StatutCompte::ACTIF,
|
||||
dateNaissance: $dateOfBirth,
|
||||
createdAt: $now,
|
||||
hashedPassword: $hashedPassword,
|
||||
activatedAt: $now,
|
||||
consentementParental: null,
|
||||
firstName: $firstName,
|
||||
lastName: $lastName,
|
||||
studentNumber: $studentNumber,
|
||||
);
|
||||
$this->userRepository->save($user);
|
||||
++$result->createdUsers;
|
||||
} else {
|
||||
$userWasChanged = false;
|
||||
|
||||
if ($user->firstName !== $firstName || $user->lastName !== $lastName) {
|
||||
$user->mettreAJourInfos($firstName, $lastName);
|
||||
$userWasChanged = true;
|
||||
}
|
||||
|
||||
if (!$user->aLeRole($role)) {
|
||||
$user->attribuerRole($role, $now);
|
||||
$userWasChanged = true;
|
||||
}
|
||||
|
||||
if ($user->statut === StatutCompte::SUSPENDU) {
|
||||
$user->debloquer($now);
|
||||
$userWasChanged = true;
|
||||
}
|
||||
|
||||
if ($user->statut === StatutCompte::ACTIF) {
|
||||
$user->changerMotDePasse($hashedPassword, $now);
|
||||
$userWasChanged = true;
|
||||
} else {
|
||||
$result->addWarning(sprintf(
|
||||
'Le compte "%s" existe déjà avec le statut "%s". Le mot de passe commun n\'a pas pu être réinitialisé.',
|
||||
(string) $email,
|
||||
$user->statut->value,
|
||||
));
|
||||
}
|
||||
|
||||
if ($userWasChanged) {
|
||||
$this->userRepository->save($user);
|
||||
}
|
||||
}
|
||||
|
||||
$result->addAccount(
|
||||
role: $role->label(),
|
||||
name: trim($firstName . ' ' . $lastName),
|
||||
email: (string) $email,
|
||||
);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function resolveAcademicYearId(): AcademicYearId
|
||||
{
|
||||
$resolved = $this->currentAcademicYearResolver->resolve('current');
|
||||
if ($resolved === null) {
|
||||
throw new RuntimeException('Impossible de résoudre l\'année scolaire courante.');
|
||||
}
|
||||
|
||||
return AcademicYearId::fromString($resolved);
|
||||
}
|
||||
|
||||
private function resolveAcademicYearStartYear(): int
|
||||
{
|
||||
$startYear = $this->currentAcademicYearResolver->resolveStartYear('current');
|
||||
if ($startYear === null) {
|
||||
throw new RuntimeException('Impossible de résoudre le millésime de l\'année scolaire courante.');
|
||||
}
|
||||
|
||||
return $startYear;
|
||||
}
|
||||
|
||||
private function emailFor(string $emailSlug, string $subdomain): string
|
||||
{
|
||||
return sprintf('%s.%s@classeo.test', strtolower($emailSlug), strtolower($subdomain));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{key: string, role: Role, emailSlug: string, firstName: string, lastName: string}>
|
||||
*/
|
||||
private function staffBlueprints(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'key' => 'admin',
|
||||
'role' => Role::ADMIN,
|
||||
'emailSlug' => 'admin.direction',
|
||||
'firstName' => 'Camille',
|
||||
'lastName' => 'Martin',
|
||||
],
|
||||
[
|
||||
'key' => 'vie-scolaire',
|
||||
'role' => Role::VIE_SCOLAIRE,
|
||||
'emailSlug' => 'vie.scolaire',
|
||||
'firstName' => 'Nora',
|
||||
'lastName' => 'Petit',
|
||||
],
|
||||
[
|
||||
'key' => 'secretariat',
|
||||
'role' => Role::SECRETARIAT,
|
||||
'emailSlug' => 'secretariat',
|
||||
'firstName' => 'Lucie',
|
||||
'lastName' => 'Bernard',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{key: string, subjectCode: string, emailSlug: string, firstName: string, lastName: string}>
|
||||
*/
|
||||
private function teacherBlueprints(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'key' => 'teacher-math',
|
||||
'subjectCode' => 'MATH',
|
||||
'emailSlug' => 'prof.amina.benali',
|
||||
'firstName' => 'Amina',
|
||||
'lastName' => 'Benali',
|
||||
],
|
||||
[
|
||||
'key' => 'teacher-fr',
|
||||
'subjectCode' => 'FR',
|
||||
'emailSlug' => 'prof.julie.caron',
|
||||
'firstName' => 'Julie',
|
||||
'lastName' => 'Caron',
|
||||
],
|
||||
[
|
||||
'key' => 'teacher-hg',
|
||||
'subjectCode' => 'HG',
|
||||
'emailSlug' => 'prof.marc.garcia',
|
||||
'firstName' => 'Marc',
|
||||
'lastName' => 'Garcia',
|
||||
],
|
||||
[
|
||||
'key' => 'teacher-svt',
|
||||
'subjectCode' => 'SVT',
|
||||
'emailSlug' => 'prof.sophie.lambert',
|
||||
'firstName' => 'Sophie',
|
||||
'lastName' => 'Lambert',
|
||||
],
|
||||
[
|
||||
'key' => 'teacher-ang',
|
||||
'subjectCode' => 'ANG',
|
||||
'emailSlug' => 'prof.david.nguyen',
|
||||
'firstName' => 'David',
|
||||
'lastName' => 'Nguyen',
|
||||
],
|
||||
[
|
||||
'key' => 'teacher-eps',
|
||||
'subjectCode' => 'EPS',
|
||||
'emailSlug' => 'prof.clara.petit',
|
||||
'firstName' => 'Clara',
|
||||
'lastName' => 'Petit',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{key: string, emailSlug: string, firstName: string, lastName: string}>
|
||||
*/
|
||||
private function parentBlueprints(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'parent-nadia-martin', 'emailSlug' => 'parent.nadia.martin', 'firstName' => 'Nadia', 'lastName' => 'Martin'],
|
||||
['key' => 'parent-karim-martin', 'emailSlug' => 'parent.karim.martin', 'firstName' => 'Karim', 'lastName' => 'Martin'],
|
||||
['key' => 'parent-ines-lopez', 'emailSlug' => 'parent.ines.lopez', 'firstName' => 'Ines', 'lastName' => 'Lopez'],
|
||||
['key' => 'parent-raul-lopez', 'emailSlug' => 'parent.raul.lopez', 'firstName' => 'Raul', 'lastName' => 'Lopez'],
|
||||
['key' => 'parent-claire-bernard', 'emailSlug' => 'parent.claire.bernard', 'firstName' => 'Claire', 'lastName' => 'Bernard'],
|
||||
['key' => 'parent-mehdi-petit', 'emailSlug' => 'parent.mehdi.petit', 'firstName' => 'Mehdi', 'lastName' => 'Petit'],
|
||||
['key' => 'parent-laura-moreau', 'emailSlug' => 'parent.laura.moreau', 'firstName' => 'Laura', 'lastName' => 'Moreau'],
|
||||
['key' => 'parent-olivier-moreau', 'emailSlug' => 'parent.olivier.moreau', 'firstName' => 'Olivier', 'lastName' => 'Moreau'],
|
||||
['key' => 'parent-celine-dupont', 'emailSlug' => 'parent.celine.dupont', 'firstName' => 'Celine', 'lastName' => 'Dupont'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{
|
||||
* key: string,
|
||||
* emailSlug: string,
|
||||
* firstName: string,
|
||||
* lastName: string,
|
||||
* className: string,
|
||||
* dateOfBirth: DateTimeImmutable,
|
||||
* studentNumber: string
|
||||
* }>
|
||||
*/
|
||||
private function studentBlueprints(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'student-lina-martin', 'emailSlug' => 'eleve.lina.martin', 'firstName' => 'Lina', 'lastName' => 'Martin', 'className' => '6A', 'dateOfBirth' => new DateTimeImmutable('2013-02-14'), 'studentNumber' => 'DEMO-001'],
|
||||
['key' => 'student-chloe-lopez', 'emailSlug' => 'eleve.chloe.lopez', 'firstName' => 'Chloe', 'lastName' => 'Lopez', 'className' => '6A', 'dateOfBirth' => new DateTimeImmutable('2013-05-02'), 'studentNumber' => 'DEMO-002'],
|
||||
['key' => 'student-hugo-lopez', 'emailSlug' => 'eleve.hugo.lopez', 'firstName' => 'Hugo', 'lastName' => 'Lopez', 'className' => '6A', 'dateOfBirth' => new DateTimeImmutable('2013-07-22'), 'studentNumber' => 'DEMO-003'],
|
||||
['key' => 'student-zoe-moreau', 'emailSlug' => 'eleve.zoe.moreau', 'firstName' => 'Zoe', 'lastName' => 'Moreau', 'className' => '6A', 'dateOfBirth' => new DateTimeImmutable('2013-11-09'), 'studentNumber' => 'DEMO-004'],
|
||||
['key' => 'student-jade-bernard', 'emailSlug' => 'eleve.jade.bernard', 'firstName' => 'Jade', 'lastName' => 'Bernard', 'className' => '5A', 'dateOfBirth' => new DateTimeImmutable('2012-01-18'), 'studentNumber' => 'DEMO-005'],
|
||||
['key' => 'student-ethan-bernard', 'emailSlug' => 'eleve.ethan.bernard', 'firstName' => 'Ethan', 'lastName' => 'Bernard', 'className' => '5A', 'dateOfBirth' => new DateTimeImmutable('2012-03-27'), 'studentNumber' => 'DEMO-006'],
|
||||
['key' => 'student-nino-moreau', 'emailSlug' => 'eleve.nino.moreau', 'firstName' => 'Nino', 'lastName' => 'Moreau', 'className' => '5A', 'dateOfBirth' => new DateTimeImmutable('2012-09-04'), 'studentNumber' => 'DEMO-007'],
|
||||
['key' => 'student-sarah-dupont', 'emailSlug' => 'eleve.sarah.dupont', 'firstName' => 'Sarah', 'lastName' => 'Dupont', 'className' => '5A', 'dateOfBirth' => new DateTimeImmutable('2012-12-30'), 'studentNumber' => 'DEMO-008'],
|
||||
['key' => 'student-noah-martin', 'emailSlug' => 'eleve.noah.martin', 'firstName' => 'Noah', 'lastName' => 'Martin', 'className' => '4A', 'dateOfBirth' => new DateTimeImmutable('2011-02-11'), 'studentNumber' => 'DEMO-009'],
|
||||
['key' => 'student-adam-petit', 'emailSlug' => 'eleve.adam.petit', 'firstName' => 'Adam', 'lastName' => 'Petit', 'className' => '4A', 'dateOfBirth' => new DateTimeImmutable('2011-06-19'), 'studentNumber' => 'DEMO-010'],
|
||||
['key' => 'student-yasmine-petit', 'emailSlug' => 'eleve.yasmine.petit', 'firstName' => 'Yasmine', 'lastName' => 'Petit', 'className' => '4A', 'dateOfBirth' => new DateTimeImmutable('2011-08-25'), 'studentNumber' => 'DEMO-011'],
|
||||
['key' => 'student-mael-dupont', 'emailSlug' => 'eleve.mael.dupont', 'firstName' => 'Mael', 'lastName' => 'Dupont', 'className' => '4A', 'dateOfBirth' => new DateTimeImmutable('2011-10-03'), 'studentNumber' => 'DEMO-012'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{
|
||||
* code: non-empty-string,
|
||||
* name: non-empty-string,
|
||||
* color: non-empty-string,
|
||||
* description: non-empty-string
|
||||
* }>
|
||||
*/
|
||||
private function subjectBlueprints(): array
|
||||
{
|
||||
return [
|
||||
['code' => 'MATH', 'name' => 'Mathématiques', 'color' => '#2563EB', 'description' => 'Cours de mathématiques pour la démonstration.'],
|
||||
['code' => 'FR', 'name' => 'Français', 'color' => '#DC2626', 'description' => 'Lecture, grammaire et expression écrite.'],
|
||||
['code' => 'HG', 'name' => 'Histoire-Géographie', 'color' => '#D97706', 'description' => 'Repères historiques et géographiques.'],
|
||||
['code' => 'SVT', 'name' => 'SVT', 'color' => '#16A34A', 'description' => 'Sciences de la vie et de la Terre.'],
|
||||
['code' => 'ANG', 'name' => 'Anglais', 'color' => '#7C3AED', 'description' => 'Cours d’anglais pour tous les niveaux.'],
|
||||
['code' => 'EPS', 'name' => 'EPS', 'color' => '#0891B2', 'description' => 'Éducation physique et sportive.'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{name: string, level: SchoolLevel, capacity: int, description: string}>
|
||||
*/
|
||||
private function classBlueprints(): array
|
||||
{
|
||||
return [
|
||||
['name' => '6A', 'level' => SchoolLevel::SIXIEME, 'capacity' => 28, 'description' => 'Classe de sixième utilisée pour les démonstrations.'],
|
||||
['name' => '5A', 'level' => SchoolLevel::CINQUIEME, 'capacity' => 28, 'description' => 'Classe de cinquième utilisée pour les démonstrations.'],
|
||||
['name' => '4A', 'level' => SchoolLevel::QUATRIEME, 'capacity' => 28, 'description' => 'Classe de quatrième utilisée pour les démonstrations.'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{studentKey: string, guardianKey: string, relationshipType: RelationshipType}>
|
||||
*/
|
||||
private function guardianLinkBlueprints(): array
|
||||
{
|
||||
return [
|
||||
['studentKey' => 'student-lina-martin', 'guardianKey' => 'parent-nadia-martin', 'relationshipType' => RelationshipType::MOTHER],
|
||||
['studentKey' => 'student-lina-martin', 'guardianKey' => 'parent-karim-martin', 'relationshipType' => RelationshipType::FATHER],
|
||||
['studentKey' => 'student-noah-martin', 'guardianKey' => 'parent-nadia-martin', 'relationshipType' => RelationshipType::MOTHER],
|
||||
['studentKey' => 'student-noah-martin', 'guardianKey' => 'parent-karim-martin', 'relationshipType' => RelationshipType::FATHER],
|
||||
['studentKey' => 'student-chloe-lopez', 'guardianKey' => 'parent-ines-lopez', 'relationshipType' => RelationshipType::MOTHER],
|
||||
['studentKey' => 'student-chloe-lopez', 'guardianKey' => 'parent-raul-lopez', 'relationshipType' => RelationshipType::FATHER],
|
||||
['studentKey' => 'student-hugo-lopez', 'guardianKey' => 'parent-ines-lopez', 'relationshipType' => RelationshipType::MOTHER],
|
||||
['studentKey' => 'student-hugo-lopez', 'guardianKey' => 'parent-raul-lopez', 'relationshipType' => RelationshipType::FATHER],
|
||||
['studentKey' => 'student-jade-bernard', 'guardianKey' => 'parent-claire-bernard', 'relationshipType' => RelationshipType::MOTHER],
|
||||
['studentKey' => 'student-ethan-bernard', 'guardianKey' => 'parent-claire-bernard', 'relationshipType' => RelationshipType::MOTHER],
|
||||
['studentKey' => 'student-adam-petit', 'guardianKey' => 'parent-mehdi-petit', 'relationshipType' => RelationshipType::FATHER],
|
||||
['studentKey' => 'student-yasmine-petit', 'guardianKey' => 'parent-mehdi-petit', 'relationshipType' => RelationshipType::FATHER],
|
||||
['studentKey' => 'student-zoe-moreau', 'guardianKey' => 'parent-laura-moreau', 'relationshipType' => RelationshipType::MOTHER],
|
||||
['studentKey' => 'student-zoe-moreau', 'guardianKey' => 'parent-olivier-moreau', 'relationshipType' => RelationshipType::FATHER],
|
||||
['studentKey' => 'student-nino-moreau', 'guardianKey' => 'parent-laura-moreau', 'relationshipType' => RelationshipType::MOTHER],
|
||||
['studentKey' => 'student-nino-moreau', 'guardianKey' => 'parent-olivier-moreau', 'relationshipType' => RelationshipType::FATHER],
|
||||
['studentKey' => 'student-sarah-dupont', 'guardianKey' => 'parent-celine-dupont', 'relationshipType' => RelationshipType::TUTOR_F],
|
||||
['studentKey' => 'student-mael-dupont', 'guardianKey' => 'parent-celine-dupont', 'relationshipType' => RelationshipType::TUTOR_F],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{
|
||||
* dayOfWeek: DayOfWeek,
|
||||
* startTime: string,
|
||||
* endTime: string,
|
||||
* assignments: list<array{className: string, subjectCode: string, room: string}>
|
||||
* }>
|
||||
*/
|
||||
private function scheduleBlueprints(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'dayOfWeek' => DayOfWeek::MONDAY,
|
||||
'startTime' => '08:00',
|
||||
'endTime' => '09:00',
|
||||
'assignments' => [
|
||||
['className' => '6A', 'subjectCode' => 'MATH', 'room' => 'B12'],
|
||||
['className' => '5A', 'subjectCode' => 'FR', 'room' => 'B14'],
|
||||
['className' => '4A', 'subjectCode' => 'HG', 'room' => 'B16'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'dayOfWeek' => DayOfWeek::MONDAY,
|
||||
'startTime' => '09:00',
|
||||
'endTime' => '10:00',
|
||||
'assignments' => [
|
||||
['className' => '6A', 'subjectCode' => 'SVT', 'room' => 'LAB1'],
|
||||
['className' => '5A', 'subjectCode' => 'ANG', 'room' => 'B15'],
|
||||
['className' => '4A', 'subjectCode' => 'EPS', 'room' => 'GYM1'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'dayOfWeek' => DayOfWeek::TUESDAY,
|
||||
'startTime' => '08:00',
|
||||
'endTime' => '09:00',
|
||||
'assignments' => [
|
||||
['className' => '6A', 'subjectCode' => 'FR', 'room' => 'B13'],
|
||||
['className' => '5A', 'subjectCode' => 'HG', 'room' => 'B16'],
|
||||
['className' => '4A', 'subjectCode' => 'MATH', 'room' => 'B12'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'dayOfWeek' => DayOfWeek::TUESDAY,
|
||||
'startTime' => '09:00',
|
||||
'endTime' => '10:00',
|
||||
'assignments' => [
|
||||
['className' => '6A', 'subjectCode' => 'ANG', 'room' => 'B15'],
|
||||
['className' => '5A', 'subjectCode' => 'EPS', 'room' => 'GYM1'],
|
||||
['className' => '4A', 'subjectCode' => 'SVT', 'room' => 'LAB1'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'dayOfWeek' => DayOfWeek::THURSDAY,
|
||||
'startTime' => '13:00',
|
||||
'endTime' => '14:00',
|
||||
'assignments' => [
|
||||
['className' => '6A', 'subjectCode' => 'HG', 'room' => 'B16'],
|
||||
['className' => '5A', 'subjectCode' => 'MATH', 'room' => 'B12'],
|
||||
['className' => '4A', 'subjectCode' => 'FR', 'room' => 'B14'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'dayOfWeek' => DayOfWeek::FRIDAY,
|
||||
'startTime' => '14:00',
|
||||
'endTime' => '15:00',
|
||||
'assignments' => [
|
||||
['className' => '6A', 'subjectCode' => 'EPS', 'room' => 'GYM1'],
|
||||
['className' => '5A', 'subjectCode' => 'SVT', 'room' => 'LAB1'],
|
||||
['className' => '4A', 'subjectCode' => 'ANG', 'room' => 'B15'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function teacherKeysBySubject(): array
|
||||
{
|
||||
$mapping = [];
|
||||
|
||||
foreach ($this->teacherBlueprints() as $blueprint) {
|
||||
$mapping[$blueprint['subjectCode']] = $blueprint['key'];
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
public function defaultSchoolNameForSubdomain(string $subdomain): string
|
||||
{
|
||||
return sprintf(
|
||||
'Établissement de démo %s',
|
||||
ucwords(str_replace('-', ' ', trim($subdomain))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Infrastructure\Service;
|
||||
|
||||
use App\Administration\Application\Port\OfficialCalendarProvider;
|
||||
use App\Administration\Application\Port\PasswordHasher;
|
||||
use App\Administration\Domain\Model\AcademicYear\PeriodType;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntry;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryId;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolZone;
|
||||
use App\Administration\Domain\Model\User\Email;
|
||||
use App\Administration\Domain\Repository\GradingConfigurationRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassAssignmentRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryGradingConfigurationRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryPeriodConfigurationRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolCalendarRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySubjectRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryTeacherAssignmentRepository;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryUserRepository;
|
||||
use App\Administration\Infrastructure\School\SchoolIdResolver;
|
||||
use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver;
|
||||
use App\Administration\Infrastructure\Service\DemoDataGenerator;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleSlotRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Infrastructure\Tenant\TenantConfig;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use App\Shared\Infrastructure\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
final class DemoDataGeneratorTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440123';
|
||||
private const string TENANT_SUBDOMAIN = 'demo';
|
||||
|
||||
private InMemoryUserRepository $userRepository;
|
||||
private InMemoryClassRepository $classRepository;
|
||||
private InMemorySubjectRepository $subjectRepository;
|
||||
private InMemoryClassAssignmentRepository $classAssignmentRepository;
|
||||
private InMemoryTeacherAssignmentRepository $teacherAssignmentRepository;
|
||||
private InMemoryStudentGuardianRepository $studentGuardianRepository;
|
||||
private InMemoryScheduleSlotRepository $scheduleSlotRepository;
|
||||
private InMemoryPeriodConfigurationRepository $periodConfigurationRepository;
|
||||
private InMemorySchoolCalendarRepository $schoolCalendarRepository;
|
||||
private GradingConfigurationRepository $gradingConfigurationRepository;
|
||||
private DemoDataGenerator $generator;
|
||||
private TenantConfig $tenantConfig;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userRepository = new InMemoryUserRepository();
|
||||
$this->classRepository = new InMemoryClassRepository();
|
||||
$this->subjectRepository = new InMemorySubjectRepository();
|
||||
$this->classAssignmentRepository = new InMemoryClassAssignmentRepository();
|
||||
$this->teacherAssignmentRepository = new InMemoryTeacherAssignmentRepository();
|
||||
$this->studentGuardianRepository = new InMemoryStudentGuardianRepository();
|
||||
$this->scheduleSlotRepository = new InMemoryScheduleSlotRepository();
|
||||
$this->periodConfigurationRepository = new InMemoryPeriodConfigurationRepository();
|
||||
$this->schoolCalendarRepository = new InMemorySchoolCalendarRepository();
|
||||
$this->gradingConfigurationRepository = new InMemoryGradingConfigurationRepository();
|
||||
|
||||
$clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-03-10 10:00:00');
|
||||
}
|
||||
};
|
||||
|
||||
$tenantContext = new TenantContext();
|
||||
$currentAcademicYearResolver = new CurrentAcademicYearResolver($tenantContext, $clock);
|
||||
|
||||
$passwordHasher = new class implements PasswordHasher {
|
||||
public function hash(string $plainPassword): string
|
||||
{
|
||||
return 'hashed_' . $plainPassword;
|
||||
}
|
||||
|
||||
public function verify(string $hashedPassword, string $plainPassword): bool
|
||||
{
|
||||
return $hashedPassword === 'hashed_' . $plainPassword;
|
||||
}
|
||||
};
|
||||
|
||||
$officialCalendarProvider = new class implements OfficialCalendarProvider {
|
||||
public function joursFeries(string $academicYear): array
|
||||
{
|
||||
return [
|
||||
new CalendarEntry(
|
||||
id: CalendarEntryId::generate(),
|
||||
type: CalendarEntryType::HOLIDAY,
|
||||
startDate: new DateTimeImmutable('2025-11-11'),
|
||||
endDate: new DateTimeImmutable('2025-11-11'),
|
||||
label: 'Armistice',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function vacancesParZone(SchoolZone $zone, string $academicYear): array
|
||||
{
|
||||
return [
|
||||
new CalendarEntry(
|
||||
id: CalendarEntryId::generate(),
|
||||
type: CalendarEntryType::VACATION,
|
||||
startDate: new DateTimeImmutable('2025-12-20'),
|
||||
endDate: new DateTimeImmutable('2026-01-04'),
|
||||
label: 'Vacances de Noël',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function toutesEntreesOfficielles(SchoolZone $zone, string $academicYear): array
|
||||
{
|
||||
return [
|
||||
...$this->joursFeries($academicYear),
|
||||
...$this->vacancesParZone($zone, $academicYear),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
$this->generator = new DemoDataGenerator(
|
||||
userRepository: $this->userRepository,
|
||||
classRepository: $this->classRepository,
|
||||
subjectRepository: $this->subjectRepository,
|
||||
classAssignmentRepository: $this->classAssignmentRepository,
|
||||
teacherAssignmentRepository: $this->teacherAssignmentRepository,
|
||||
studentGuardianRepository: $this->studentGuardianRepository,
|
||||
scheduleSlotRepository: $this->scheduleSlotRepository,
|
||||
periodConfigurationRepository: $this->periodConfigurationRepository,
|
||||
schoolCalendarRepository: $this->schoolCalendarRepository,
|
||||
gradingConfigurationRepository: $this->gradingConfigurationRepository,
|
||||
passwordHasher: $passwordHasher,
|
||||
clock: $clock,
|
||||
tenantContext: $tenantContext,
|
||||
currentAcademicYearResolver: $currentAcademicYearResolver,
|
||||
schoolIdResolver: new SchoolIdResolver(),
|
||||
officialCalendarProvider: $officialCalendarProvider,
|
||||
);
|
||||
|
||||
$this->tenantConfig = new TenantConfig(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
subdomain: self::TENANT_SUBDOMAIN,
|
||||
databaseUrl: 'postgresql://localhost/demo',
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGeneratesACompleteDemoDataset(): void
|
||||
{
|
||||
$result = $this->generator->generate(
|
||||
tenantConfig: $this->tenantConfig,
|
||||
password: 'DemoPassword123!',
|
||||
schoolName: 'Établissement Démo',
|
||||
zone: SchoolZone::B,
|
||||
periodType: PeriodType::TRIMESTER,
|
||||
);
|
||||
|
||||
self::assertSame(30, $result->createdUsers);
|
||||
self::assertSame(6, $result->createdSubjects);
|
||||
self::assertSame(3, $result->createdClasses);
|
||||
self::assertSame(12, $result->createdClassAssignments);
|
||||
self::assertSame(18, $result->createdTeacherAssignments);
|
||||
self::assertSame(18, $result->createdGuardianLinks);
|
||||
self::assertSame(18, $result->createdScheduleSlots);
|
||||
self::assertTrue($result->periodConfigurationCreated);
|
||||
self::assertTrue($result->schoolCalendarCreated);
|
||||
self::assertTrue($result->gradingConfigurationCreated);
|
||||
self::assertCount(30, $result->accounts);
|
||||
self::assertSame('2025-2026', $result->academicYearLabel);
|
||||
|
||||
self::assertCount(30, $this->userRepository->findAllByTenant($this->tenantConfig->tenantId));
|
||||
self::assertCount(12, $this->userRepository->findStudentsByTenant($this->tenantConfig->tenantId));
|
||||
|
||||
$student = $this->userRepository->findByEmail(
|
||||
new Email('eleve.lina.martin.demo@classeo.test'),
|
||||
$this->tenantConfig->tenantId,
|
||||
);
|
||||
self::assertNotNull($student);
|
||||
self::assertSame('hashed_DemoPassword123!', $student->hashedPassword);
|
||||
self::assertSame('DEMO-001', $student->studentNumber);
|
||||
|
||||
$mathTeacher = $this->userRepository->findByEmail(
|
||||
new Email('prof.amina.benali.demo@classeo.test'),
|
||||
$this->tenantConfig->tenantId,
|
||||
);
|
||||
self::assertNotNull($mathTeacher);
|
||||
|
||||
$currentYearId = $this->resolveCurrentAcademicYearId();
|
||||
self::assertCount(
|
||||
3,
|
||||
$this->classRepository->findActiveByTenantAndYear($this->tenantConfig->tenantId, $currentYearId),
|
||||
);
|
||||
self::assertCount(6, $this->subjectRepository->findActiveByTenantAndSchool(
|
||||
$this->tenantConfig->tenantId,
|
||||
\App\Administration\Domain\Model\SchoolClass\SchoolId::fromString(
|
||||
(new SchoolIdResolver())->resolveForTenant(self::TENANT_ID),
|
||||
),
|
||||
));
|
||||
self::assertCount(
|
||||
18,
|
||||
$this->teacherAssignmentRepository->findAllActiveByTenant($this->tenantConfig->tenantId),
|
||||
);
|
||||
self::assertCount(
|
||||
2,
|
||||
$this->studentGuardianRepository->findGuardiansForStudent($student->id, $this->tenantConfig->tenantId),
|
||||
);
|
||||
|
||||
$calendar = $this->schoolCalendarRepository->findByTenantAndYear($this->tenantConfig->tenantId, $currentYearId);
|
||||
self::assertNotNull($calendar);
|
||||
self::assertCount(2, $calendar->entries());
|
||||
|
||||
$periodConfiguration = $this->periodConfigurationRepository->findByAcademicYear(
|
||||
$this->tenantConfig->tenantId,
|
||||
$currentYearId,
|
||||
);
|
||||
self::assertNotNull($periodConfiguration);
|
||||
self::assertSame(PeriodType::TRIMESTER, $periodConfiguration->type);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itIsIdempotentWhenRunTwice(): void
|
||||
{
|
||||
$this->generator->generate(
|
||||
tenantConfig: $this->tenantConfig,
|
||||
password: 'DemoPassword123!',
|
||||
schoolName: 'Établissement Démo',
|
||||
zone: SchoolZone::B,
|
||||
periodType: PeriodType::TRIMESTER,
|
||||
);
|
||||
|
||||
$result = $this->generator->generate(
|
||||
tenantConfig: $this->tenantConfig,
|
||||
password: 'DemoPassword123!',
|
||||
schoolName: 'Établissement Démo',
|
||||
zone: SchoolZone::B,
|
||||
periodType: PeriodType::TRIMESTER,
|
||||
);
|
||||
|
||||
self::assertSame(0, $result->createdUsers);
|
||||
self::assertSame(0, $result->createdSubjects);
|
||||
self::assertSame(0, $result->createdClasses);
|
||||
self::assertSame(0, $result->createdClassAssignments);
|
||||
self::assertSame(0, $result->createdTeacherAssignments);
|
||||
self::assertSame(0, $result->createdGuardianLinks);
|
||||
self::assertSame(0, $result->createdScheduleSlots);
|
||||
self::assertFalse($result->periodConfigurationCreated);
|
||||
self::assertFalse($result->schoolCalendarCreated);
|
||||
self::assertFalse($result->gradingConfigurationCreated);
|
||||
self::assertCount(30, $result->accounts);
|
||||
}
|
||||
|
||||
private function resolveCurrentAcademicYearId(): \App\Administration\Domain\Model\SchoolClass\AcademicYearId
|
||||
{
|
||||
$tenantContext = new TenantContext();
|
||||
$tenantContext->setCurrentTenant($this->tenantConfig);
|
||||
$clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-03-10 10:00:00');
|
||||
}
|
||||
};
|
||||
|
||||
$resolver = new CurrentAcademicYearResolver($tenantContext, $clock);
|
||||
|
||||
return \App\Administration\Domain\Model\SchoolClass\AcademicYearId::fromString(
|
||||
$resolver->resolve('current') ?? throw new RuntimeException('Missing academic year'),
|
||||
);
|
||||
}
|
||||
}
|
||||
200
deploy/vps/generate-demo-data.sh
Executable file
200
deploy/vps/generate-demo-data.sh
Executable file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
ROOT_DIR=$(cd -- "${SCRIPT_DIR}/../.." && pwd)
|
||||
ENV_FILE="${CLASSEO_ENV_FILE:-${SCRIPT_DIR}/.env}"
|
||||
COMPOSE_FILE="${CLASSEO_COMPOSE_FILE:-${ROOT_DIR}/compose.prod.yaml}"
|
||||
PHP_SERVICE="${CLASSEO_PHP_SERVICE:-php}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: ./deploy/vps/generate-demo-data.sh [options]
|
||||
|
||||
Generate a full demo dataset inside the tenant database used by the VPS deployment.
|
||||
|
||||
Options:
|
||||
--tenant SUBDOMAIN Tenant subdomain. Defaults to TENANT_SUBDOMAIN from ${ENV_FILE}.
|
||||
--password PASSWORD Shared password for all generated accounts.
|
||||
Default: DemoPassword123!
|
||||
--school NAME School name displayed on generated accounts.
|
||||
--zone ZONE School zone: A, B or C. Default: B
|
||||
--period-type TYPE Academic period type: trimester or semester.
|
||||
Default: trimester
|
||||
--env-file PATH Override env file path. Default: ${ENV_FILE}
|
||||
--compose-file PATH Override compose file path. Default: ${COMPOSE_FILE}
|
||||
--service NAME Override PHP service name. Default: ${PHP_SERVICE}
|
||||
-h, --help Show this help.
|
||||
|
||||
Examples:
|
||||
./deploy/vps/generate-demo-data.sh
|
||||
./deploy/vps/generate-demo-data.sh --password 'Demo2026!'
|
||||
./deploy/vps/generate-demo-data.sh --tenant demo --school 'College de demo'
|
||||
EOF
|
||||
}
|
||||
|
||||
prompt_with_default() {
|
||||
local label="${1}"
|
||||
local default_value="${2:-}"
|
||||
local answer
|
||||
|
||||
if [ -n "${default_value}" ]; then
|
||||
printf "%s [%s]: " "${label}" "${default_value}" >&2
|
||||
else
|
||||
printf "%s: " "${label}" >&2
|
||||
fi
|
||||
|
||||
IFS= read -r answer
|
||||
printf '%s\n' "${answer:-${default_value}}"
|
||||
}
|
||||
|
||||
require_file() {
|
||||
local path="${1}"
|
||||
local label="${2}"
|
||||
|
||||
if [ ! -f "${path}" ]; then
|
||||
echo "Missing ${label}: ${path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Required command not found: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
TENANT=""
|
||||
PASSWORD="DemoPassword123!"
|
||||
SCHOOL=""
|
||||
ZONE="B"
|
||||
PERIOD_TYPE="trimester"
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--tenant)
|
||||
TENANT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--tenant=*)
|
||||
TENANT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--password)
|
||||
PASSWORD="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--password=*)
|
||||
PASSWORD="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--school)
|
||||
SCHOOL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--school=*)
|
||||
SCHOOL="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--zone)
|
||||
ZONE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--zone=*)
|
||||
ZONE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--period-type)
|
||||
PERIOD_TYPE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--period-type=*)
|
||||
PERIOD_TYPE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--env-file)
|
||||
ENV_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--env-file=*)
|
||||
ENV_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--compose-file)
|
||||
COMPOSE_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--compose-file=*)
|
||||
COMPOSE_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--service)
|
||||
PHP_SERVICE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--service=*)
|
||||
PHP_SERVICE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
echo >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_command docker
|
||||
require_file "${ENV_FILE}" "env file"
|
||||
require_file "${COMPOSE_FILE}" "compose file"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${ENV_FILE}"
|
||||
set +a
|
||||
|
||||
if [ -z "${TENANT}" ]; then
|
||||
TENANT="${TENANT_SUBDOMAIN:-}"
|
||||
fi
|
||||
|
||||
if [ -z "${TENANT}" ] && [ -t 0 ]; then
|
||||
TENANT=$(prompt_with_default "Tenant subdomain" "demo")
|
||||
fi
|
||||
|
||||
if [ -z "${TENANT}" ]; then
|
||||
echo "Tenant subdomain is required. Pass --tenant or define TENANT_SUBDOMAIN in ${ENV_FILE}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${SCHOOL}" ] && [ -t 0 ]; then
|
||||
SCHOOL=$(prompt_with_default "School name (optional)" "")
|
||||
fi
|
||||
|
||||
COMMAND=(
|
||||
docker compose
|
||||
--env-file "${ENV_FILE}"
|
||||
-f "${COMPOSE_FILE}"
|
||||
exec "${PHP_SERVICE}"
|
||||
php bin/console app:dev:generate-demo-data
|
||||
"--tenant=${TENANT}"
|
||||
"--password=${PASSWORD}"
|
||||
"--zone=${ZONE}"
|
||||
"--period-type=${PERIOD_TYPE}"
|
||||
)
|
||||
|
||||
if [ -n "${SCHOOL}" ]; then
|
||||
COMMAND+=("--school=${SCHOOL}")
|
||||
fi
|
||||
|
||||
echo "Running demo data generator for tenant: ${TENANT}"
|
||||
echo "Compose file: ${COMPOSE_FILE}"
|
||||
echo "Env file: ${ENV_FILE}"
|
||||
echo
|
||||
|
||||
"${COMMAND[@]}"
|
||||
@@ -7,6 +7,9 @@ Pour un deploiement mono-serveur de validation ou de demo, voir :
|
||||
- `compose.prod.yaml`
|
||||
- `deploy/vps/`
|
||||
|
||||
Le dossier `deploy/vps/` contient aussi un wrapper pour peupler une demo :
|
||||
- `deploy/vps/generate-demo-data.sh`
|
||||
|
||||
## Architecture Multi-tenant
|
||||
|
||||
Classeo utilise une architecture multi-tenant où chaque école a son propre sous-domaine :
|
||||
|
||||
@@ -19,6 +19,7 @@ Le dossier `deploy/vps/` contient deja les fichiers necessaires :
|
||||
- `deploy/vps/Caddyfile`
|
||||
- `deploy/vps/generate-jwt.sh`
|
||||
- `deploy/vps/postgres/01-create-tenant-db.sh`
|
||||
- `deploy/vps/generate-demo-data.sh`
|
||||
|
||||
## 1. Prerequis
|
||||
|
||||
@@ -288,9 +289,47 @@ docker compose --env-file deploy/vps/.env -f compose.prod.yaml exec php \
|
||||
|
||||
Le tenant PostgreSQL est cree automatiquement au premier demarrage via `deploy/vps/postgres/01-create-tenant-db.sh`.
|
||||
|
||||
## 13. Creer un utilisateur de demo
|
||||
## 13. Generer un jeu de donnees de demo
|
||||
|
||||
Optionnel, mais pratique pour une verification complete.
|
||||
Pour peupler rapidement l'application avec des comptes et des donnees realistes :
|
||||
- direction
|
||||
- vie scolaire
|
||||
- secretariat
|
||||
- professeurs
|
||||
- eleves
|
||||
- parents
|
||||
- matieres
|
||||
- classes
|
||||
- affectations
|
||||
- emploi du temps
|
||||
|
||||
Le plus simple est d'utiliser le wrapper VPS :
|
||||
|
||||
```bash
|
||||
./deploy/vps/generate-demo-data.sh
|
||||
```
|
||||
|
||||
Le script lit `deploy/vps/.env`, reprend `TENANT_SUBDOMAIN` par defaut, puis execute la commande Symfony dans le conteneur `php`.
|
||||
|
||||
Exemples :
|
||||
|
||||
```bash
|
||||
./deploy/vps/generate-demo-data.sh --password 'Demo2026!'
|
||||
./deploy/vps/generate-demo-data.sh --school 'College de demo'
|
||||
./deploy/vps/generate-demo-data.sh --tenant demo --zone B --period-type trimester
|
||||
```
|
||||
|
||||
La commande utilise un mot de passe commun pour tous les comptes, avec une valeur par defaut si tu n'en fournis pas, et affiche tous les comptes crees.
|
||||
Elle est relancable sans dupliquer les donnees.
|
||||
|
||||
Alternative sans wrapper :
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/vps/.env -f compose.prod.yaml exec php \
|
||||
php bin/console app:dev:generate-demo-data --tenant="${TENANT_SUBDOMAIN}"
|
||||
```
|
||||
|
||||
Si tu veux juste creer un compte unique de verification, la commande unitaire existe toujours :
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/vps/.env -f compose.prod.yaml exec php \
|
||||
@@ -339,6 +378,9 @@ docker compose --env-file deploy/vps/.env -f compose.prod.yaml exec php \
|
||||
|
||||
docker compose --env-file deploy/vps/.env -f compose.prod.yaml exec php \
|
||||
php bin/console tenant:migrate "${TENANT_SUBDOMAIN}"
|
||||
|
||||
# Optionnel: remettre un jeu de demo complet a jour
|
||||
./deploy/vps/generate-demo-data.sh
|
||||
```
|
||||
|
||||
## 16. Reinstallation sur une nouvelle machine
|
||||
|
||||
Reference in New Issue
Block a user