Export flights to CSV in admin #194
This commit is contained in:
parent
95a7365fee
commit
276b93fc57
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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');
|
||||
|
72
app/Services/ExporterService.php
Normal file
72
app/Services/ExporterService.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Interfaces\ImportExport;
|
||||
use App\Interfaces\Service;
|
||||
use App\Repositories\FlightRepository;
|
||||
use App\Services\Import\FlightExporter;
|
||||
use Illuminate\Support\Collection;
|
||||
use League\Csv\CharsetConverter;
|
||||
use League\Csv\Writer;
|
||||
|
||||
/**
|
||||
* Class ImporterService
|
||||
* @package App\Services
|
||||
*/
|
||||
class ExporterService extends Service
|
||||
{
|
||||
protected $flightRepo;
|
||||
|
||||
/**
|
||||
* ImporterService constructor.
|
||||
* @param FlightRepository $flightRepo
|
||||
*/
|
||||
public function __construct(FlightRepository $flightRepo) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
107
app/Services/Import/FlightExporter.php
Normal file
107
app/Services/Import/FlightExporter.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Import;
|
||||
|
||||
use App\Interfaces\ImportExport;
|
||||
use App\Models\Enums\FlightType;
|
||||
use App\Models\Fare;
|
||||
use App\Models\Flight;
|
||||
use App\Models\Subfleet;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* The flight importer can be imported or export. Operates on rows
|
||||
*
|
||||
* @package App\Services\Import
|
||||
*/
|
||||
class FlightExporter extends ImportExport
|
||||
{
|
||||
/**
|
||||
* Set the current columns and other setup
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
self::$columns = FlightImporter::$columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a flight, parse out the different rows
|
||||
* @param Flight $flight
|
||||
* @return array
|
||||
*/
|
||||
public function export(Flight $flight): array
|
||||
{
|
||||
$ret = [];
|
||||
foreach(self::$columns as $column) {
|
||||
$ret[$column] = $flight->{$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);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
@section('title', 'Flights')
|
||||
|
||||
@section('actions')
|
||||
<li><a href="{{ route('admin.flights.export') }}"><i class="ti-plus"></i>Export to CSV</a></li>
|
||||
<li><a href="{{ route('admin.flights.import') }}"><i class="ti-plus"></i>Import from CSV</a></li>
|
||||
<li><a href="{{ route('admin.flightfields.index') }}"><i class="ti-plus"></i>Fields</a></li>
|
||||
<li>
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user