Add api routes to get/add/remove bids for a user #172

This commit is contained in:
Nabeel Shahzad 2018-02-22 10:44:15 -06:00
parent e176772512
commit 6dfab75f08
8 changed files with 120 additions and 362 deletions

View File

@ -0,0 +1,20 @@
<?php
/**
*
*/
namespace App\Exceptions;
use Symfony\Component\HttpKernel\Exception\HttpException;
class BidExists extends HttpException
{
public function __construct(string $message = null, \Exception $previous = null, int $code = 0, array $headers = [])
{
parent::__construct(
409,
'A bid already exists for this flight',
$previous, $headers, $code
);
}
}

View File

@ -9,9 +9,11 @@ use App\Http\Resources\User as UserResource;
use App\Models\Enums\PirepState; use App\Models\Enums\PirepState;
use App\Models\UserBid; use App\Models\UserBid;
use App\Repositories\Criteria\WhereCriteria; use App\Repositories\Criteria\WhereCriteria;
use App\Repositories\FlightRepository;
use App\Repositories\PirepRepository; use App\Repositories\PirepRepository;
use App\Repositories\SubfleetRepository; use App\Repositories\SubfleetRepository;
use App\Repositories\UserRepository; use App\Repositories\UserRepository;
use App\Services\FlightService;
use App\Services\UserService; use App\Services\UserService;
use Auth; use Auth;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -21,24 +23,32 @@ use Prettus\Repository\Exceptions\RepositoryException;
class UserController extends RestController class UserController extends RestController
{ {
protected $pirepRepo, protected $flightRepo,
$flightSvc,
$pirepRepo,
$subfleetRepo, $subfleetRepo,
$userRepo, $userRepo,
$userSvc; $userSvc;
/** /**
* UserController constructor. * UserController constructor.
* @param FlightRepository $flightRepo
* @param FlightService $flightSvc
* @param PirepRepository $pirepRepo * @param PirepRepository $pirepRepo
* @param SubfleetRepository $subfleetRepo * @param SubfleetRepository $subfleetRepo
* @param UserRepository $userRepo * @param UserRepository $userRepo
* @param UserService $userSvc * @param UserService $userSvc
*/ */
public function __construct( public function __construct(
FlightRepository $flightRepo,
FlightService $flightSvc,
PirepRepository $pirepRepo, PirepRepository $pirepRepo,
SubfleetRepository $subfleetRepo, SubfleetRepository $subfleetRepo,
UserRepository $userRepo, UserRepository $userRepo,
UserService $userSvc UserService $userSvc
) { ) {
$this->flightRepo = $flightRepo;
$this->flightSvc = $flightSvc;
$this->pirepRepo = $pirepRepo; $this->pirepRepo = $pirepRepo;
$this->subfleetRepo = $subfleetRepo; $this->subfleetRepo = $subfleetRepo;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
@ -80,13 +90,31 @@ class UserController extends RestController
/** /**
* Return all of the bids for the passed-in user * Return all of the bids for the passed-in user
* @param $id * @param Request $request
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection * @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() $user = $this->userRepo->find($this->getUserId($request));
->pluck('flight');
# 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); return FlightResource::collection($flights);
} }

View File

@ -8,6 +8,7 @@ use App\Repositories\AirlineRepository;
use App\Repositories\AirportRepository; use App\Repositories\AirportRepository;
use App\Repositories\Criteria\WhereCriteria; use App\Repositories\Criteria\WhereCriteria;
use App\Repositories\FlightRepository; use App\Repositories\FlightRepository;
use App\Services\FlightService;
use App\Services\GeoService; use App\Services\GeoService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -26,17 +27,20 @@ class FlightController extends Controller
* @param AirlineRepository $airlineRepo * @param AirlineRepository $airlineRepo
* @param AirportRepository $airportRepo * @param AirportRepository $airportRepo
* @param FlightRepository $flightRepo * @param FlightRepository $flightRepo
* @param FlightService $flightSvc
* @param GeoService $geoSvc * @param GeoService $geoSvc
*/ */
public function __construct( public function __construct(
AirlineRepository $airlineRepo, AirlineRepository $airlineRepo,
AirportRepository $airportRepo, AirportRepository $airportRepo,
FlightRepository $flightRepo, FlightRepository $flightRepo,
FlightService $flightSvc,
GeoService $geoSvc GeoService $geoSvc
) { ) {
$this->airlineRepo = $airlineRepo; $this->airlineRepo = $airlineRepo;
$this->airportRepo = $airportRepo; $this->airportRepo = $airportRepo;
$this->flightRepo = $flightRepo; $this->flightRepo = $flightRepo;
$this->flightSvc = $flightSvc;
$this->geoSvc = $geoSvc; $this->geoSvc = $geoSvc;
} }

View File

@ -65,9 +65,15 @@ Route::group(['middleware' => ['api.auth']], function ()
Route::get('user/fleet', 'UserController@fleet'); Route::get('user/fleet', 'UserController@fleet');
Route::get('user/pireps', 'UserController@pireps'); 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}', 'UserController@get');
Route::get('users/{id}/bids', 'UserController@bids');
Route::get('users/{id}/fleet', 'UserController@fleet'); Route::get('users/{id}/fleet', 'UserController@fleet');
Route::get('users/{id}/pireps', 'UserController@pireps'); Route::get('users/{id}/pireps', 'UserController@pireps');
Route::get('users/{id}/bids', 'UserController@bids');
Route::put('users/{id}/bids', 'UserController@bids');
}); });

View File

@ -2,6 +2,7 @@
namespace App\Services; namespace App\Services;
use App\Exceptions\BidExists;
use App\Models\Flight; use App\Models\Flight;
use App\Models\User; use App\Models\User;
use App\Models\UserBid; use App\Models\UserBid;
@ -129,13 +130,14 @@ class FlightService extends BaseService
* @param Flight $flight * @param Flight $flight
* @param User $user * @param User $user
* @return UserBid|null * @return UserBid|null
* @throws \App\Exceptions\BidExists
*/ */
public function addBid(Flight $flight, User $user) public function addBid(Flight $flight, User $user)
{ {
# If it's already been bid on, then it can't be bid on again # 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')) { if($flight->has_bid && setting('bids.disable_flight_on_bid')) {
Log::info($flight->id . ' already has a bid, skipping'); 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 # 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 * Remove a bid from a given flight
* @param Flight $flight * @param Flight $flight
* @param User $user * @param User $user
* @throws Exception
*/ */
public function removeBid(Flight $flight, User $user) public function removeBid(Flight $flight, User $user)
{ {

View File

@ -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("<a href=\"https://www.thunderforest.com\" target=\"_blank\" style=\"\">Thunderforest</a>");
attrib.addAttribution("<a href=\"https://www.openaip.net\" target=\"_blank\" style=\"\">openAIP</a>");
attrib.addAttribution("<a href=\"https://www.openstreetmap.org/copyright\" target=\"_blank\" style=\"\">OpenStreetMap</a> contributors");
attrib.addAttribution("<a href=\"https://www.openweathermap.org\" target=\"_blank\" style=\"\">OpenWeatherMap</a>");
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,
}
})();

View File

@ -155,8 +155,8 @@ class FlightTest extends TestCase
$this->assertTrue($flight->has_bid); $this->assertTrue($flight->has_bid);
# Check the table and make sure thee entry is there # Check the table and make sure thee entry is there
$user_bid = UserBid::where(['flight_id'=>$flight->id, 'user_id'=>$user->id])->get(); $this->expectException(\App\Exceptions\BidExists::class);
$this->assertNotNull($user_bid); $this->flightSvc->addBid($flight, $user);
$user->refresh(); $user->refresh();
$this->assertEquals(1, $user->bids->count()); $this->assertEquals(1, $user->bids->count());
@ -216,10 +216,39 @@ class FlightTest extends TestCase
$flight = $this->addFlight($user1); $flight = $this->addFlight($user1);
# Put bid on the flight to block it off # 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); # Try adding again, should throw an exception
$this->assertNull($bidRepeat); $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);
} }
/** /**

View File

@ -127,6 +127,24 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return $req; 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 * Override the DELETE calls to inject the user API key
* @param string $uri * @param string $uri