class RentalsView(BaseView): """ Gets a list of all rentals. """ url = "/rentals" name = "rentals" with_user = match_getter(get_user, 'user', firebase_id=GetFrom.AUTH_HEADER) @with_user @docs(summary="Get All Rentals") @requires(UserIsAdmin()) @returns( JSendSchema.of(rentals=Many( RentalSchema(only=("id", "user_id", "user_url", "bike_identifier", "bike_url", "start_time", "is_active"))))) async def get(self, user): return { "status": JSendStatus.SUCCESS, "data": { "rentals": [ await rental.serialize(self.rental_manager, self.bike_connection_manager, self.reservation_manager, self.request.app.router) for rental in await get_rentals() ] } }
class UserRentalsView(BaseView): """ Gets or adds to the users list of rentals. """ url = f"/users/{{id:{USER_IDENTIFIER_REGEX}}}/rentals" name = "user_rentals" with_user = match_getter(get_user, 'user', user_id='id') with_rentals = match_getter(get_rentals, 'rentals', user='******') @with_user @with_rentals @docs(summary="Get All Rentals For User") @requires(UserMatchesToken() | UserIsAdmin()) @returns(JSendSchema.of(rentals=Many(RentalSchema()))) async def get(self, user, rentals: List[Rental]): return { "status": JSendStatus.SUCCESS, "data": { "rentals": [ await rental.serialize(self.rental_manager, self.request.app.router) for rental in rentals ] } }
class LowBikesView(BaseView): url = "/bikes/low" @docs(summary="Get All Low Battery Bikes") @returns( JSendSchema.of(bikes=Many( BikeSchema(only=("identifier", "battery", "current_location"))))) async def get(self): """ There may come a point where a bike hasn't been able to generate enough power to sustain its battery level. The that point (30% battery or lower), the bike will be listed here. If you claim and recharge one of these bikes, you will receive discounts on your next trip proportional to the amount charged. These stack up. If, for example, you charged 5 bikes for 92, 45, 37, 78, and 83 percent each then your next 5 rides will be 92% off, then 45% off, etc. """ low_battery_bikes = await self.bike_connection_manager.low_battery(30) serialized_bikes = [ bike.serialize(self.bike_connection_manager, self.rental_manager, self.reservation_manager) for bike in low_battery_bikes ] serialized_bikes = [ bike for bike in serialized_bikes if bike["available"] ] return { "status": JSendStatus.SUCCESS, "data": { "bikes": serialized_bikes } }
class UserCurrentReservationView(BaseView): """ Gets the users' current reservation. """ url = f"/users/{{id:{USER_IDENTIFIER_REGEX}}}/reservations/current" with_reservation = match_getter(current_reservations, "reservations", user="******") with_user = match_getter(get_user, "user", user_id="id") @with_user @with_reservation @docs(summary="Get Current Reservations For User") @requires(UserMatchesToken() | UserIsAdmin()) @returns(JSendSchema.of(reservations=Many(CurrentReservationSchema()))) async def get(self, user, reservations: List[Reservation]): return { "status": JSendStatus.SUCCESS, "data": { "reservations": [ reservation.serialize(self.request.app.router, self.reservation_manager) for reservation in reservations ] } }
async def test_get_pickups(self, client, random_pickup_point): resp = await client.get('/api/v1/pickups') schema = JSendSchema.of(pickups=Many(PickupPointSchema())) data = schema.load(await resp.json()) assert data["status"] == JSendStatus.SUCCESS
class BrokenBikesView(BaseView): """ Gets the list of bikes with active issues, along with the open issues for those bikes. """ url = "/bikes/broken" with_bikes = match_getter(get_broken_bikes, "broken_bikes") with_admin = match_getter(get_user, "user", firebase_id=GetFrom.AUTH_HEADER) @with_admin @with_bikes @docs(summary="Get All Broken Bikes") @requires(UserIsAdmin()) @returns(JSendSchema.of(bikes=Many(BikeSchema()))) async def get(self, user, broken_bikes): """ A broken bike is one that has at least one issue open. Broken bikes must be serviced, and so their status is shown here for use by the operators. These bikes can be loaded into a path-finding algorithm and serviced as needed. """ return { "status": JSendStatus.SUCCESS, "data": { "bikes": [ bike.serialize(self.bike_connection_manager, self.rental_manager, self.reservation_manager, issues=issues) for bike, issues in broken_bikes ] } }
class PickupReservationsView(BaseView): """ Gets or adds to a pickup point's list of reservations. """ url = f"/pickups/{{id:{PICKUP_IDENTIFIER_REGEX}}}/reservations" with_user = match_getter(get_user, "user", firebase_id=GetFrom.AUTH_HEADER) with_pickup = match_getter(get_pickup_point, "pickup", pickup_id="id") @with_user @docs(summary="Get All Reservations For Pickup Point") @requires(UserIsAdmin()) @returns(JSendSchema.of(reservations=Many(ReservationSchema()))) async def get(self, user): reservation_ids = self.reservation_manager.reservations_in( self.request.match_info["id"]) return { "status": JSendStatus.SUCCESS, "data": { "reservations": [ reservation.serialize(self.request.app.router, self.reservation_manager) for reservation in await get_reservations(*reservation_ids) ] } } @with_pickup @with_user @docs(summary="Create Reservation At Pickup Point") @expects(CreateReservationSchema()) @returns(error=JSendSchema(), success=JSendSchema.of(reservation=ReservationSchema())) async def post(self, user, pickup): """ To claim a reservation, simply rent a bike from the same pickup point as you usually would do. This will automatically claim that bike for you, ending your reservation, and starting your rental. """ time = self.request["data"]["reserved_for"] try: reservation = await self.reservation_manager.reserve( user, pickup, time) except ReservationError as e: return "error", { "status": JSendStatus.FAIL, "data": { "message": str(e) } } else: return "success", { "status": JSendStatus.SUCCESS, "data": { "reservation": reservation.serialize(self.request.app.router, self.reservation_manager) } }
async def test_get_issues(self, client, random_admin): schema = JSendSchema.of(issues=Many(IssueSchema())) await open_issue(random_admin, "test issue!") resp = await client.get( '/api/v1/issues', headers={"Authorization": f"Bearer {random_admin.firebase_id}"}) data = schema.load(await resp.json()) assert len(data["data"]["issues"]) == 1
class BikesView(BaseView): """ Gets the bikes, or adds a new bike. """ url = "/bikes" with_user = match_getter(get_user, Optional("user"), firebase_id=Optional(GetFrom.AUTH_HEADER)) @with_user @docs(summary="Get All Bikes") @returns(JSendSchema.of(bikes=Many(BikeSchema(exclude=("public_key", ))))) async def get(self, user): """Gets all the bikes from the system.""" bikes = [ bike.serialize(self.bike_connection_manager, self.rental_manager, self.reservation_manager, include_location=user is not None and user.type is not UserType.USER) for bike in await get_bikes() ] if self.request.query.get("available") == "true": bikes = (bike for bike in bikes if bike["status"] == "available") return {"status": JSendStatus.SUCCESS, "data": {"bikes": bikes}} @docs(summary="Register New Bike") @expects(BikeRegisterSchema()) @returns(bad_key=(JSendSchema(), web.HTTPBadRequest), registered=JSendSchema.of(bike=BikeSchema(only=('identifier', 'available')))) async def post(self): """ Registers a bike with the system.""" try: bike = await register_bike(self.request["data"]["public_key"], self.request["data"]["master_key"]) except BadKeyError as error: return "bad_key", { "status": JSendStatus.FAIL, "data": { "message": "The supplied master key is invalid.", "errors": error.args }, } else: return "registered", { "status": JSendStatus.SUCCESS, "data": { "bike": bike.serialize(self.bike_connection_manager, self.rental_manager, self.reservation_manager) } }
async def test_get_pickup_reservations(self, client, random_pickup_point, reservation_manager, random_admin): await reservation_manager.reserve( random_admin, random_pickup_point, datetime.now(timezone.utc) + timedelta(hours=4)) response = await client.get( f'/api/v1/pickups/{random_pickup_point.id}/reservations', headers={"Authorization": f"Bearer {random_admin.firebase_id}"}) response_data = JSendSchema.of( reservations=Many(ReservationSchema())).load(await response.json()) assert len(response_data["data"]["reservations"]) == 1
async def test_get_bikes(self, client: TestClient, random_bike): """Assert that anyone can get the entire list of bikes.""" resp = await client.get('/api/v1/bikes') schema = JSendSchema.of(bikes=Many(BikeSchema())) data = schema.load(await resp.json()) assert data["status"] == JSendStatus.SUCCESS assert isinstance(data["data"]["bikes"], list) assert len(data["data"]["bikes"]) == 1 assert data["data"]["bikes"][0]["identifier"] == random_bike.identifier
async def test_get_bike_rentals(self, client: TestClient, random_bike, random_admin): """Assert that you can get the rentals for a given bike.""" response = await client.get( f'/api/v1/bikes/{random_bike.identifier}/rentals', headers={"Authorization": f"Bearer {random_admin.firebase_id}"}) response_schema = JSendSchema.of(rentals=Many(RentalSchema())) response_data = response_schema.load(await response.json()) assert response_data["status"] == JSendStatus.SUCCESS assert isinstance(response_data["data"]["rentals"], list)
async def test_get_broken_bikes(self, client: TestClient, random_admin, random_bike): """Assert that an admin can get broken bikes from the system.""" await open_issue(random_admin, "This is broken", random_bike) response = await client.get( f"/api/v1/bikes/broken", headers={"Authorization": f"Bearer {random_admin.firebase_id}"}) response_data = JSendSchema.of(bikes=Many(BikeSchema())).load( await response.json()) assert response_data["status"] == JSendStatus.SUCCESS assert len(response_data["data"]["bikes"]) == 1
async def test_get_issues_for_bike(self, client, random_admin, random_bike): await Issue.create(user=random_admin, bike=random_bike, description="OMG AWFUL") response = await client.get( f"/api/v1/bikes/{random_bike.identifier}/issues", headers={"Authorization": f"Bearer {random_admin.firebase_id}"}) response_data = JSendSchema.of(issues=Many(IssueSchema())).load( await response.json()) assert response_data["status"] == JSendStatus.SUCCESS assert "issues" in response_data["data"] assert all(issue["bike_identifier"] == random_bike.identifier for issue in response_data["data"]["issues"])
class UserIssuesView(BaseView): """ Gets or adds to the users' list of issues. """ url = f"/users/{{id:{USER_IDENTIFIER_REGEX}}}/issues" name = "user_issues" with_issues = match_getter(get_issues, "issues", user='******') with_user = match_getter(get_user, 'user', user_id='id') @with_user @with_issues @docs(summary="Get All Issues For User") @requires(UserMatchesToken() | UserIsAdmin()) @returns(JSendSchema.of(issues=Many(IssueSchema()))) async def get(self, user, issues): return { "status": JSendStatus.SUCCESS, "data": { "issues": [issue.serialize(self.request.app.router) for issue in issues] } } @with_user @docs(summary="Open Issue For User") @requires(UserMatchesToken() | UserIsAdmin()) @expects(IssueSchema(only=('description', 'bike_identifier'))) @returns( JSendSchema.of(issue=IssueSchema(only=('id', 'user_id', 'user_url', 'bike_identifier', 'description', 'opened_at')))) async def post(self, user): issue_data = { "description": self.request["data"]["description"], "user": user } if "bike_identifier" in self.request["data"]: issue_data["bike"] = await get_bike( identifier=self.request["data"]["bike_identifier"]) issue = await open_issue(**issue_data) return { "status": JSendStatus.SUCCESS, "data": { "issue": issue.serialize(self.request.app.router) } }
async def test_get_rentals(self, client: TestClient, random_admin, random_bike): """Assert that you can get a list of all rentals.""" await client.app["rental_manager"].create(random_admin, random_bike) response = await client.get( '/api/v1/rentals', headers={"Authorization": f"Bearer {random_admin.firebase_id}"}) response_schema = JSendSchema.of(rentals=Many(RentalSchema())) response_data = response_schema.load(await response.json()) assert response_data["status"] == JSendStatus.SUCCESS assert len(response_data["data"]["rentals"]) == 1 rental = response_data["data"]["rentals"][0] assert rental["bike_identifier"] == random_bike.identifier assert (await client.get(rental["bike_url"])).status != 404
class IssuesView(BaseView): """ Gets the list of issues or adds a new issue. """ url = "/issues" with_issues = match_getter(partial(get_issues, is_active=True), "issues") with_admin = match_getter(get_user, "user", firebase_id=GetFrom.AUTH_HEADER) @with_admin @with_issues @docs(summary="Get All Issues") @requires(UserIsAdmin()) @returns(JSendSchema.of(issues=Many(IssueSchema(exclude=('user', 'bike'))))) async def get(self, user, issues: List[Issue]): return { "status": JSendStatus.SUCCESS, "data": {"issues": [issue.serialize(self.request.app.router) for issue in issues]} }
class BikeIssuesView(BaseView): url = f"/bikes/{{identifier:{BIKE_IDENTIFIER_REGEX}}}/issues" with_issues = match_getter(partial(get_issues, is_active=True), 'issues', bike=('identifier', str)) with_bike = match_getter(get_bike, 'bike', identifier=('identifier', str)) with_user = match_getter(get_user, "user", firebase_id=GetFrom.AUTH_HEADER) @with_issues @with_user @docs(summary="Get All Open Issues On Bike") @requires(UserIsAdmin()) @returns(JSendSchema.of(issues=Many(IssueSchema()))) async def get(self, issues: List[Issue], user): return { "status": JSendStatus.SUCCESS, "data": { "issues": [issue.serialize(self.request.app.router) for issue in issues] } } @with_user @with_bike @docs(summary="Open A New Issue About Bike") @expects(IssueSchema(only=('description', ))) @returns( JSendSchema.of(issue=IssueSchema(only=('id', 'user_id', 'user_url', 'bike_identifier', 'description', 'opened_at')))) async def post(self, user, bike): issue = await open_issue( description=self.request["data"]["description"], user=user, bike=bike) return { "status": JSendStatus.SUCCESS, "data": { "issue": issue.serialize(self.request.app.router) } }
class ReservationsView(BaseView): """ Gets the list of reservations. """ url = "/reservations" with_user = match_getter(get_user, 'user', firebase_id=GetFrom.AUTH_HEADER) @with_user @docs(summary="Get All Reservations") @requires(UserIsAdmin()) @returns(JSendSchema.of(reservations=Many(ReservationSchema()))) async def get(self, user): return { "status": JSendStatus.SUCCESS, "data": {"reservations": [ reservation.serialize(self.request.app.router, self.reservation_manager) for reservation in await get_reservations() ]} }
async def test_get_bikes_in_pickup(self, client, random_pickup_point: PickupPoint, bike_connection_manager): bike1 = await Bike.create(public_key_hex="abcdef") bike2 = await Bike.create(public_key_hex="badcfe") await bike_connection_manager.update_location(bike1, location=Point(100, 100)) await bike_connection_manager.update_location( bike1, location=random_pickup_point.area.centroid) await bike_connection_manager.update_location(bike2, location=Point(100, 100)) response = await client.get( f'/api/v1/pickups/{random_pickup_point.id}/bikes') schema = JSendSchema.of(bikes=Many(BikeSchema())) data = schema.load(await response.json()) assert data["status"] == JSendStatus.SUCCESS assert len(data["data"]["bikes"]) == 1 assert data["data"]["bikes"][0]["public_key"] == b'\xab\xcd\xef'
class PickupShortagesView(BaseView): """ Gets all pickup points with shortages. """ url = "/pickups/shortages" with_user = match_getter(get_user, "user", firebase_id=GetFrom.AUTH_HEADER) @with_user @requires(UserIsAdmin()) @returns(JSendSchema.of(pickups=Many(PickupPointSchema()))) async def get(self, user: User): shortages = self.reservation_sourcer.shortages() return { "status": JSendStatus.SUCCESS, "data": { "pickups": [ pickup.serialize(self.reservation_manager, count, date) for pickup, (count, date) in shortages.items() ] } }
class PickupBikesView(BaseView): """ Gets list of bikes currently at a pickup point. """ url = f"/pickups/{{id:{PICKUP_IDENTIFIER_REGEX}}}/bikes" with_pickup = match_getter(get_pickup_point, 'pickup', pickup_id='id') @with_pickup @docs(summary="Get All Bikes In Pickup Point") @returns(JSendSchema.of(bikes=Many(BikeSchema()))) async def get(self, pickup: PickupPoint): return { "status": JSendStatus.SUCCESS, "data": { "bikes": [ bike.serialize(self.bike_connection_manager, self.rental_manager, self.reservation_manager) for bike in await self.reservation_manager.available_bikes(pickup) ] } }
class UsersView(BaseView): """ Gets or adds to the list of users. """ url = "/users" name = "users" with_user = match_getter(get_user, 'user', firebase_id=GetFrom.AUTH_HEADER) @with_user @docs(summary="Get All Users") @requires(UserIsAdmin()) @expects(None) @returns(JSendSchema.of(users=Many(UserSchema()))) async def get(self, user): return { "status": JSendStatus.SUCCESS, "data": { "users": await get_users() } } @docs(summary="Create A User") @requires(ValidToken()) @expects(UserSchema(only=('first', 'email'))) @returns(JSendSchema.of(user=UserSchema())) async def post(self): """ Anyone who has already authenticated with firebase can then create a user in the system. This must be done before you use the rest of the system, but only has to be done once. """ try: user = await create_user(**self.request["data"], firebase_id=self.request["token"]) except UserExistsError: user = await get_user(firebase_id=self.request["token"]) user = await update_user(user, **self.request["data"]) return {"status": JSendStatus.SUCCESS, "data": {"user": user}}
class PickupsView(BaseView): """ Gets or adds to the list of all pick-up points. """ url = "/pickups" name = "pickups" with_optional_user = match_getter(get_user, Optional("user"), firebase_id=Optional( GetFrom.AUTH_HEADER)) @docs(summary="Get All Pickup Points") @with_optional_user @returns(JSendSchema.of(pickups=Many(PickupPointSchema()))) async def get(self, user: User): pickups = { pickup: (None, None) for pickup in await get_pickup_points() } if user is not None and user.is_admin: pickups.update(self.reservation_sourcer.shortages()) return { "status": JSendStatus.SUCCESS, "data": { "pickups": [ pickup.serialize(self.reservation_manager, count, date) for pickup, (count, date) in pickups.items() ] } } @docs(summary="Create A Pickup Point") async def post(self): raise NotImplementedError()
class BikeRentalsView(BaseView): """ Gets the rentals for a single bike. """ url = f"/bikes/{{identifier:{BIKE_IDENTIFIER_REGEX}}}/rentals" with_bike = match_getter(get_bike, 'bike', identifier=('identifier', str)) with_user = match_getter(get_user, 'user', firebase_id=GetFrom.AUTH_HEADER) @with_bike @with_user @docs(summary="Get Past Rentals For Bike") @requires(UserIsAdmin()) @returns(JSendSchema.of(rentals=Many(RentalSchema()))) async def get(self, bike: Bike, user): return { "status": JSendStatus.SUCCESS, "data": { "rentals": [ await rental.serialize(self.rental_manager, self.bike_connection_manager, self.reservation_manager, self.request.app.router) for rental in await get_rentals_for_bike(bike=bike) ] } } @with_bike @with_user @docs(summary="Start A New Rental") @requires(UserMatchesToken() & UserCanPay() & BikeNotInUse() & BikeNotBroken(max_issues=1)) @returns(rental_created=JSendSchema.of(rental=CurrentRentalSchema( exclude=('user', 'bike', 'events'))), active_rental=JSendSchema(), reservation_error=JSendSchema()) async def post(self, bike: Bike, user: User): """ It is most logical that a rental is created on a bike resource. This metaphor matches real life the best, as it resembles picking bike off the rack. A user may start a rental on any bike that is not currently in use, by simply sending a POST request to the bike's rentals resource ``/api/v1/bikes/rentals`` with the user's firebase token. """ reservations = await current_reservations(user) if reservations: try: rental, start_location = await self.reservation_manager.claim( user, bike) except CollectionError: # they can try and rent it normally reservations = [] except ReservationError as e: return "reservation_error", { "status": JSendStatus.FAIL, "data": { "message": str(e) } } except ActiveRentalError as e: return "active_rental", { "status": JSendStatus.FAIL, "data": { "message": "You already have an active rental!", "rental_id": e.rental_id, "url": str(self.request.app.router["me"].url_for( tail="/rentals/current")) } } if not reservations: try: rental, start_location = await self.rental_manager.create( user, bike) except ActiveRentalError as e: return "active_rental", { "status": JSendStatus.FAIL, "data": { "message": "You already have an active rental!", "rental_id": e.rental_id, "url": str(self.request.app.router["me"].url_for( tail="/rentals/current")) } } return "rental_created", { "status": JSendStatus.SUCCESS, "data": { "rental": await rental.serialize(self.rental_manager, self.bike_connection_manager, self.reservation_manager, self.request.app.router, start_location=start_location, current_location=start_location) } }