diff --git a/app/Http/Controllers/Admin/FlightController.php b/app/Http/Controllers/Admin/FlightController.php index 12e1b2d4..a435278e 100644 --- a/app/Http/Controllers/Admin/FlightController.php +++ b/app/Http/Controllers/Admin/FlightController.php @@ -15,6 +15,7 @@ use App\Repositories\FareRepository; use App\Repositories\FlightFieldRepository; use App\Repositories\FlightRepository; use App\Repositories\SubfleetRepository; +use App\Services\ExporterService; use App\Services\FareService; use App\Services\FlightService; use App\Services\ImporterService; @@ -310,6 +311,27 @@ class FlightController extends Controller return redirect(route('admin.flights.index')); } + /** + * Run the flight exporter + * @param Request $request + * @return \Symfony\Component\HttpFoundation\BinaryFileResponse + * @throws \League\Csv\Exception + */ + public function export(Request $request) + { + $exporter = app(ExporterService::class); + $path = storage_path('app/import/export_flight.csv'); + + $flights = $this->flightRepo->all(); + $exporter->exportFlights($flights, $path); + + return response() + ->download($path, 'flights.csv', [ + 'content-type' => 'text/csv', + ]) + ->deleteFileAfterSend(true); + } + /** * * @param Request $request diff --git a/app/Interfaces/ImportExport.php b/app/Interfaces/ImportExport.php index 98722e35..c9392d5f 100644 --- a/app/Interfaces/ImportExport.php +++ b/app/Interfaces/ImportExport.php @@ -17,26 +17,6 @@ class ImportExport */ public static $columns = []; - /** - * Need to implement in a child class! - * @throws \RuntimeException - */ - public function export() - { - throw new \RuntimeException('Calling export, needs to be implemented in child!'); - } - - /** - * Need to implement in a child class! - * @param array $row - * @param $index - * @throws \RuntimeException - */ - public function import(array $row, $index) - { - throw new \RuntimeException('Calling import, needs to be implemented in child!'); - } - /** * Get the airline from the ICAO * @param $code @@ -118,4 +98,48 @@ class ImportExport return $ret; } + + /** + * @param $obj + * @return mixed + */ + public function objectToMultiString($obj) + { + if(!\is_array($obj)) { + return $obj; + } + + $ret_list = []; + foreach ($obj as $key => $val) { + if(is_numeric($key) && !\is_array($val)) { + $ret_list[] = $val; + continue; + } + + $key = trim($key); + + if(!\is_array($val)) { + $val = trim($val); + $ret_list[] = "{$key}={$val}"; + } else { + $q = []; + foreach($val as $subkey => $subval) { + if(is_numeric($subkey)) { + $q[] = $subval; + } else { + $q[] = "{$subkey}={$subval}"; + } + } + + $q = implode('&', $q); + if(!empty($q)) { + $ret_list[] = "{$key}?{$q}"; + } else { + $ret_list[] = $key; + } + } + } + + return implode(';', $ret_list); + } } diff --git a/app/Models/Flight.php b/app/Models/Flight.php index 4c937802..70206fa3 100644 --- a/app/Models/Flight.php +++ b/app/Models/Flight.php @@ -16,6 +16,7 @@ use PhpUnitsOfMeasure\Exception\NonStringUnitName; * @property mixed route_code * @property mixed route_leg * @property Collection field_values + * @property Collection fares */ class Flight extends Model { diff --git a/app/Routes/admin.php b/app/Routes/admin.php index 57a604b8..c5d5dce6 100644 --- a/app/Routes/admin.php +++ b/app/Routes/admin.php @@ -32,6 +32,7 @@ Route::group([ Route::resource('finances', 'FinanceController'); # flights and aircraft associations + Route::get('flights/export', 'FlightController@export')->name('flights.export'); Route::match(['get', 'post'], 'flights/import', 'FlightController@import')->name('flights.import'); Route::match(['get', 'post', 'put', 'delete'], 'flights/{id}/fares', 'FlightController@fares'); Route::match(['get', 'post', 'put', 'delete'], 'flights/{id}/fields', 'FlightController@field_values'); diff --git a/app/Services/ExporterService.php b/app/Services/ExporterService.php new file mode 100644 index 00000000..0560c79f --- /dev/null +++ b/app/Services/ExporterService.php @@ -0,0 +1,72 @@ +flightRepo = $flightRepo; + } + + /** + * @param $csv_file + * @return Writer + */ + public function openCsv($csv_file): Writer + { + $writer = Writer::createFromPath($csv_file, 'w+'); + CharsetConverter::addTo($writer, 'utf-8', 'iso-8859-15'); + return $writer; + } + + /** + * Run the actual importer + * @param Collection $collection + * @param Writer $writer + * @param ImportExport $exporter + * @return bool + * @throws \League\Csv\CannotInsertRecord + */ + protected function runExport(Collection $collection, Writer $writer, ImportExport $exporter): bool + { + $writer->insertOne($exporter->getColumns()); + foreach ($collection as $row) { + $writer->insertOne($exporter->export($row)); + } + + return true; + } + + /** + * Export all of the flights + * @param Collection $flights + * @param string $csv_file + * @return mixed + * @throws \League\Csv\Exception + */ + public function exportFlights($flights, $csv_file) + { + $writer = $this->openCsv($csv_file); + + $exporter = new FlightExporter(); + return $this->runExport($flights, $writer, $exporter); + } +} diff --git a/app/Services/FareService.php b/app/Services/FareService.php index 7d2e1bbc..3bb51020 100644 --- a/app/Services/FareService.php +++ b/app/Services/FareService.php @@ -98,6 +98,12 @@ class FareService extends Service { $flight->fares()->syncWithoutDetaching([$fare->id]); + foreach($override as $key => $item) { + if(!$item) { + unset($override[$key]); + } + } + # modify any pivot values? if (\count($override) > 0) { $flight->fares()->updateExistingPivot($fare->id, $override); diff --git a/app/Services/Import/FlightExporter.php b/app/Services/Import/FlightExporter.php new file mode 100644 index 00000000..e454ee9e --- /dev/null +++ b/app/Services/Import/FlightExporter.php @@ -0,0 +1,107 @@ +{$column}; + } + + # Modify special fields + $ret['airline'] = $ret['airline']->icao; + $ret['distance'] = $ret['distance']->toNumber(); + + $ret['fares'] = $this->getFares($flight); + $ret['fields'] = $this->getFields($flight); + $ret['subfleets'] = $this->getSubfleets($flight); + + return $ret; + } + + /** + * Return any custom fares that have been made to this flight + * @param Flight $flight + * @return string + */ + protected function getFares(Flight &$flight): string + { + $fares = []; + foreach($flight->fares as $fare) { + $fare_export = []; + if($fare->pivot->price) { + $fare_export['price'] = $fare->pivot->price; + } + + if ($fare->pivot->cost) { + $fare_export['cost'] = $fare->pivot->cost; + } + + if ($fare->pivot->capacity) { + $fare_export['capacity'] = $fare->pivot->capacity; + } + + $fares[$fare->code] = $fare_export; + } + + return $this->objectToMultiString($fares); + } + + /** + * Parse all of the subfields + * @param Flight $flight + * @return string + */ + protected function getFields(Flight &$flight): string + { + $ret = []; + foreach ($flight->field_values as $field) { + $ret[$field->name] = $field->value; + } + + return $this->objectToMultiString($ret); + } + + /** + * Create the list of subfleets that are associated here + * @param Flight $flight + * @return string + */ + protected function getSubfleets(Flight &$flight): string + { + $subfleets = []; + foreach($flight->subfleets as $subfleet) { + $subfleets[] = $subfleet->type; + } + + return $this->objectToMultiString($subfleets); + } +} diff --git a/resources/views/admin/flights/index.blade.php b/resources/views/admin/flights/index.blade.php index 97163a23..a993e48c 100644 --- a/resources/views/admin/flights/index.blade.php +++ b/resources/views/admin/flights/index.blade.php @@ -2,6 +2,7 @@ @section('title', 'Flights') @section('actions') +
  • Export to CSV
  • Import from CSV
  • Fields
  • diff --git a/tests/ImporterTest.php b/tests/ImporterTest.php index 64889cd5..e99be4a6 100644 --- a/tests/ImporterTest.php +++ b/tests/ImporterTest.php @@ -40,18 +40,21 @@ class ImporterTest extends TestCase $fare_economy = factory(App\Models\Fare::class)->create(['code' => 'Y', 'capacity' => 150]); $fare_svc->setForSubfleet($subfleet, $fare_economy); + $fare_economy = factory(App\Models\Fare::class)->create(['code' => 'B', 'capacity' => 20]); + $fare_svc->setForSubfleet($subfleet, $fare_economy); + # Add first class $fare_first = factory(App\Models\Fare::class)->create(['code' => 'F', 'capacity' => 10]); $fare_svc->setForSubfleet($subfleet, $fare_first); - return $airline; + return [$airline, $subfleet]; } /** * Test the parsing of different field/column which can be used * for specifying different field values */ - public function testMultiFieldValues() + public function testConvertStringtoObjects(): void { $tests = [ [ @@ -105,6 +108,15 @@ class ImporterTest extends TestCase ] ] ], + [ + 'input' => 'Y?;F?price=1200', + 'expected' => [ + 0 => 'Y', + 'F' => [ + 'price' => 1200 + ] + ] + ], [ 'input' => 'Departure Gate=4;Arrival Gate=C61', 'expected' => [ @@ -116,17 +128,147 @@ class ImporterTest extends TestCase foreach($tests as $test) { $parsed = $this->importBaseClass->parseMultiColumnValues($test['input']); - $this->assertEquals($parsed, $test['expected']); + $this->assertEquals($test['expected'], $parsed); } } + /** + * Tests for converting the different object/array key values + * into the format that we use in CSV files + */ + public function testConvertObjectToString(): void + { + $tests = [ + [ + 'input' => ['gate'], + 'expected' => 'gate', + ], + [ + 'input' => [ + 'gate', + 'cost index', + ], + 'expected' => 'gate;cost index', + ], + [ + 'input' => [ + 'gate' => 'B32', + 'cost index' => '100' + ], + 'expected' => 'gate=B32;cost index=100', + ], + [ + 'input' => [ + 'Y' => [ + 'price' => 200, + 'cost' => 100, + ], + 'F' => [ + 'price' => 1200 + ] + ], + 'expected' => 'Y?price=200&cost=100;F?price=1200', + ], + [ + 'input' => [ + 'Y' => [ + 'price', + 'cost', + ], + 'F' => [ + 'price' => 1200 + ] + ], + 'expected' => 'Y?price&cost;F?price=1200', + ], + [ + 'input' => [ + 'Y' => [ + 'price', + 'cost', + ], + 'F' => [] + ], + 'expected' => 'Y?price&cost;F', + ], + [ + 'input' => [ + 0 => 'Y', + 'F' => [ + 'price' => 1200 + ] + ], + 'expected' => 'Y;F?price=1200', + ], + [ + 'input' => [ + 'Departure Gate' => '4', + 'Arrival Gate' => 'C61', + ], + 'expected' => 'Departure Gate=4;Arrival Gate=C61', + ], + ]; + + foreach ($tests as $test) { + $parsed = $this->importBaseClass->objectToMultiString($test['input']); + $this->assertEquals($test['expected'], $parsed); + } + } + + /** + * Test exporting all the flights to a file + */ + public function testFlightExporter(): void + { + $fareSvc = app(FareService::class); + + [$airline, $subfleet] = $this->insertFlightsScaffoldData(); + $subfleet2 = factory(App\Models\Subfleet::class)->create(['type' => 'B74X']); + + $fareY = \App\Models\Fare::where('code', 'Y')->first(); + $fareF = \App\Models\Fare::where('code', 'F')->first(); + + $flight = factory(App\Models\Flight::class)->create([ + 'airline_id' => $airline->id, + ]); + + $flight->subfleets()->syncWithoutDetaching([$subfleet->id, $subfleet2->id]); + + // + $fareSvc->setForFlight($flight, $fareY, ['capacity' => '100']); + $fareSvc->setForFlight($flight, $fareF); + + // Add some custom fields + \App\Models\FlightFieldValue::create([ + 'flight_id' => $flight->id, + 'name' => 'Departure Gate', + 'value' => '4' + ]); + + \App\Models\FlightFieldValue::create([ + 'flight_id' => $flight->id, + 'name' => 'Arrival Gate', + 'value' => 'C41' + ]); + + // Test the conversion + + $exporter = new \App\Services\Import\FlightExporter(); + $exported = $exporter->export($flight); + + $this->assertEquals('VMS', $exported['airline']); + $this->assertEquals('A32X;B74X', $exported['subfleets']); + $this->assertEquals('Y?capacity=100;F', $exported['fares']); + $this->assertEquals('Departure Gate=4;Arrival Gate=C41', $exported['fields']); + } + /** * Test the flight importer * @throws \League\Csv\Exception */ public function testFlightImporter(): void { - $airline = $this->insertFlightsScaffoldData(); + [$airline, $subfleet] = $this->insertFlightsScaffoldData(); $file_path = base_path('tests/data/flights.csv'); $this->importSvc->importFlights($file_path); @@ -166,7 +308,7 @@ class ImporterTest extends TestCase // Check the fare class $fares = $this->fareSvc->getForFlight($flight); - $this->assertCount(2, $fares); + $this->assertCount(3, $fares); $first = $fares->where('code', 'Y')->first(); $this->assertEquals(300, $first->price); @@ -184,10 +326,9 @@ class ImporterTest extends TestCase } /** - * * @throws \League\Csv\Exception */ - public function testAircraftImporter() + public function testAircraftImporter(): void { $subfleet = factory(App\Models\Subfleet::class)->create(['type' => 'A32X']); @@ -206,10 +347,9 @@ class ImporterTest extends TestCase } /** - * * @throws \League\Csv\Exception */ - public function testAirportImporter() + public function testAirportImporter(): void { $file_path = base_path('tests/data/airports.csv'); $this->importSvc->importAirports($file_path); diff --git a/tests/data/flights.csv b/tests/data/flights.csv index 0e7ef33a..c7b47884 100644 --- a/tests/data/flights.csv +++ b/tests/data/flights.csv @@ -1,2 +1,2 @@ airline,flight_number,route_code,route_leg,dpt_airport,arr_airport,alt_airport,days,dpt_time,arr_time,level,distance,flight_time,flight_type,route,notes,active,subfleets,fares,fields -VMS,1972,,,KAUS,KJFK,KLGA,,0810 CST,1235 EST,350,1477,207,P,ILEXY2 ZENZI LFK ELD J29 MEM Q29 JHW J70 STENT J70 MAGIO J70 LVZ LENDY6,"Just a flight",1,A32X,Y?price=300&cost=100&capacity=130;F?price=600&cost=400,Departure Gate=4;Arrival Gate=C41 +VMS,1972,,,KAUS,KJFK,KLGA,,0810 CST,1235 EST,350,1477,207,P,ILEXY2 ZENZI LFK ELD J29 MEM Q29 JHW J70 STENT J70 MAGIO J70 LVZ LENDY6,"Just a flight",1,A32X,Y?price=300&cost=100&capacity=130;F?price=600&cost=400;B?,Departure Gate=4;Arrival Gate=C41