Add api routes to get/add/remove bids for a user #172
This commit is contained in:
parent
e176772512
commit
6dfab75f08
20
app/Exceptions/BidExists.php
Normal file
20
app/Exceptions/BidExists.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
@ -9,9 +9,11 @@ use App\Http\Resources\User as UserResource;
|
||||
use App\Models\Enums\PirepState;
|
||||
use App\Models\UserBid;
|
||||
use App\Repositories\Criteria\WhereCriteria;
|
||||
use App\Repositories\FlightRepository;
|
||||
use App\Repositories\PirepRepository;
|
||||
use App\Repositories\SubfleetRepository;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\Services\FlightService;
|
||||
use App\Services\UserService;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
@ -21,24 +23,32 @@ use Prettus\Repository\Exceptions\RepositoryException;
|
||||
|
||||
class UserController extends RestController
|
||||
{
|
||||
protected $pirepRepo,
|
||||
protected $flightRepo,
|
||||
$flightSvc,
|
||||
$pirepRepo,
|
||||
$subfleetRepo,
|
||||
$userRepo,
|
||||
$userSvc;
|
||||
|
||||
/**
|
||||
* UserController constructor.
|
||||
* @param FlightRepository $flightRepo
|
||||
* @param FlightService $flightSvc
|
||||
* @param PirepRepository $pirepRepo
|
||||
* @param SubfleetRepository $subfleetRepo
|
||||
* @param UserRepository $userRepo
|
||||
* @param UserService $userSvc
|
||||
*/
|
||||
public function __construct(
|
||||
FlightRepository $flightRepo,
|
||||
FlightService $flightSvc,
|
||||
PirepRepository $pirepRepo,
|
||||
SubfleetRepository $subfleetRepo,
|
||||
UserRepository $userRepo,
|
||||
UserService $userSvc
|
||||
) {
|
||||
$this->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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
|
||||
});
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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,
|
||||
}
|
||||
})();
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user