diff --git a/app/Exceptions/BidExists.php b/app/Exceptions/BidExists.php new file mode 100644 index 00000000..4b3e8415 --- /dev/null +++ b/app/Exceptions/BidExists.php @@ -0,0 +1,20 @@ +flightRepo = $flightRepo; + $this->flightSvc = $flightSvc; $this->pirepRepo = $pirepRepo; $this->subfleetRepo = $subfleetRepo; $this->userRepo = $userRepo; @@ -80,13 +90,31 @@ class UserController extends RestController /** * Return all of the bids for the passed-in user - * @param $id - * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection + * @param Request $request + * @return mixed + * @throws \App\Exceptions\BidExists + * @throws \App\Services\Exception */ - public function bids($id) + public function bids(Request $request) { - $flights = UserBid::where(['user_id' => $id])->get() - ->pluck('flight'); + $user = $this->userRepo->find($this->getUserId($request)); + + # Add a bid + if ($request->isMethod('PUT')) { + $flight_id = $request->input('flight_id'); + $flight = $this->flightRepo->find($flight_id); + $this->flightSvc->addBid($flight, $user); + } + + elseif ($request->isMethod('DELETE')) { + $flight_id = $request->input('flight_id'); + $flight = $this->flightRepo->find($flight_id); + $this->flightSvc->removeBid($flight, $user); + } + + # Return the flights they currently have bids on + $flights = UserBid::where(['user_id' => $user->id]) + ->get()->pluck('flight'); return FlightResource::collection($flights); } diff --git a/app/Http/Controllers/Frontend/FlightController.php b/app/Http/Controllers/Frontend/FlightController.php index 85286745..a9ccb89c 100644 --- a/app/Http/Controllers/Frontend/FlightController.php +++ b/app/Http/Controllers/Frontend/FlightController.php @@ -8,6 +8,7 @@ use App\Repositories\AirlineRepository; use App\Repositories\AirportRepository; use App\Repositories\Criteria\WhereCriteria; use App\Repositories\FlightRepository; +use App\Services\FlightService; use App\Services\GeoService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -26,17 +27,20 @@ class FlightController extends Controller * @param AirlineRepository $airlineRepo * @param AirportRepository $airportRepo * @param FlightRepository $flightRepo + * @param FlightService $flightSvc * @param GeoService $geoSvc */ public function __construct( AirlineRepository $airlineRepo, AirportRepository $airportRepo, FlightRepository $flightRepo, + FlightService $flightSvc, GeoService $geoSvc ) { $this->airlineRepo = $airlineRepo; $this->airportRepo = $airportRepo; $this->flightRepo = $flightRepo; + $this->flightSvc = $flightSvc; $this->geoSvc = $geoSvc; } diff --git a/app/Routes/api.php b/app/Routes/api.php index a35f81ca..ca705e89 100755 --- a/app/Routes/api.php +++ b/app/Routes/api.php @@ -65,9 +65,15 @@ Route::group(['middleware' => ['api.auth']], function () Route::get('user/fleet', 'UserController@fleet'); Route::get('user/pireps', 'UserController@pireps'); + Route::get('user/bids', 'UserController@bids'); + Route::put('user/bids', 'UserController@bids'); + Route::delete('user/bids', 'UserController@bids'); + Route::get('users/{id}', 'UserController@get'); - Route::get('users/{id}/bids', 'UserController@bids'); Route::get('users/{id}/fleet', 'UserController@fleet'); Route::get('users/{id}/pireps', 'UserController@pireps'); + Route::get('users/{id}/bids', 'UserController@bids'); + Route::put('users/{id}/bids', 'UserController@bids'); + }); diff --git a/app/Services/FlightService.php b/app/Services/FlightService.php index 3aba45ec..8369d53f 100644 --- a/app/Services/FlightService.php +++ b/app/Services/FlightService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Exceptions\BidExists; use App\Models\Flight; use App\Models\User; use App\Models\UserBid; @@ -129,13 +130,14 @@ class FlightService extends BaseService * @param Flight $flight * @param User $user * @return UserBid|null + * @throws \App\Exceptions\BidExists */ public function addBid(Flight $flight, User $user) { # If it's already been bid on, then it can't be bid on again if($flight->has_bid && setting('bids.disable_flight_on_bid')) { Log::info($flight->id . ' already has a bid, skipping'); - return null; + throw new BidExists(); } # See if we're allowed to have multiple bids or not @@ -170,7 +172,6 @@ class FlightService extends BaseService * Remove a bid from a given flight * @param Flight $flight * @param User $user - * @throws Exception */ public function removeBid(Flight $flight, User $user) { diff --git a/public/assets/system/js/system.js b/public/assets/system/js/system.js deleted file mode 100644 index 5360d736..00000000 --- a/public/assets/system/js/system.js +++ /dev/null @@ -1,348 +0,0 @@ -/** - * - * @type {{render_airspace_map, render_live_map, render_route_map}} - */ - -const phpvms = (function() { - - const PLAN_ROUTE_COLOR = '#36b123'; - const ACTUAL_ROUTE_COLOR = '#172aea'; - - const draw_base_map = (opts) => { - - opts = _.defaults(opts, { - render_elem: 'map', - center: [29.98139, -95.33374], - zoom: 5, - maxZoom: 10, - layers: [], - set_marker: false, - }); - - let feature_groups = []; - /*var openaip_airspace_labels = new L.TileLayer.WMS( - "http://{s}.tile.maps.openaip.net/geowebcache/service/wms", { - maxZoom: 14, - minZoom: 12, - layers: 'openaip_approved_airspaces_labels', - tileSize: 1024, - detectRetina: true, - subdomains: '12', - format: 'image/png', - transparent: true - }); - - openaip_airspace_labels.addTo(map);*/ - - const opencyclemap_phys_osm = new L.TileLayer( - 'http://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png?apikey=f09a38fa87514de4890fc96e7fe8ecb1', { - maxZoom: 14, - minZoom: 4, - format: 'image/png', - transparent: true - }); - - feature_groups.push(opencyclemap_phys_osm); - - /*const openaip_cached_basemap = new L.TileLayer("http://{s}.tile.maps.openaip.net/geowebcache/service/tms/1.0.0/openaip_basemap@EPSG%3A900913@png/{z}/{x}/{y}.png", { - maxZoom: 14, - minZoom: 4, - tms: true, - detectRetina: true, - subdomains: '12', - format: 'image/png', - transparent: true - }); - - feature_groups.push(openaip_cached_basemap); - */ - - const openaip_basemap_phys_osm = L.featureGroup(feature_groups); - - let map = L.map('map', { - layers: [openaip_basemap_phys_osm], - center: opts.center, - zoom: opts.zoom, - scrollWheelZoom: false, - }); - - const attrib = L.control.attribution({position: 'bottomleft'}); - attrib.addAttribution("Thunderforest"); - attrib.addAttribution("openAIP"); - attrib.addAttribution("OpenStreetMap contributors"); - attrib.addAttribution("OpenWeatherMap"); - - attrib.addTo(map); - - return map; - }; - - - /** - * Show some popup text when a feature is clicked on - * @param feature - * @param layer - */ - const onFeaturePointClick = (feature, layer) => { - let popup_html = ""; - if (feature.properties && feature.properties.popup) { - popup_html += feature.properties.popup; - } - - layer.bindPopup(popup_html); - }; - - /** - * Show each point as a marker - * @param feature - * @param latlng - * @returns {*} - */ - const pointToLayer = (feature, latlng) => { - return L.circleMarker(latlng, { - radius: 12, - fillColor: "#ff7800", - color: "#000", - weight: 1, - opacity: 1, - fillOpacity: 0.8 - }); - }; - - /** - * - * @param opts - * @private - */ - const _render_route_map = (opts) => { - - opts = _.defaults(opts, { - route_points: null, - planned_route_line: null, - actual_route_points: null, - actual_route_line: null, - render_elem: 'map', - }); - - console.log(opts); - - let map = draw_base_map(opts); - - let geodesicLayer = L.geodesic([], { - weight: 7, - opacity: 0.9, - color: PLAN_ROUTE_COLOR, - steps: 50, - wrap: false, - }).addTo(map); - - geodesicLayer.geoJson(opts.planned_route_line); - - try { - map.fitBounds(geodesicLayer.getBounds()); - } catch (e) { console.log(e); } - - // Draw the route points after - if (opts.route_points !== null) { - let route_points = L.geoJSON(opts.route_points, { - onEachFeature: onFeaturePointClick, - pointToLayer: pointToLayer, - style: { - "color": PLAN_ROUTE_COLOR, - "weight": 5, - "opacity": 0.65, - }, - }); - - route_points.addTo(map); - } - - /** - * draw the actual route - */ - - if (opts.actual_route_line !== null && opts.actual_route_line.features.length > 0) { - let geodesicLayer = L.geodesic([], { - weight: 7, - opacity: 0.9, - color: ACTUAL_ROUTE_COLOR, - steps: 50, - wrap: false, - }).addTo(map); - - geodesicLayer.geoJson(opts.actual_route_line); - - try { - map.fitBounds(geodesicLayer.getBounds()); - } catch (e) { - console.log(e); - } - } - - if (opts.actual_route_points !== null && opts.actual_route_points.features.length > 0) { - let route_points = L.geoJSON(opts.actual_route_points, { - onEachFeature: onFeaturePointClick, - pointToLayer: pointToLayer, - style: { - "color": ACTUAL_ROUTE_COLOR, - "weight": 5, - "opacity": 0.65, - }, - }); - - route_points.addTo(map); - } - }; - - /** - * Render a map with the airspace, etc around a given set of coords - * e.g, the airport map - * @param opts - */ - const _render_airspace_map = (opts) => { - opts = _.defaults(opts, { - render_elem: 'map', - overlay_elem: '', - lat: 0, - lon: 0, - zoom: 12, - layers: [], - set_marker: false, - }); - - let map = draw_base_map(opts); - const coords = [opts.lat, opts.lon]; - console.log('Applying coords', coords); - - map.setView(coords, opts.zoom); - if (opts.set_marker === true) { - L.marker(coords).addTo(map); - } - - return map; - }; - - /** - * Render the live map - * @param opts - * @private - */ - const _render_live_map = (opts) => { - - opts = _.defaults(opts, { - update_uri: '/api/acars', - pirep_uri: '/api/pireps/{id}/acars', - positions: null, - render_elem: 'map', - aircraft_icon: '/assets/img/acars/aircraft.png', - }); - - const map = draw_base_map(opts); - const aircraftIcon = L.icon({ - iconUrl: opts.aircraft_icon, - iconSize: [42, 42], - iconAnchor: [21, 21], - }); - - let layerFlights = null; - let layerSelFlight = null; - let layerSelFlightFeature = null; - let layerSelFlightLayer = null; - - /** - * When a flight is clicked on, show the path, etc for that flight - * @param feature - * @param layer - */ - const onFlightClick = (feature, layer) => { - - const uri = opts.pirep_uri.replace('{id}', feature.properties.pirep_id); - - const flight_route = $.ajax({ - url: uri, - dataType: "json", - error: console.log - }); - - $.when(flight_route).done((routeJson) => { - if(layerSelFlight !== null) { - map.removeLayer(layerSelFlight); - } - - layerSelFlight = L.geodesic([], { - weight: 7, - opacity: 0.9, - color: ACTUAL_ROUTE_COLOR, - wrap: false, - }).addTo(map); - - layerSelFlight.geoJson(routeJson.line); - - layerSelFlightFeature = feature; - layerSelFlightLayer = layer; - //map.fitBounds(layerSelFlight.getBounds()); - }); - }; - - const updateMap = () => { - - console.log('reloading flights from acars...'); - - /** - * AJAX UPDATE - */ - - let flights = $.ajax({ - url: opts.update_uri, - dataType: "json", - error: console.log - }); - - $.when(flights).done(function (flightGeoJson) { - - if (layerFlights !== null) { - layerFlights.clearLayers(); - } - - layerFlights = L.geoJSON(flightGeoJson, { - onEachFeature: (feature, layer) => { - - layer.on({ - click: (e) => { - onFlightClick(feature, layer); - } - }); - - let popup_html = ""; - if (feature.properties && feature.properties.popup) { - popup_html += feature.properties.popup; - } - - layer.bindPopup(popup_html); - }, - pointToLayer: function(feature, latlon) { - return L.marker(latlon, { - icon: aircraftIcon, - rotationAngle: feature.properties.heading - }); - } - }); - - layerFlights.addTo(map); - - if (layerSelFlight !== null) { - onFlightClick(layerSelFlightFeature, layerSelFlightLayer); - } - }); - }; - - updateMap(); - setInterval(updateMap, 10000); - }; - - return { - render_airspace_map: _render_airspace_map, - render_live_map: _render_live_map, - render_route_map: _render_route_map, - } -})(); diff --git a/tests/FlightTest.php b/tests/FlightTest.php index 021189e2..2d1c31ad 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -155,8 +155,8 @@ class FlightTest extends TestCase $this->assertTrue($flight->has_bid); # Check the table and make sure thee entry is there - $user_bid = UserBid::where(['flight_id'=>$flight->id, 'user_id'=>$user->id])->get(); - $this->assertNotNull($user_bid); + $this->expectException(\App\Exceptions\BidExists::class); + $this->flightSvc->addBid($flight, $user); $user->refresh(); $this->assertEquals(1, $user->bids->count()); @@ -216,10 +216,39 @@ class FlightTest extends TestCase $flight = $this->addFlight($user1); # Put bid on the flight to block it off - $bid = $this->flightSvc->addBid($flight, $user1); + $this->flightSvc->addBid($flight, $user1); - $bidRepeat = $this->flightSvc->addBid($flight, $user2); - $this->assertNull($bidRepeat); + # Try adding again, should throw an exception + $this->expectException(\App\Exceptions\BidExists::class); + $this->flightSvc->addBid($flight, $user2); + } + + /** + * Add a flight bid VIA the API + */ + public function testAddBidApi() + { + $this->user = factory(User::class)->create(); + $user2 = factory(User::class)->create(); + $flight = $this->addFlight($this->user); + + $uri = '/api/user/bids'; + $data = ['flight_id' => $flight->id]; + + $body = $this->put($uri, $data)->json('data'); + + $this->assertCount(1, $body); + $this->assertEquals($body[0]['id'], $flight->id); + + # Now try to have the second user bid on it + # Should return a 409 error + $response = $this->put($uri, $data, [], $user2); + $response->assertStatus(409); + + # Try now deleting the bid from the user + $response = $this->delete($uri, $data); + $body = $response->json('data'); + $this->assertCount(0, $body); } /** diff --git a/tests/TestCase.php b/tests/TestCase.php index 1bf55beb..d583d231 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -127,6 +127,24 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase return $req; } + /** + * Override the PUT calls to inject the user API key + * @param string $uri + * @param array $data + * @param array $headers + * @param null $user + * @return \Illuminate\Foundation\Testing\TestResponse + */ + public function put($uri, array $data = [], array $headers = [], $user = null) + { + $req = parent::put($uri, $data, $this->headers($user, $headers)); + if ($req->isClientError() || $req->isServerError()) { + Log::error('PUT Error: ' . $uri, $req->json()); + } + + return $req; + } + /** * Override the DELETE calls to inject the user API key * @param string $uri