From b9beb6c804ae547458876d986e597e16d9a70a22 Mon Sep 17 00:00:00 2001 From: Nabeel Shahzad Date: Tue, 20 Mar 2018 19:17:11 -0500 Subject: [PATCH] Add importers in console and admin for flights/aircraft/subfleets and airport #194 --- app/Console/Commands/ImportCsv.php | 59 ++++++ app/Database/factories/FareFactory.php | 4 +- .../2017_06_10_040335_create_fares_table.php | 2 +- ...17_06_23_011011_create_subfleet_tables.php | 2 +- .../Controllers/Admin/AircraftController.php | 45 ++++- .../Controllers/Admin/AirportController.php | 41 +++- .../Controllers/Admin/FlightController.php | 36 +++- .../Controllers/Admin/SubfleetController.php | 58 ++++-- app/Interfaces/ImportExport.php | 121 +++++++++++ app/Models/Aircraft.php | 5 +- app/Models/Airline.php | 1 + app/Models/Enums/FlightType.php | 27 +++ app/Models/Fare.php | 2 +- app/Models/Subfleet.php | 4 +- app/Routes/admin.php | 12 +- app/Services/FlightService.php | 8 +- app/Services/Import/AircraftImporter.php | 90 +++++++++ app/Services/Import/AirportImporter.php | 55 +++++ app/Services/Import/FlightImporter.php | 168 ++++++++++++++++ app/Services/Import/SubfleetImporter.php | 54 +++++ app/Services/ImporterService.php | 170 +++++++++++----- composer.json | 3 +- composer.lock | 189 ++++++++++++------ config/filesystems.php | 42 +--- .../views/admin/aircraft/import.blade.php | 5 + .../views/admin/aircraft/index.blade.php | 15 +- .../views/admin/airports/import.blade.php | 5 + .../views/admin/airports/index.blade.php | 9 +- .../views/admin/flights/import.blade.php | 5 + resources/views/admin/flights/index.blade.php | 3 +- resources/views/admin/shared/import.blade.php | 32 +++ .../views/admin/subfleets/import.blade.php | 5 + .../views/admin/subfleets/index.blade.php | 2 + storage/app/.gitignore | 1 + storage/app/import/.gitignore | 2 + tests/ImporterTest.php | 187 ++++++++++++++++- tests/TestCase.php | 18 ++ tests/data/aircraft.csv | 2 + tests/data/airports.csv | 2 + tests/data/flights.csv | 2 + tests/data/subfleets.csv | 2 + 41 files changed, 1270 insertions(+), 225 deletions(-) create mode 100644 app/Console/Commands/ImportCsv.php create mode 100644 app/Interfaces/ImportExport.php create mode 100644 app/Services/Import/AircraftImporter.php create mode 100644 app/Services/Import/AirportImporter.php create mode 100644 app/Services/Import/FlightImporter.php create mode 100644 app/Services/Import/SubfleetImporter.php create mode 100644 resources/views/admin/aircraft/import.blade.php create mode 100644 resources/views/admin/airports/import.blade.php create mode 100644 resources/views/admin/flights/import.blade.php create mode 100644 resources/views/admin/shared/import.blade.php create mode 100644 resources/views/admin/subfleets/import.blade.php create mode 100755 storage/app/import/.gitignore create mode 100644 tests/data/aircraft.csv create mode 100644 tests/data/airports.csv create mode 100644 tests/data/flights.csv create mode 100644 tests/data/subfleets.csv diff --git a/app/Console/Commands/ImportCsv.php b/app/Console/Commands/ImportCsv.php new file mode 100644 index 00000000..ab711545 --- /dev/null +++ b/app/Console/Commands/ImportCsv.php @@ -0,0 +1,59 @@ +importer = $importer; + } + + /** + * @return mixed|void + * @throws \League\Csv\Exception + */ + public function handle() + { + $type = $this->argument('type'); + $file = $this->argument('file'); + + if (\in_array($type, ['flight', 'flights'])) { + $status = $this->importer->importFlights($file); + } elseif ($type === 'aircraft') { + $status = $this->importer->importAircraft($file); + } elseif (\in_array($type, ['airport', 'airports'])) { + $status = $this->importer->importAirports($file); + } elseif ($type === 'subfleet') { + $status = $this->importer->importSubfleets($file); + } + + foreach($status['success'] as $line) { + $this->info($line); + } + + foreach ($status['failed'] as $line) { + $this->error($line); + } + } +} diff --git a/app/Database/factories/FareFactory.php b/app/Database/factories/FareFactory.php index 0289435b..634e573f 100644 --- a/app/Database/factories/FareFactory.php +++ b/app/Database/factories/FareFactory.php @@ -5,8 +5,8 @@ use Faker\Generator as Faker; $factory->define(App\Models\Fare::class, function (Faker $faker) { return [ 'id' => null, - 'code' => $faker->text(5), - 'name' => $faker->text(20), + 'code' => $faker->unique()->text(50), + 'name' => $faker->text(50), 'price' => $faker->randomFloat(2, 100, 1000), 'cost' => function (array $fare) { return round($fare['price'] / 2); diff --git a/app/Database/migrations/2017_06_10_040335_create_fares_table.php b/app/Database/migrations/2017_06_10_040335_create_fares_table.php index e7ec7cac..541f2739 100644 --- a/app/Database/migrations/2017_06_10_040335_create_fares_table.php +++ b/app/Database/migrations/2017_06_10_040335_create_fares_table.php @@ -14,7 +14,7 @@ class CreateFaresTable extends Migration { Schema::create('fares', function (Blueprint $table) { $table->increments('id'); - $table->string('code', 50); + $table->string('code', 50)->unique(); $table->string('name', 50); $table->unsignedDecimal('price')->nullable()->default(0.00); $table->unsignedDecimal('cost')->nullable()->default(0.00); diff --git a/app/Database/migrations/2017_06_23_011011_create_subfleet_tables.php b/app/Database/migrations/2017_06_23_011011_create_subfleet_tables.php index 86cfea54..a786020a 100644 --- a/app/Database/migrations/2017_06_23_011011_create_subfleet_tables.php +++ b/app/Database/migrations/2017_06_23_011011_create_subfleet_tables.php @@ -13,8 +13,8 @@ class CreateSubfleetTables extends Migration Schema::create('subfleets', function (Blueprint $table) { $table->increments('id'); $table->unsignedInteger('airline_id')->nullable(); + $table->string('type', 50)->unique(); $table->string('name', 50); - $table->string('type', 50); $table->unsignedTinyInteger('fuel_type')->nullable(); $table->unsignedDecimal('ground_handling_multiplier')->nullable()->default(100); $table->unsignedDecimal('cargo_capacity')->nullable(); diff --git a/app/Http/Controllers/Admin/AircraftController.php b/app/Http/Controllers/Admin/AircraftController.php index a9422211..20971c5e 100644 --- a/app/Http/Controllers/Admin/AircraftController.php +++ b/app/Http/Controllers/Admin/AircraftController.php @@ -10,9 +10,12 @@ use App\Models\Enums\AircraftStatus; use App\Models\Expense; use App\Models\Subfleet; use App\Repositories\AircraftRepository; +use App\Services\ImporterService; use Flash; use Illuminate\Http\Request; +use Log; use Prettus\Repository\Criteria\RequestCriteria; +use Storage; /** * Class AircraftController @@ -20,16 +23,20 @@ use Prettus\Repository\Criteria\RequestCriteria; */ class AircraftController extends Controller { - private $aircraftRepo; + private $aircraftRepo, + $importSvc; /** * AircraftController constructor. * @param AircraftRepository $aircraftRepo + * @param ImporterService $importSvc */ public function __construct( - AircraftRepository $aircraftRepo + AircraftRepository $aircraftRepo, + ImporterService $importSvc ) { $this->aircraftRepo = $aircraftRepo; + $this->importSvc = $importSvc; } /** @@ -67,7 +74,6 @@ class AircraftController extends Controller $aircraft = $this->aircraftRepo->create($attrs); Flash::success('Aircraft saved successfully.'); - return redirect(route('admin.aircraft.edit', ['id' => $aircraft->id])); } @@ -80,7 +86,6 @@ class AircraftController extends Controller if (empty($aircraft)) { Flash::error('Aircraft not found'); - return redirect(route('admin.aircraft.index')); } @@ -119,7 +124,6 @@ class AircraftController extends Controller if (empty($aircraft)) { Flash::error('Aircraft not found'); - return redirect(route('admin.aircraft.index')); } @@ -127,7 +131,6 @@ class AircraftController extends Controller $this->aircraftRepo->update($attrs, $id); Flash::success('Aircraft updated successfully.'); - return redirect(route('admin.aircraft.index')); } @@ -140,17 +143,43 @@ class AircraftController extends Controller if (empty($aircraft)) { Flash::error('Aircraft not found'); - return redirect(route('admin.aircraft.index')); } $this->aircraftRepo->delete($id); Flash::success('Aircraft deleted successfully.'); - return redirect(route('admin.aircraft.index')); } + /** + * + * @param Request $request + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \League\Csv\Exception + */ + public function import(Request $request) + { + $logs = [ + 'success' => [], + 'failed' => [], + ]; + + if ($request->isMethod('post')) { + $path = Storage::putFileAs( + 'import', $request->file('csv_file'), 'aircraft' + ); + + $path = storage_path('app/'.$path); + Log::info('Uploaded flights import file to '.$path); + $logs = $this->importSvc->importAircraft($path); + } + + return view('admin.aircraft.import', [ + 'logs' => $logs, + ]); + } + /** * @param Aircraft|null $aircraft * @return mixed diff --git a/app/Http/Controllers/Admin/AirportController.php b/app/Http/Controllers/Admin/AirportController.php index 68a320c7..3c981ffe 100644 --- a/app/Http/Controllers/Admin/AirportController.php +++ b/app/Http/Controllers/Admin/AirportController.php @@ -9,10 +9,13 @@ use App\Models\Airport; use App\Models\Expense; use App\Repositories\AirportRepository; use App\Repositories\Criteria\WhereCriteria; +use App\Services\ImporterService; use Flash; use Illuminate\Http\Request; use Jackiedo\Timezonelist\Facades\Timezonelist; +use Log; use Response; +use Storage; /** * Class AirportController @@ -20,15 +23,19 @@ use Response; */ class AirportController extends Controller { - private $airportRepo; + private $airportRepo, + $importSvc; /** * @param AirportRepository $airportRepo + * @param ImporterService $importSvc */ public function __construct( - AirportRepository $airportRepo + AirportRepository $airportRepo, + ImporterService $importSvc ) { $this->airportRepo = $airportRepo; + $this->importSvc = $importSvc; } /** @@ -162,17 +169,43 @@ class AirportController extends Controller if (empty($airport)) { Flash::error('Airport not found'); - return redirect(route('admin.airports.index')); } $this->airportRepo->delete($id); Flash::success('Airport deleted successfully.'); - return redirect(route('admin.airports.index')); } + /** + * + * @param Request $request + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \League\Csv\Exception + */ + public function import(Request $request) + { + $logs = [ + 'success' => [], + 'failed' => [], + ]; + + if ($request->isMethod('post')) { + $path = Storage::putFileAs( + 'import', $request->file('csv_file'), 'airports' + ); + + $path = storage_path('app/'.$path); + Log::info('Uploaded flights import file to '.$path); + $logs = $this->importSvc->importAirports($path); + } + + return view('admin.airports.import', [ + 'logs' => $logs, + ]); + } + /** * @param Airport|null $airport * @return mixed diff --git a/app/Http/Controllers/Admin/FlightController.php b/app/Http/Controllers/Admin/FlightController.php index ae243c18..12e1b2d4 100644 --- a/app/Http/Controllers/Admin/FlightController.php +++ b/app/Http/Controllers/Admin/FlightController.php @@ -17,11 +17,13 @@ use App\Repositories\FlightRepository; use App\Repositories\SubfleetRepository; use App\Services\FareService; use App\Services\FlightService; +use App\Services\ImporterService; use App\Support\Units\Time; use Flash; use Illuminate\Http\Request; use Log; use Response; +use Storage; /** * Class FlightController @@ -36,6 +38,7 @@ class FlightController extends Controller $flightFieldRepo, $fareSvc, $flightSvc, + $importSvc, $subfleetRepo; /** @@ -44,9 +47,10 @@ class FlightController extends Controller * @param AirportRepository $airportRepo * @param FareRepository $fareRepo * @param FlightRepository $flightRepo - * @param FlightFieldRepository $flightFieldRepository + * @param FlightFieldRepository $flightFieldRepo * @param FareService $fareSvc * @param FlightService $flightSvc + * @param ImporterService $importSvc * @param SubfleetRepository $subfleetRepo */ public function __construct( @@ -57,6 +61,7 @@ class FlightController extends Controller FlightFieldRepository $flightFieldRepo, FareService $fareSvc, FlightService $flightSvc, + ImporterService $importSvc, SubfleetRepository $subfleetRepo ) { $this->airlineRepo = $airlineRepo; @@ -66,6 +71,7 @@ class FlightController extends Controller $this->flightFieldRepo = $flightFieldRepo; $this->fareSvc = $fareSvc; $this->flightSvc = $flightSvc; + $this->importSvc = $importSvc; $this->subfleetRepo = $subfleetRepo; } @@ -304,6 +310,34 @@ class FlightController extends Controller return redirect(route('admin.flights.index')); } + /** + * + * @param Request $request + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \League\Csv\Exception + */ + public function import(Request $request) + { + $logs = [ + 'success' => [], + 'failed' => [], + ]; + + if ($request->isMethod('post')) { + $path = Storage::putFileAs( + 'import', $request->file('csv_file'), 'flights' + ); + + $path = storage_path('app/'.$path); + Log::info('Uploaded flights import file to '.$path); + $logs = $this->importSvc->importFlights($path); + } + + return view('admin.flights.import', [ + 'logs' => $logs, + ]); + } + /** * @param $flight * @return mixed diff --git a/app/Http/Controllers/Admin/SubfleetController.php b/app/Http/Controllers/Admin/SubfleetController.php index db759f2f..8d4fac94 100644 --- a/app/Http/Controllers/Admin/SubfleetController.php +++ b/app/Http/Controllers/Admin/SubfleetController.php @@ -15,10 +15,13 @@ use App\Repositories\RankRepository; use App\Repositories\SubfleetRepository; use App\Services\FareService; use App\Services\FleetService; +use App\Services\ImporterService; use Flash; use Illuminate\Http\Request; +use Log; use Prettus\Repository\Criteria\RequestCriteria; use Response; +use Storage; /** * Class SubfleetController @@ -30,6 +33,7 @@ class SubfleetController extends Controller $fareRepo, $fareSvc, $fleetSvc, + $importSvc, $rankRepo, $subfleetRepo; @@ -37,23 +41,26 @@ class SubfleetController extends Controller * SubfleetController constructor. * @param AircraftRepository $aircraftRepo * @param FleetService $fleetSvc - * @param RankRepository $rankRepo - * @param SubfleetRepository $subfleetRepo * @param FareRepository $fareRepo * @param FareService $fareSvc + * @param ImporterService $importSvc + * @param RankRepository $rankRepo + * @param SubfleetRepository $subfleetRepo */ public function __construct( AircraftRepository $aircraftRepo, FleetService $fleetSvc, - RankRepository $rankRepo, - SubfleetRepository $subfleetRepo, FareRepository $fareRepo, - FareService $fareSvc + FareService $fareSvc, + ImporterService $importSvc, + RankRepository $rankRepo, + SubfleetRepository $subfleetRepo ) { $this->aircraftRepo = $aircraftRepo; $this->fareRepo = $fareRepo; $this->fareSvc = $fareSvc; $this->fleetSvc = $fleetSvc; + $this->importSvc = $importSvc; $this->rankRepo = $rankRepo; $this->subfleetRepo = $subfleetRepo; } @@ -133,7 +140,6 @@ class SubfleetController extends Controller $subfleet = $this->subfleetRepo->create($input); Flash::success('Subfleet saved successfully.'); - return redirect(route('admin.subfleets.edit', ['id' => $subfleet->id])); } @@ -148,12 +154,10 @@ class SubfleetController extends Controller if (empty($subfleet)) { Flash::error('Subfleet not found'); - return redirect(route('admin.subfleets.index')); } $avail_fares = $this->getAvailFares($subfleet); - return view('admin.subfleets.show', [ 'subfleet' => $subfleet, 'avail_fares' => $avail_fares, @@ -171,7 +175,6 @@ class SubfleetController extends Controller if (empty($subfleet)) { Flash::error('Subfleet not found'); - return redirect(route('admin.subfleets.index')); } @@ -200,14 +203,12 @@ class SubfleetController extends Controller if (empty($subfleet)) { Flash::error('Subfleet not found'); - return redirect(route('admin.subfleets.index')); } $this->subfleetRepo->update($request->all(), $id); Flash::success('Subfleet updated successfully.'); - return redirect(route('admin.subfleets.index')); } @@ -222,7 +223,6 @@ class SubfleetController extends Controller if (empty($subfleet)) { Flash::error('Subfleet not found'); - return redirect(route('admin.subfleets.index')); } @@ -231,17 +231,43 @@ class SubfleetController extends Controller $aircraft = $this->aircraftRepo->findWhere(['subfleet_id' => $id], ['id']); if ($aircraft->count() > 0) { Flash::error('There are aircraft still assigned to this subfleet, you can\'t delete it!')->important(); - return redirect(route('admin.subfleets.index')); } $this->subfleetRepo->delete($id); Flash::success('Subfleet deleted successfully.'); - return redirect(route('admin.subfleets.index')); } + /** + * + * @param Request $request + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \League\Csv\Exception + */ + public function import(Request $request) + { + $logs = [ + 'success' => [], + 'failed' => [], + ]; + + if ($request->isMethod('post')) { + $path = Storage::putFileAs( + 'import', $request->file('csv_file'), 'subfleets' + ); + + $path = storage_path('app/'.$path); + Log::info('Uploaded flights import file to '.$path); + $logs = $this->importSvc->importSubfleets($path); + } + + return view('admin.subfleets.import', [ + 'logs' => $logs, + ]); + } + /** * @param Subfleet $subfleet * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View @@ -251,7 +277,6 @@ class SubfleetController extends Controller $subfleet->refresh(); $avail_ranks = $this->getAvailRanks($subfleet); - return view('admin.subfleets.ranks', [ 'subfleet' => $subfleet, 'avail_ranks' => $avail_ranks, @@ -267,7 +292,6 @@ class SubfleetController extends Controller $subfleet->refresh(); $avail_fares = $this->getAvailFares($subfleet); - return view('admin.subfleets.fares', [ 'subfleet' => $subfleet, 'avail_fares' => $avail_fares, @@ -321,7 +345,6 @@ class SubfleetController extends Controller protected function return_expenses_view(?Subfleet $subfleet) { $subfleet->refresh(); - return view('admin.subfleets.expenses', [ 'subfleet' => $subfleet, ]); @@ -377,7 +400,6 @@ class SubfleetController extends Controller $subfleet = $this->subfleetRepo->findWithoutFail($id); if (empty($subfleet)) { return $this->return_fares_view($subfleet); - //return view('admin.aircraft.fares', ['fares' => []]); } if ($request->isMethod('get')) { diff --git a/app/Interfaces/ImportExport.php b/app/Interfaces/ImportExport.php new file mode 100644 index 00000000..98722e35 --- /dev/null +++ b/app/Interfaces/ImportExport.php @@ -0,0 +1,121 @@ +first(); + } + + /** + * @return array + */ + public function getColumns() + { + return static::$columns; + } + + /** + * Set a key-value pair to an array + * @param $kvp_str + * @param array $arr + */ + protected function kvpToArray($kvp_str, array &$arr) + { + $item = explode('=', $kvp_str); + if (\count($item) === 1) { # just a list? + $arr[] = trim($item[0]); + } else { # actually a key-value pair + $k = trim($item[0]); + $v = trim($item[1]); + $arr[$k] = $v; + } + } + + /** + * Parse a multi column values field. E.g: + * Y?price=200&cost=100; F?price=1200 + * or + * gate=B32;cost index=100 + * + * Converted into a multi-dimensional array + * + * @param $field + * @return array|string + */ + public function parseMultiColumnValues($field) + { + $ret = []; + $split_values = explode(';', $field); + + # No multiple values in here, just a straight value + if (\count($split_values) === 1) { + return [$split_values[0]]; + } + + foreach ($split_values as $value) { + # This isn't in the query string format, so it's + # just a straight key-value pair set + if (strpos($value, '?') === false) { + $this->kvpToArray($value, $ret); + continue; + } + + # This contains the query string, which turns it + # into the multi-level array + + $query_str = explode('?', $value); + $parent = trim($query_str[0]); + + $children = []; + $kvp = explode('&', trim($query_str[1])); + foreach ($kvp as $items) { + $this->kvpToArray($items, $children); + } + + $ret[$parent] = $children; + } + + return $ret; + } +} diff --git a/app/Models/Aircraft.php b/app/Models/Aircraft.php index 03defed5..0c042fd3 100644 --- a/app/Models/Aircraft.php +++ b/app/Models/Aircraft.php @@ -7,6 +7,7 @@ use App\Models\Enums\AircraftStatus; use App\Models\Traits\ExpensableTrait; /** + * @property int id * @property mixed subfleet_id * @property string name * @property string icao @@ -38,8 +39,6 @@ class Aircraft extends Model /** * The attributes that should be casted to native types. - * - * @var array */ protected $casts = [ 'subfleet_id' => 'integer', @@ -50,8 +49,6 @@ class Aircraft extends Model /** * Validation rules - * - * @var array */ public static $rules = [ 'subfleet_id' => 'required', diff --git a/app/Models/Airline.php b/app/Models/Airline.php index 4de3ea62..aa553bf1 100644 --- a/app/Models/Airline.php +++ b/app/Models/Airline.php @@ -8,6 +8,7 @@ use App\Models\Traits\JournalTrait; /** * Class Airline + * @property mixed id * @property string code * @property string icao * @property string iata diff --git a/app/Models/Enums/FlightType.php b/app/Models/Enums/FlightType.php index 2a87e899..562a5671 100644 --- a/app/Models/Enums/FlightType.php +++ b/app/Models/Enums/FlightType.php @@ -19,4 +19,31 @@ class FlightType extends Enum FlightType::CARGO => 'Cargo', FlightType::CHARTER => 'Charter', ]; + + /** + * Return value from P, C or H + * @param $code + * @return int + */ + public static function getFromCode($code): int + { + if(is_numeric($code)) { + return (int) $code; + } + + $code = strtolower($code); + if($code === 'p') { + return self::PASSENGER; + } + + if ($code === 'c') { + return self::CARGO; + } + + if($code === 'h') { + return self::CHARTER; + } + + return self::PASSENGER; + } } diff --git a/app/Models/Fare.php b/app/Models/Fare.php index 0b16b169..be4904f6 100644 --- a/app/Models/Fare.php +++ b/app/Models/Fare.php @@ -33,7 +33,7 @@ class Fare extends Model ]; public static $rules = [ - 'code' => 'required', + 'code' => 'required|unique', 'name' => 'required', ]; diff --git a/app/Models/Subfleet.php b/app/Models/Subfleet.php index ab5edf3d..ed93054f 100644 --- a/app/Models/Subfleet.php +++ b/app/Models/Subfleet.php @@ -21,8 +21,8 @@ class Subfleet extends Model protected $fillable = [ 'airline_id', - 'name', 'type', + 'name', 'fuel_type', 'ground_handling_multiplier', 'cargo_capacity', @@ -40,8 +40,8 @@ class Subfleet extends Model ]; protected static $rules = [ + 'type' => 'required|unique', 'name' => 'required', - 'type' => 'required', 'ground_handling_multiplier' => 'nullable|numeric', ]; diff --git a/app/Routes/admin.php b/app/Routes/admin.php index e82fd51d..57a604b8 100644 --- a/app/Routes/admin.php +++ b/app/Routes/admin.php @@ -10,15 +10,17 @@ Route::group([ Route::resource('airlines', 'AirlinesController'); Route::match(['get', 'post', 'put'], 'airports/fuel', 'AirportController@fuel'); - Route::resource('airports', 'AirportController'); + Route::match(['get', 'post'], 'airports/import', 'AirportController@import')->name('airports.import'); Route::match(['get', 'post', 'put', 'delete'], 'airports/{id}/expenses', 'AirportController@expenses'); + Route::resource('airports', 'AirportController'); # Awards Route::resource('awards', 'AwardController'); # aircraft and fare associations - Route::resource('aircraft', 'AircraftController'); + Route::match(['get', 'post'], 'aircraft/import', 'AircraftController@import')->name('aircraft.import'); Route::match(['get', 'post', 'put', 'delete'], 'aircraft/{id}/expenses', 'AircraftController@expenses'); + Route::resource('aircraft', 'AircraftController'); # expenses Route::resource('expenses', 'ExpenseController'); @@ -30,10 +32,11 @@ Route::group([ Route::resource('finances', 'FinanceController'); # flights and aircraft associations - Route::resource('flights', 'FlightController'); + 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'); Route::match(['get', 'post', 'put', 'delete'], 'flights/{id}/subfleets', 'FlightController@subfleets'); + Route::resource('flights', 'FlightController'); Route::resource('flightfields', 'FlightFieldController'); @@ -55,10 +58,11 @@ Route::group([ Route::match(['post', 'put'], 'settings', 'SettingsController@update')->name('settings.update'); # subfleet - Route::resource('subfleets', 'SubfleetController'); + Route::match(['get', 'post'], 'subfleets/import', 'SubfleetController@import')->name('subfleets.import'); Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/expenses', 'SubfleetController@expenses'); Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/fares', 'SubfleetController@fares'); Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/ranks', 'SubfleetController@ranks'); + Route::resource('subfleets', 'SubfleetController'); Route::resource('users', 'UserController'); Route::get('users/{id}/regen_apikey', diff --git a/app/Services/FlightService.php b/app/Services/FlightService.php index dbf9431a..78ec454b 100644 --- a/app/Services/FlightService.php +++ b/app/Services/FlightService.php @@ -114,15 +114,15 @@ class FlightService extends Service /** * Update any custom PIREP fields - * @param Flight $flight_id - * @param array $field_values + * @param Flight $flight + * @param array $field_values */ - public function updateCustomFields(Flight $flight_id, array $field_values): void + public function updateCustomFields(Flight $flight, array $field_values): void { foreach ($field_values as $fv) { FlightFieldValue::updateOrCreate( [ - 'flight_id' => $flight_id, + 'flight_id' => $flight->id, 'name' => $fv['name'], ], [ diff --git a/app/Services/Import/AircraftImporter.php b/app/Services/Import/AircraftImporter.php new file mode 100644 index 00000000..ff40f4f5 --- /dev/null +++ b/app/Services/Import/AircraftImporter.php @@ -0,0 +1,90 @@ + $type])->first(); + if (!$subfleet) { + $subfleet = new Subfleet([ + 'type' => $type, + 'name' => $type, + ]); + + $subfleet->save(); + } + + return $subfleet; + } + + /** + * Import a flight, parse out the different rows + * @param array $row + * @param int $index + * @return bool + */ + public function import(array $row, $index) + { + $subfleet = $this->getSubfleet($row['subfleet']); + + $row['subfleet_id'] = $subfleet->id; + + # Generate a hex code + if(!$row['hex_code']) { + $row['hex_code'] = ICAO::createHexCode(); + } + + # Set a default status + if($row['status'] === null) { + $row['status'] = AircraftStatus::ACTIVE; + } + + # Just set its state right now as parked + $row['state'] = AircraftState::PARKED; + + # Try to add or update + $aircraft = Aircraft::firstOrNew([ + 'registration' => $row['registration'], + ], $row); + + try { + $aircraft->save(); + } catch(\Exception $e) { + $this->status = 'Error in row '.$index.': '.$e->getMessage(); + return false; + } + + $this->status = 'Imported '.$row['registration'].' '.$row['name']; + return true; + } +} diff --git a/app/Services/Import/AirportImporter.php b/app/Services/Import/AirportImporter.php new file mode 100644 index 00000000..69f5cc41 --- /dev/null +++ b/app/Services/Import/AirportImporter.php @@ -0,0 +1,55 @@ + $row['icao'] + ], $row); + + try { + $airport->save(); + } catch(\Exception $e) { + $this->status = 'Error in row '.$index.': '.$e->getMessage(); + return false; + } + + $this->status = 'Imported ' . $row['icao']; + return true; + } +} diff --git a/app/Services/Import/FlightImporter.php b/app/Services/Import/FlightImporter.php new file mode 100644 index 00000000..e678c4ec --- /dev/null +++ b/app/Services/Import/FlightImporter.php @@ -0,0 +1,168 @@ +airlineRepo = app(AirlineRepository::class); + $this->fareSvc = app(FareService::class); + $this->flightSvc = app(FlightService::class); + } + + /** + * Import a flight, parse out the different rows + * @param array $row + * @param int $index + * @return bool + */ + public function import(array $row, $index) + { + // Get the airline ID from the ICAO code + $airline = $this->getAirline($row['airline']); + + // Try to find this flight + $flight = Flight::firstOrNew([ + 'airline_id' => $airline->id, + 'flight_number' => $row['flight_number'], + 'route_code' => $row['route_code'], + 'route_leg' => $row['route_leg'], + ], $row); + + // Any specific transformations + // Flight type can be set to P - Passenger, C - Cargo, or H - Charter + $flight->setAttribute('flight_type', FlightType::getFromCode($row['flight_type'])); + $flight->setAttribute('active', get_truth_state($row['active'])); + + try { + $flight->save(); + } catch (\Exception $e) { + $this->status = 'Error in row '.$index.': '.$e->getMessage(); + return false; + } + + $this->processSubfleets($flight, $row['subfleets']); + $this->processFares($flight, $row['fares']); + $this->processFields($flight, $row['fields']); + + $this->status = 'Imported row '.$index; + return true; + } + + /** + * Parse out all of the subfleets and associate them to the flight + * The subfleet is created if it doesn't exist + * @param Flight $flight + * @param $col + */ + protected function processSubfleets(Flight &$flight, $col): void + { + $count = 0; + $subfleets = $this->parseMultiColumnValues($col); + foreach($subfleets as $subfleet_type) { + $subfleet = Subfleet::firstOrNew( + ['type' => $subfleet_type], + ['name' => $subfleet_type] + ); + + $subfleet->save(); + + # sync + $flight->subfleets()->syncWithoutDetaching([$subfleet->id]); + $count ++; + } + + Log::info('Subfleets added/processed: '.$count); + } + + /** + * Parse all of the fares in the multi-format + * @param Flight $flight + * @param $col + */ + protected function processFares(Flight &$flight, $col): void + { + $fares = $this->parseMultiColumnValues($col); + foreach ($fares as $fare_code => $fare_attributes) { + if (\is_int($fare_code)) { + $fare_code = $fare_attributes; + $fare_attributes = []; + } + + $fare = Fare::firstOrNew(['code' => $fare_code], ['name' => $fare_code]); + $this->fareSvc->setForFlight($flight, $fare, $fare_attributes); + } + } + + /** + * Parse all of the subfields + * @param Flight $flight + * @param $col + */ + protected function processFields(Flight &$flight, $col): void + { + $pass_fields = []; + $fields = $this->parseMultiColumnValues($col); + foreach($fields as $field_name => $field_value) { + $pass_fields[] = [ + 'name' => $field_name, + 'value' => $field_value, + ]; + } + + $this->flightSvc->updateCustomFields($flight, $pass_fields); + } +} diff --git a/app/Services/Import/SubfleetImporter.php b/app/Services/Import/SubfleetImporter.php new file mode 100644 index 00000000..791ca584 --- /dev/null +++ b/app/Services/Import/SubfleetImporter.php @@ -0,0 +1,54 @@ +getAirline($row['airline']); + if(!$airline) { + $this->status = 'Airline '.$row['airline'].' not found, row: '.$index; + return false; + } + + $row['airline_id'] = $airline->id; + + $subfleet = Subfleet::firstOrNew([ + 'type' => $row['type'] + ], $row); + + try { + $subfleet->save(); + } catch(\Exception $e) { + $this->status = 'Error in row '.$index.': '.$e->getMessage(); + return false; + } + + $this->status = 'Imported ' . $row['type']; + return true; + } +} diff --git a/app/Services/ImporterService.php b/app/Services/ImporterService.php index 013c0c10..e6e41855 100644 --- a/app/Services/ImporterService.php +++ b/app/Services/ImporterService.php @@ -2,8 +2,15 @@ namespace App\Services; +use App\Interfaces\ImportExport; use App\Interfaces\Service; +use App\Models\Airport; use App\Repositories\FlightRepository; +use App\Services\Import\AircraftImporter; +use App\Services\Import\AirportImporter; +use App\Services\Import\FlightImporter; +use App\Services\Import\SubfleetImporter; +use League\Csv\Reader; /** * Class ImporterService @@ -24,75 +31,140 @@ class ImporterService extends Service } /** - * Set a key-value pair to an array - * @param $kvp_str - * @param array $arr + * @param $csv_file + * @return Reader + * @throws \League\Csv\Exception */ - protected function setKvp($kvp_str, array &$arr) + public function openCsv($csv_file) { - $item = explode('=', $kvp_str); - if (\count($item) === 1) { # just a list? - $arr[] = trim($item[0]); - } else { # actually a key-value pair - $k = trim($item[0]); - $v = trim($item[1]); - $arr[$k] = $v; - } + $reader = Reader::createFromPath($csv_file); + $reader->setDelimiter(','); + $reader->setEnclosure('"'); + + return $reader; } /** - * Parse a multi column values field. E.g: - * Y?price=200&cost=100; F?price=1200 - * or - * gate=B32;cost index=100 - * - * Converted into a multi-dimensional array - * - * @param $field - * @return array|string + * Run the actual importer + * @param Reader $reader + * @param ImportExport $importer + * @return array */ - public function parseMultiColumnValues($field) + protected function runImport(Reader $reader, ImportExport $importer): array { - $ret = []; - $split_values = explode(';', $field); + $import_report = [ + 'success' => [], + 'failed' => [], + ]; - # No multiple values in here, just a straight value - if (\count($split_values) === 1) { - return $split_values[0]; - } + $cols = $importer->getColumns(); + $first_header = $cols[0]; - foreach ($split_values as $value) { - # This isn't in the query string format, so it's - # just a straight key-value pair set - if (strpos($value, '?') === false) { - $this->setKvp($value, $ret); + $records = $reader->getRecords($cols); + foreach ($records as $offset => $row) { + // check if the first row being read is the header + if ($row[$first_header] === $first_header) { continue; } - # This contains the query string, which turns it - # into the multi-level array - - $query_str = explode('?', $value); - $parent = trim($query_str[0]); - - $children = []; - $kvp = explode('&', trim($query_str[1])); - foreach ($kvp as $items) { - $this->setKvp($items, $children); + $success = $importer->import($row, $offset); + if ($success) { + $import_report['success'][] = $importer->status; + } else { + $import_report['failed'][] = $importer->status; } - - $ret[$parent] = $children; } - return $ret; + return $import_report; + } + + /** + * Import aircraft + * @param string $csv_file + * @param bool $delete_previous + * @return mixed + * @throws \League\Csv\Exception + */ + public function importAircraft($csv_file, bool $delete_previous = true) + { + if ($delete_previous) { + # TODO: delete airports + } + + $reader = $this->openCsv($csv_file); + if (!$reader) { + return false; + } + + $importer = new AircraftImporter(); + return $this->runImport($reader, $importer); + } + + /** + * Import airports + * @param string $csv_file + * @param bool $delete_previous + * @return mixed + * @throws \League\Csv\Exception + */ + public function importAirports($csv_file, bool $delete_previous = true) + { + if ($delete_previous) { + Airport::truncate(); + } + + $reader = $this->openCsv($csv_file); + if (!$reader) { + return false; + } + + $importer = new AirportImporter(); + return $this->runImport($reader, $importer); } /** * Import flights - * @param $csv_str - * @param bool $delete_previous + * @param string $csv_file + * @param bool $delete_previous + * @return mixed + * @throws \League\Csv\Exception */ - public function importFlights($csv_str, bool $delete_previous = true) + public function importFlights($csv_file, bool $delete_previous = true) { + if ($delete_previous) { + # TODO: Delete all from: flights, flight_field_values + } + + $reader = $this->openCsv($csv_file); + if (!$reader) { + # TODO: Throw an error + return false; + } + + $importer = new FlightImporter(); + return $this->runImport($reader, $importer); + } + + /** + * Import subfleets + * @param string $csv_file + * @param bool $delete_previous + * @return mixed + * @throws \League\Csv\Exception + */ + public function importSubfleets($csv_file, bool $delete_previous = true) + { + if ($delete_previous) { + # TODO: Cleanup subfleet data + } + + $reader = $this->openCsv($csv_file); + if (!$reader) { + # TODO: Throw an error + return false; + } + + $importer = new SubfleetImporter(); + return $this->runImport($reader, $importer); } } diff --git a/composer.json b/composer.json index c2271b9a..0ec6f8f3 100755 --- a/composer.json +++ b/composer.json @@ -38,7 +38,8 @@ "markrogoyski/math-php": "^0.38.0", "akaunting/money": "^1.0", "igaster/laravel-theme": "^2.0", - "anhskohbo/no-captcha": "^3.0" + "anhskohbo/no-captcha": "^3.0", + "league/csv": "^9.1" }, "require-dev": { "phpunit/phpunit": "~7.0", diff --git a/composer.lock b/composer.lock index d43d692d..65e32f3b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "55cf432213722ec0f7ad7f80a4edc648", + "content-hash": "833c46d2dbb420462272d9a86fa28ff9", "packages": [ { "name": "akaunting/money", @@ -168,7 +168,7 @@ "Arrilot\\Widgets\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -223,7 +223,7 @@ "Cache\\Adapter\\Common\\": "" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -291,7 +291,7 @@ "/Tests/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -353,7 +353,7 @@ "/Tests/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -408,7 +408,7 @@ "Cache\\TagInterop\\": "" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -527,7 +527,7 @@ "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -643,7 +643,7 @@ "Cron\\": "src/Cron/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -703,7 +703,7 @@ "Egulias\\EmailValidator\\": "EmailValidator" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -795,7 +795,7 @@ "Firebase\\JWT\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -863,7 +863,7 @@ "src/Google/Service/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "Apache-2.0" ], @@ -945,7 +945,7 @@ "Google\\Auth\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "Apache-2.0" ], @@ -999,7 +999,7 @@ "GuzzleHttp\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1057,7 +1057,7 @@ "src/functions_include.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1112,7 +1112,7 @@ "src/functions_include.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1174,7 +1174,7 @@ "Hashids\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1309,7 +1309,7 @@ "Irazasyed\\LaravelGAMP\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1356,7 +1356,7 @@ "Jackiedo\\Timezonelist\\": "src/Jackiedo/Timezonelist" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1409,7 +1409,7 @@ "stubs/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1460,7 +1460,7 @@ "Joshbrw\\LaravelModuleInstaller\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1493,7 +1493,7 @@ "Traitor\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1552,7 +1552,7 @@ "src/Laracasts/Flash/functions.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1753,7 +1753,7 @@ "src/helpers.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -1771,6 +1771,73 @@ "homepage": "https://laravelcollective.com", "time": "2018-02-12T14:19:42+00:00" }, + { + "name": "league/csv", + "version": "9.1.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/csv.git", + "reference": "0d0b12f1a0093a6c39014a5d118f6ba4274539ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/0d0b12f1a0093a6c39014a5d118f6ba4274539ee", + "reference": "0d0b12f1a0093a6c39014a5d118f6ba4274539ee", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=7.0.10" + }, + "require-dev": { + "ext-curl": "*", + "friendsofphp/php-cs-fixer": "^2.0", + "phpstan/phpstan": "^0.9.2", + "phpstan/phpstan-phpunit": "^0.9.4", + "phpstan/phpstan-strict-rules": "^0.9.0", + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Csv\\": "src" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://github.com/nyamsprod/", + "role": "Developer" + } + ], + "description": "Csv data manipulation made easy in PHP", + "homepage": "http://csv.thephpleague.com", + "keywords": [ + "csv", + "export", + "filter", + "import", + "read", + "write" + ], + "time": "2018-03-12T07:20:01+00:00" + }, { "name": "league/flysystem", "version": "1.0.43", @@ -1961,7 +2028,7 @@ "League\\ISO3166\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -2154,7 +2221,7 @@ "VaCentral\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -2311,7 +2378,7 @@ "src/helpers.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -2420,7 +2487,7 @@ "Http\\Discovery\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -2473,7 +2540,7 @@ "PhpUnitsOfMeasure\\": "source/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -2532,7 +2599,7 @@ "phpseclib\\": "phpseclib/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -2688,7 +2755,7 @@ "PragmaRX\\Yaml\\Tests\\": "tests/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -2749,7 +2816,7 @@ "Prettus\\Repository\\": "src/Prettus/Repository/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -2838,7 +2905,7 @@ "Psr\\Cache\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -3102,7 +3169,7 @@ "Ramsey\\Uuid\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -3385,7 +3452,7 @@ "Spatie\\Pjax\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -4036,7 +4103,7 @@ "bootstrap.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -4092,7 +4159,7 @@ "bootstrap.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -4611,7 +4678,7 @@ "TheIconic\\Tracking\\GoogleAnalytics\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -4656,7 +4723,7 @@ "TijsVerkoyen\\CssToInlineStyles\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -4697,7 +4764,7 @@ "Tivie\\OS\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "APACHE 2.0" ], @@ -4815,7 +4882,7 @@ "src/vierbergenlars/SemVer/internal.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -4920,7 +4987,7 @@ "Webpatser\\Uuid": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -4976,7 +5043,7 @@ "/Tests/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5047,7 +5114,7 @@ "Barryvdh\\LaravelIdeHelper\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5204,7 +5271,7 @@ "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5274,7 +5341,7 @@ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5331,7 +5398,7 @@ "Whoops\\": "src/Whoops/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5479,7 +5546,7 @@ "JakubOnderka\\PhpConsoleColor": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-2-Clause" ], @@ -5523,7 +5590,7 @@ "JakubOnderka\\PhpConsoleHighlighter": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5632,7 +5699,7 @@ "src/DeepCopy/deep_copy.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5683,7 +5750,7 @@ "NunoMaduro\\Collision\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5902,7 +5969,7 @@ ] } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -5998,7 +6065,7 @@ "Prophecy\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -6068,7 +6135,7 @@ "src/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -6116,7 +6183,7 @@ "src/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -6207,7 +6274,7 @@ "src/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -6257,7 +6324,7 @@ "src/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -6391,7 +6458,7 @@ "src/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -6488,7 +6555,7 @@ "src/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -6551,7 +6618,7 @@ "src/" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], @@ -7101,7 +7168,7 @@ "Webmozart\\Assert\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "MIT" ], @@ -7155,7 +7222,7 @@ "src/functions.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "http://packagist.org/downloads/", "license": [ "Apache2" ], diff --git a/config/filesystems.php b/config/filesystems.php index 75b50022..442da8f8 100755 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -1,46 +1,8 @@ 'local', - - /* - |-------------------------------------------------------------------------- - | Default Cloud Filesystem Disk - |-------------------------------------------------------------------------- - | - | Many applications store files both locally and in the cloud. For this - | reason, you may specify a default "cloud" driver here. This driver - | will be bound as the Cloud disk implementation in the container. - | - */ - 'cloud' => 's3', - - /* - |-------------------------------------------------------------------------- - | Filesystem Disks - |-------------------------------------------------------------------------- - | - | Here you may configure as many filesystem "disks" as you wish, and you - | may even configure multiple disks of the same driver. Defaults have - | been setup for each driver as an example of the required options. - | - */ - 'disks' => [ 'local' => [ @@ -50,7 +12,7 @@ return [ 'public' => [ 'driver' => 'local', - 'root' => storage_path('app/public'), + 'root' => app_path('public/assets/upload'), 'visibility' => 'public', ], @@ -61,7 +23,5 @@ return [ 'region' => 'your-region', 'bucket' => 'your-bucket', ], - ], - ]; diff --git a/resources/views/admin/aircraft/import.blade.php b/resources/views/admin/aircraft/import.blade.php new file mode 100644 index 00000000..0d10c53a --- /dev/null +++ b/resources/views/admin/aircraft/import.blade.php @@ -0,0 +1,5 @@ +@extends('admin.app') +@section('title', 'Import Aircraft') +@section('content') + @include('admin.shared.import', ['route' => 'admin.aircraft.import']) +@endsection diff --git a/resources/views/admin/aircraft/index.blade.php b/resources/views/admin/aircraft/index.blade.php index 53426925..0cf7cc58 100644 --- a/resources/views/admin/aircraft/index.blade.php +++ b/resources/views/admin/aircraft/index.blade.php @@ -1,17 +1,10 @@ @extends('admin.app') - @section('title', 'Aircraft') + @section('actions') -
  • - - - Subfleets -
  • -
  • - - - New Aircraft -
  • +
  • Import from CSV
  • +
  • Subfleets
  • +
  • New Aircraft
  • @endsection @section('content') diff --git a/resources/views/admin/airports/import.blade.php b/resources/views/admin/airports/import.blade.php new file mode 100644 index 00000000..f66aa08d --- /dev/null +++ b/resources/views/admin/airports/import.blade.php @@ -0,0 +1,5 @@ +@extends('admin.app') +@section('title', 'Import Airports') +@section('content') + @include('admin.shared.import', ['route' => 'admin.airports.import']) +@endsection diff --git a/resources/views/admin/airports/index.blade.php b/resources/views/admin/airports/index.blade.php index 33794b16..b3bcd552 100644 --- a/resources/views/admin/airports/index.blade.php +++ b/resources/views/admin/airports/index.blade.php @@ -1,12 +1,9 @@ @extends('admin.app') - @section('title', 'Airports') + @section('actions') -
  • - - - Add New -
  • +
  • Import from CSV
  • +
  • Add New
  • @endsection @section('content') diff --git a/resources/views/admin/flights/import.blade.php b/resources/views/admin/flights/import.blade.php new file mode 100644 index 00000000..79282f37 --- /dev/null +++ b/resources/views/admin/flights/import.blade.php @@ -0,0 +1,5 @@ +@extends('admin.app') +@section('title', 'Import Flights') +@section('content') + @include('admin.shared.import', ['route' => 'admin.flights.import']) +@endsection diff --git a/resources/views/admin/flights/index.blade.php b/resources/views/admin/flights/index.blade.php index 919b8587..97163a23 100644 --- a/resources/views/admin/flights/index.blade.php +++ b/resources/views/admin/flights/index.blade.php @@ -1,7 +1,8 @@ @extends('admin.app') - @section('title', 'Flights') + @section('actions') +
  • Import from CSV
  • Fields
  • diff --git a/resources/views/admin/shared/import.blade.php b/resources/views/admin/shared/import.blade.php new file mode 100644 index 00000000..fe7da239 --- /dev/null +++ b/resources/views/admin/shared/import.blade.php @@ -0,0 +1,32 @@ +
    +
    + {{ Form::open(['method' => 'post', 'route' => $route, 'files' => true]) }} + +
    +
    + {{ Form::label('csv_file', 'Chose a CSV file to import') }} + {{ Form::file('csv_file', ['accept' => '.csv']) }} +
    + +
    +
    + {{ Form::button('Start Import', ['type' => 'submit', 'class' => 'btn btn-success']) }} +
    +
    + + {{ Form::close() }} + +
    +

    Logs

    + @foreach($logs['success'] as $line) +

    {{ $line }}

    + @endforeach + +

    Errors

    + @foreach($logs['failed'] as $line) +

    {{ $line }}

    + @endforeach +
    +
    +
    +
    diff --git a/resources/views/admin/subfleets/import.blade.php b/resources/views/admin/subfleets/import.blade.php new file mode 100644 index 00000000..42e0d477 --- /dev/null +++ b/resources/views/admin/subfleets/import.blade.php @@ -0,0 +1,5 @@ +@extends('admin.app') +@section('title', 'Import Subfleets') +@section('content') + @include('admin.shared.import', ['route' => 'admin.subfleets.import']) +@endsection diff --git a/resources/views/admin/subfleets/index.blade.php b/resources/views/admin/subfleets/index.blade.php index eda80220..7ec61fb4 100644 --- a/resources/views/admin/subfleets/index.blade.php +++ b/resources/views/admin/subfleets/index.blade.php @@ -2,6 +2,8 @@ @section('title', 'Subfleets') @section('actions') +
  • Import from CSV +
  • Add New diff --git a/storage/app/.gitignore b/storage/app/.gitignore index 8fa24b09..d73fdf19 100755 --- a/storage/app/.gitignore +++ b/storage/app/.gitignore @@ -1,4 +1,5 @@ * +!import/ !public/ !.gitignore .xml diff --git a/storage/app/import/.gitignore b/storage/app/import/.gitignore new file mode 100755 index 00000000..d6b7ef32 --- /dev/null +++ b/storage/app/import/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/ImporterTest.php b/tests/ImporterTest.php index 5f4a410e..64889cd5 100644 --- a/tests/ImporterTest.php +++ b/tests/ImporterTest.php @@ -1,16 +1,50 @@ importerSvc = app(\App\Services\ImporterService::class); + parent::setUp(); + $this->importBaseClass = new \App\Interfaces\ImportExport(); + $this->importSvc = app(\App\Services\ImporterService::class); + $this->fareSvc = app(\App\Services\FareService::class); + } + + /** + * Add some of the basic data needed to properly import the flights.csv file + * @return mixed + */ + protected function insertFlightsScaffoldData() + { + $fare_svc = app(FareService::class); + + $al = [ + 'icao' => 'VMS', + 'name' => 'phpVMS Airlines', + ]; + + $airline = factory(App\Models\Airline::class)->create($al); + $subfleet = factory(App\Models\Subfleet::class)->create(['type' => 'A32X']); + + # Add the economy class + $fare_economy = factory(App\Models\Fare::class)->create(['code' => 'Y', 'capacity' => 150]); + $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; } /** @@ -22,7 +56,7 @@ class ImporterTest extends TestCase $tests = [ [ 'input' => 'gate', - 'expected' => 'gate' + 'expected' => ['gate'] ], [ 'input' => 'gate;cost index', @@ -61,12 +95,155 @@ class ImporterTest extends TestCase 'price' => 1200 ] ] - ] + ], + [ + 'input' => 'Y; F?price=1200', + 'expected' => [ + 0 => 'Y', + 'F' => [ + 'price' => 1200 + ] + ] + ], + [ + 'input' => 'Departure Gate=4;Arrival Gate=C61', + 'expected' => [ + 'Departure Gate' => '4', + 'Arrival Gate' => 'C61', + ] + ], ]; foreach($tests as $test) { - $parsed = $this->importerSvc->parseMultiColumnValues($test['input']); + $parsed = $this->importBaseClass->parseMultiColumnValues($test['input']); $this->assertEquals($parsed, $test['expected']); } } + + /** + * Test the flight importer + * @throws \League\Csv\Exception + */ + public function testFlightImporter(): void + { + $airline = $this->insertFlightsScaffoldData(); + + $file_path = base_path('tests/data/flights.csv'); + $this->importSvc->importFlights($file_path); + + // See if it imported + $flight = \App\Models\Flight::where([ + 'airline_id' => $airline->id, + 'flight_number' => '1972' + ])->first(); + + $this->assertNotNull($flight); + + // Check the flight itself + $this->assertEquals('KAUS', $flight->dpt_airport_id); + $this->assertEquals('KJFK', $flight->arr_airport_id); + $this->assertEquals('0810 CST', $flight->dpt_time); + $this->assertEquals('1235 EST', $flight->arr_time); + $this->assertEquals('350', $flight->level); + $this->assertEquals('1477', $flight->distance); + $this->assertEquals('207', $flight->flight_time); + $this->assertEquals(FlightType::PASSENGER, $flight->flight_type); + $this->assertEquals('ILEXY2 ZENZI LFK ELD J29 MEM Q29 JHW J70 STENT J70 MAGIO J70 LVZ LENDY6', $flight->route); + $this->assertEquals('Just a flight', $flight->notes); + $this->assertEquals(true, $flight->active); + + // Check the custom fields entered + $fields = \App\Models\FlightFieldValue::where([ + 'flight_id' => $flight->id, + ])->get(); + + $this->assertCount(2, $fields); + $dep_gate = $fields->where('name', 'Departure Gate')->first(); + $this->assertEquals('4', $dep_gate['value']); + + $dep_gate = $fields->where('name', 'Arrival Gate')->first(); + $this->assertEquals('C41', $dep_gate['value']); + + // Check the fare class + $fares = $this->fareSvc->getForFlight($flight); + $this->assertCount(2, $fares); + + $first = $fares->where('code', 'Y')->first(); + $this->assertEquals(300, $first->price); + $this->assertEquals(100, $first->cost); + $this->assertEquals(130, $first->capacity); + + $first = $fares->where('code', 'F')->first(); + $this->assertEquals(600, $first->price); + $this->assertEquals(400, $first->cost); + $this->assertEquals(10, $first->capacity); + + // Check the subfleets + $subfleets = $flight->subfleets; + $this->assertCount(1, $subfleets); + } + + /** + * + * @throws \League\Csv\Exception + */ + public function testAircraftImporter() + { + $subfleet = factory(App\Models\Subfleet::class)->create(['type' => 'A32X']); + + $file_path = base_path('tests/data/aircraft.csv'); + $this->importSvc->importAircraft($file_path); + + // See if it imported + $aircraft = \App\Models\Aircraft::where([ + 'registration' => 'N309US', + ])->first(); + + $this->assertNotNull($aircraft); + $this->assertEquals($subfleet->id, $aircraft->id); + $this->assertEquals('A320-211', $aircraft->name); + $this->assertEquals('N309US', $aircraft->registration); + } + + /** + * + * @throws \League\Csv\Exception + */ + public function testAirportImporter() + { + $file_path = base_path('tests/data/airports.csv'); + $this->importSvc->importAirports($file_path); + + // See if it imported + $airport = \App\Models\Airport::where([ + 'id' => 'KAUS', + ])->first(); + + $this->assertNotNull($airport); + $this->assertEquals('KAUS', $airport->id); + $this->assertEquals('AUS', $airport->iata); + $this->assertEquals('KAUS', $airport->icao); + } + + /** + * Test importing the subfleets + * @throws \League\Csv\Exception + */ + public function testSubfleetImporter(): void + { + $airline = factory(App\Models\Airline::class)->create(['icao' => 'VMS']); + + $file_path = base_path('tests/data/subfleets.csv'); + $this->importSvc->importSubfleets($file_path); + + // See if it imported + $subfleet = \App\Models\Subfleet::where([ + 'type' => 'A32X', + ])->first(); + + $this->assertNotNull($subfleet); + $this->assertEquals($airline->id, $subfleet->id); + $this->assertEquals('A32X', $subfleet->type); + $this->assertEquals('Airbus A320', $subfleet->name); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index d583d231..d3285b16 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -92,6 +92,24 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase } } + /** + * So we can test private/protected methods + * http://bit.ly/1mr5hMq + * @param $object + * @param $methodName + * @param array $parameters + * @return mixed + * @throws ReflectionException + */ + public function invokeMethod(&$object, $methodName, array $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } + /** * Override the GET call to inject the user API key * @param string $uri diff --git a/tests/data/aircraft.csv b/tests/data/aircraft.csv new file mode 100644 index 00000000..3c758c1f --- /dev/null +++ b/tests/data/aircraft.csv @@ -0,0 +1,2 @@ +subfleet,name,registration,hex_code,status +A32X,A320-211,N309US,, diff --git a/tests/data/airports.csv b/tests/data/airports.csv new file mode 100644 index 00000000..31778f19 --- /dev/null +++ b/tests/data/airports.csv @@ -0,0 +1,2 @@ +iata,icao,name,location,country,timezone,hub,lat,lon +AUS,KAUS,Austin-Bergstrom,"Austin, Texas, USA", United States,America/Chicago,1,30.1945,-97.6699 diff --git a/tests/data/flights.csv b/tests/data/flights.csv new file mode 100644 index 00000000..0e7ef33a --- /dev/null +++ b/tests/data/flights.csv @@ -0,0 +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 diff --git a/tests/data/subfleets.csv b/tests/data/subfleets.csv new file mode 100644 index 00000000..ce3d74b1 --- /dev/null +++ b/tests/data/subfleets.csv @@ -0,0 +1,2 @@ +airline,type,name +VMS,A32X,Airbus A320