Add artisan commands to import entries from a CSV file

This commit is contained in:
Matt Young 2025-10-27 22:59:54 -05:00
parent 3315efc83b
commit 2dfb745861
5 changed files with 351 additions and 0 deletions

View File

@ -0,0 +1,124 @@
<?php
namespace App\Console\Commands;
use App\Models\Audition;
use App\Models\Event;
use App\Models\Room;
use App\Models\ScoringGuide;
use App\Services\CsvImportService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use function auditionSetting;
use function Laravel\Prompts\select;
class importCheckAuditionsCommand extends Command
{
protected $signature = 'import:check-auditions';
protected $description = 'Check the import file for auditions that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$lowestPossibleGrade = 1;
$highestPossibleGrade = 12;
$events = Event::all();
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedAuditions = collect();
foreach ($rows as $row) {
if ($checkedAuditions->contains($row['Instrument'])) {
continue;
}
$checkedAuditions->push($row['Instrument']);
if (Audition::where('name', $row['Instrument'])->count() > 0) {
$this->info('Audition '.$row['Instrument'].' already exists');
} else {
$this->newLine();
$this->alert('Audition '.$row['Instrument'].' does not exist');
if ($events->count() === 1) {
$newEventId = $events->first()->id;
} else {
$newEventId = select(
label: 'Which event does this audition belong to?',
options: $events->pluck('name', 'id')->toArray(),
);
}
$newEventName = $row['Instrument'];
$newEventScoreOrder = Audition::max('score_order') + 1;
$newEventEntryDeadline = Carbon::yesterday('America/Chicago')->format('Y-m-d');
$newEventEntryFee = Audition::max('entry_fee');
$newEventMinimumGrade = select(
label: 'What is the minimum grade for this audition?',
options: range($lowestPossibleGrade, $highestPossibleGrade)
);
$newEventMaximumGrade = select(
label: 'What is the maximum grade for this audition?',
options: range($newEventMinimumGrade, $highestPossibleGrade)
);
$newEventRoomId = select(
label: 'Which room does this audition belong to?',
options: Room::pluck('name', 'id')->toArray(),
);
$newEventScoringGuideId = select(
label: 'Which scoring guide should this audition use',
options: ScoringGuide::pluck('name', 'id')->toArray(),
);
if (auditionSetting('advanceTo')) {
$newEventForSeating = select(
label: 'Is this audition for seating?',
options: [
1 => 'Yes',
0 => 'No',
]
);
$newEventForAdvance = select(
label: 'Is this audition for '.auditionSetting('advanceTo').'?',
options: [
1 => 'Yes',
0 => 'No',
]
);
} else {
$newEventForSeating = 1;
$newEventForAdvance = 0;
}
$this->info('New event ID: '.$newEventId);
$this->info('New event name: '.$newEventName);
$this->info('New event score order: '.$newEventScoreOrder);
$this->info('New event entry deadline: '.$newEventEntryDeadline);
$this->info('New event entry fee: '.$newEventEntryFee);
$this->info('New event minimum grade: '.$newEventMinimumGrade);
$this->info('New event maximum grade: '.$newEventMaximumGrade);
$this->info('New event room ID: '.$newEventRoomId);
$this->info('New event scoring guide ID: '.$newEventScoringGuideId);
$this->info('New event for seating: '.$newEventForSeating);
$this->info('New event for advance: '.$newEventForAdvance);
Audition::create([
'event_id' => $newEventId,
'name' => $newEventName,
'score_order' => $newEventScoreOrder,
'entry_deadline' => $newEventEntryDeadline,
'entry_fee' => $newEventEntryFee,
'minimum_grade' => $newEventMinimumGrade,
'maximum_grade' => $newEventMaximumGrade,
'room_id' => $newEventRoomId,
'scoring_guide_id' => $newEventScoringGuideId,
'for_seating' => $newEventForSeating,
'for_advancement' => $newEventForAdvance,
]);
}
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use const PHP_EOL;
use App\Models\School;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importCheckSchoolsCommand extends Command
{
protected $signature = 'import:check-schools';
protected $description = 'Check the import file for schools that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedSchools = collect();
foreach ($rows as $row) {
if ($checkedSchools->contains($row['School'])) {
continue;
}
$checkedSchools->push($row['School']);
if (School::where('name', $row['School'])->count() > 0) {
$this->info('School '.$row['School'].' already exists');
} else {
$this->newLine();
$this->alert('School '.$row['School'].' does not exist'.PHP_EOL.'Creating school...');
School::create(['name' => $row['School']]);
$this->info('School '.$row['School'].' created');
}
}
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Console\Commands;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importCheckStudentsCommand extends Command
{
protected $signature = 'import:check-students';
protected $description = 'Check the import file for students that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$purge = $this->confirm('Do you want to purge the database of existing students and entries?', false);
if ($purge) {
Entry::all()->map(function ($entry) {
$entry->delete();
});
Student::all()->map(function ($student) {
$student->delete();
});
$this->info('Database purged');
}
$schools = School::pluck('id', 'name');
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedStudents = collect();
foreach ($rows as $row) {
$uniqueData = $row['School'].$row['LastName'].$row['LastName'];
if ($checkedStudents->contains($uniqueData)) {
// continue;
}
$checkedStudents->push($uniqueData);
$currentFirstName = $row['FirstName'];
$currentLastName = $row['LastName'];
$currentSchoolName = $row['School'];
$currentSchoolId = $schools[$currentSchoolName];
if (Student::where('first_name', $currentFirstName)->where('last_name',
$currentLastName)->where('school_id', $currentSchoolId)->count() > 0) {
$this->info('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' already exists');
} else {
$this->alert('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' does not exist');
$newStudent = Student::create([
'school_id' => $currentSchoolId,
'first_name' => $currentFirstName,
'last_name' => $currentLastName,
'grade' => $row['Grade'],
]);
$this->info('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' created with id of: '.$newStudent->id);
}
}
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importImportEntriesCommand extends Command
{
protected $signature = 'import';
protected $description = 'Import entries from the import.csv file. First check schools, then students, then auditions, then run this import command';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$checkAuditions = $this->confirm('Do you want to check the auditions in the import for validity first?', true);
if ($checkAuditions) {
$this->call('import:check-auditions');
}
$checkSchools = $this->confirm('Do you want to check the schools in the import for validity first?', true);
if ($checkSchools) {
$this->call('import:check-schools');
}
$checkStudents = $this->confirm('Do you want to check the students in the import for validity first?', true);
if ($checkStudents) {
$this->call('import:check-students');
}
$purge = $this->confirm('Do you want to purge the database of existing entries?', false);
if ($purge) {
Entry::all()->map(function ($entry) {
$entry->delete();
});
$this->info('Database purged');
}
$schools = School::pluck('id', 'name');
$auditions = Audition::pluck('id', 'name');
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
foreach ($rows as $row) {
$schoolId = $schools[$row['School']];
$student = Student::where('first_name', $row['FirstName'])->where('last_name',
$row['LastName'])->where('school_id', $schoolId)->first();
if (! $student) {
$this->error('Student '.$row['FirstName'].' '.$row['LastName'].' from '.$row['School'].' does not exist');
return;
}
$auditionId = $auditions[$row['Instrument']];
try {
Entry::create([
'student_id' => $student->id,
'audition_id' => $auditionId,
]);
} catch (\Exception $e) {
$this->warn('Entry already exists for student '.$student->full_name().' in audition '.$row['Instrument']);
}
$this->info('Entry created for student '.$student->full_name().' in audition '.$row['Instrument']);
}
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Services;
class CsvImportService
{
/**
* Read a CSV file and return its contents as an array
*
* @param string $filePath Full path to the CSV file
* @param bool $trimHeaders Whether to trim whitespace from header names
* @return array Array of rows with header keys
*/
public function readCsv(string $filePath, bool $trimHeaders = true): array
{
if (! file_exists($filePath)) {
throw new \RuntimeException("File not found: {$filePath}");
}
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new \RuntimeException("Unable to open file: {$filePath}");
}
$header = null;
$rows = [];
while (($line = fgetcsv($handle, 0, ',')) !== false) {
if (! $header) {
$header = $trimHeaders ? array_map('trim', $line) : $line;
continue;
}
$row = array_combine($header, $line);
$rows[] = $row;
}
fclose($handle);
return $rows;
}
}