Export flights to CSV in admin #194

This commit is contained in:
Nabeel Shahzad 2018-03-21 17:07:30 -05:00
parent 95a7365fee
commit 276b93fc57
10 changed files with 404 additions and 30 deletions

View File

@ -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

View File

@ -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);
}
}

View File

@ -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
{

View File

@ -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');

View 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);
}
}

View File

@ -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);

View 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);
}
}

View File

@ -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>

View File

@ -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);

View File

@ -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

1 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
2 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 Y?price=300&cost=100&capacity=130;F?price=600&cost=400;B? Departure Gate=4;Arrival Gate=C41