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