示例#1
0
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)
            }
        }
示例#2
0
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
                ]
            }
        }
示例#3
0
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
                ]
            }
        }
示例#4
0
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
                ]
            }
        }
示例#5
0
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)
                }
            }
示例#6
0
class ReservationView(BaseView):
    """
    Gets or updates a single reservation.
    """
    url = "/reservations/{id}"
    name = "reservation"
    with_reservation = match_getter(get_reservation, 'reservation', rid="id")
    with_user = match_getter(get_user, 'user', firebase_id=GetFrom.AUTH_HEADER)

    @with_user
    @with_reservation
    @docs(summary="Get A Reservation")
    @requires(UserIsAdmin() | UserOwnsReservation())
    @returns(JSendSchema.of(reservation=ReservationSchema()))
    async def get(self, reservation: Reservation, user):
        return {
            "status": JSendStatus.SUCCESS,
            "data": {"reservation": reservation.serialize(self.request.app.router, self.reservation_manager)}
        }

    @with_user
    @with_reservation
    @docs(summary="Delete A Reservation")
    @requires(UserIsAdmin() | UserOwnsReservation())
    @returns(
        already_ended=(JSendSchema(), web.HTTPBadRequest),
        cancelled=JSendSchema.of(reservation=ReservationSchema()),
    )
    async def delete(self, reservation: Reservation, user):

        if reservation.outcome is not None:
            return "already_ended", {
                "status": JSendStatus.FAIL,
                "data": {"message": "The requested rental cannot be cancelled because it is not currently active."}
            }

        await self.reservation_manager.cancel(reservation)

        return "cancelled", {
            "status": JSendStatus.SUCCESS,
            "data": {"reservation": reservation.serialize(self.request.app.router, self.reservation_manager)}
        }
示例#7
0
class UserView(BaseView):
    """
    Gets, replaces or deletes a single user.
    """
    url = f"/users/{{id:{USER_IDENTIFIER_REGEX}}}"
    name = "user"
    with_user = match_getter(get_user, 'user', user_id='id')

    @with_user
    @docs(summary="Get A User")
    @requires(UserMatchesToken() | UserIsAdmin())
    @expects(None)
    @returns(JSendSchema.of(user=UserSchema()))
    async def get(self, user: User):
        return {
            "status": JSendStatus.SUCCESS,
            "data": {
                "user": user.serialize()
            }
        }

    @with_user
    @docs(summary="Replace A User")
    @requires(UserMatchesToken() | UserIsAdmin())
    @expects(UserSchema(only=('first', 'email')))
    @returns(JSendSchema.of(user=UserSchema()))
    async def put(self, user: User):
        user = await update_user(user, **self.request["data"])
        return {"status": JSendStatus.SUCCESS, "data": {"user": user}}

    @with_user
    @docs(summary="Delete A User")
    @requires(UserMatchesToken() | UserIsAdmin())
    async def delete(self, user: User):
        if user.can_pay:
            await self.payment_manager.delete_customer(user)
        await delete_user(user)
        raise web.HTTPNoContent
示例#8
0
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)
            }
        }
示例#9
0
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()
            ]}
        }
示例#10
0
class UserCurrentRentalView(BaseView):
    """
    Gets or ends the user's current rental.
    """
    url = f"/users/{{id:{USER_IDENTIFIER_REGEX}}}/rentals/current"
    name = "user_current_rental"
    with_user = match_getter(get_user, 'user', user_id='id')

    @with_user
    @docs(summary="Get Current Rental For User")
    @requires(UserMatchesToken() | UserIsAdmin())
    @returns(no_rental=(JSendSchema(), web.HTTPNotFound),
             rental_exists=JSendSchema.of(rental=CurrentRentalSchema(
                 only=('id', 'bike_identifier', 'bike_url', 'user_id',
                       'user_url', 'start_time', 'is_active',
                       'estimated_price', 'start_location',
                       'current_location'))))
    async def get(self, user: User):
        if not self.rental_manager.has_active_rental(user):
            return "no_rental", {
                "status": JSendStatus.FAIL,
                "data": {
                    "message": f"You have no current rental."
                }
            }

        current_rental, start_location, current_location = await self.rental_manager.active_rental(
            user, with_locations=True)
        return "rental_exists", {
            "status": JSendStatus.SUCCESS,
            "data": {
                "rental":
                await
                current_rental.serialize(self.rental_manager,
                                         self.bike_connection_manager,
                                         self.reservation_manager,
                                         self.request.app.router,
                                         start_location=start_location,
                                         current_location=current_location)
            }
        }
示例#11
0
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()
                ]
            }
        }
示例#12
0
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}}
示例#13
0
class PickupView(BaseView):
    """
    Gets, updates or deletes a single pick-up point.
    """
    url = f"/pickups/{{id:{PICKUP_IDENTIFIER_REGEX}}}"
    name = "pickup"
    with_pickup = match_getter(get_pickup_point, 'pickup', pickup_id='id')
    with_user = match_getter(get_user, "user", firebase_id=GetFrom.AUTH_HEADER)

    @with_pickup
    @docs(summary="Get A Pickup Point")
    @returns(JSendSchema.of(pickup=PickupPointSchema()))
    async def get(self, pickup):
        pickup_data = pickup.serialize(self.reservation_manager)

        return {"status": JSendStatus.SUCCESS, "data": {"pickup": pickup_data}}

    @with_pickup
    @with_user
    @docs(summary="Delete A Pickup Point")
    @requires(UserIsAdmin())
    async def delete(self, pickup: PickupPoint, user: User):
        await pickup.delete()
        raise web.HTTPNoContent
示例#14
0
class UserEndCurrentRentalView(BaseView):
    """"""

    url = f"/users/{{id:{USER_IDENTIFIER_REGEX}}}/rentals/current/{{action}}"
    name = "user_end_current_rental"
    with_user = match_getter(get_user, 'user', user_id='id')
    actions = ("cancel", "complete")

    @with_user
    @docs(summary="End Rental For User")
    @requires(UserMatchesToken() | UserIsAdmin())
    @returns(
        no_rental=(JSendSchema(), web.HTTPNotFound),
        invalid_action=(JSendSchema(), web.HTTPNotFound),
        rental_completed=JSendSchema.of(rental=RentalSchema(),
                                        action=String(),
                                        receipt_url=Url(allow_none=True)),
    )
    async def patch(self, user: User):
        """
        Ends a rental for a user, in one of two ways:

        - ``PATCH /users/me/rentals/current/cancel`` cancels the rental
        - ``PATCH /users/me/rentals/current/complete`` completes the rental
        """

        if not self.rental_manager.has_active_rental(user):
            return "no_rental", {
                "status": JSendStatus.FAIL,
                "data": {
                    "message": "You have no current rental."
                }
            }

        end_type = self.request.match_info["action"]
        if end_type not in self.actions:
            return "invalid_action", {
                "status": JSendStatus.FAIL,
                "data": {
                    "message":
                    f"Invalid action. Pick between {', '.join(self.actions)}",
                    "actions": self.actions
                }
            }

        if end_type == "complete":
            rental, receipt_url = await self.rental_manager.finish(user)
        elif end_type == "cancel":
            rental = await self.rental_manager.cancel(user)
            receipt_url = None
        else:
            raise Exception

        return "rental_completed", {
            "status": JSendStatus.SUCCESS,
            "data": {
                "rental":
                await rental.serialize(self.rental_manager,
                                       self.bike_connection_manager,
                                       self.reservation_manager,
                                       self.request.app.router),
                "action":
                "canceled" if end_type == "cancel" else "completed",
                "receipt_url":
                receipt_url,
            }
        }
示例#15
0
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)
            }
        }
示例#16
0
class BikeView(BaseView):
    """
    Gets or updates a single bike.
    """
    url = f"/bikes/{{identifier:{BIKE_IDENTIFIER_REGEX}}}"
    name = "bike"
    with_bike = match_getter(get_bike, 'bike', identifier=('identifier', str))
    with_user = match_getter(get_user,
                             Optional('user'),
                             firebase_id=Optional(GetFrom.AUTH_HEADER))
    with_rental = match_getter(get_rentals_for_bike)

    @with_bike
    @with_user
    @docs(summary="Get A Bike")
    @returns(JSendSchema.of(bike=BikeSchema(exclude=("public_key", ))))
    async def get(self, bike: Bike, user):
        """Gets a single bike by its id."""
        return {
            "status": JSendStatus.SUCCESS,
            "data": {
                "bike":
                bike.serialize(
                    self.bike_connection_manager,
                    self.rental_manager,
                    self.reservation_manager,
                    include_location=user is not None
                    and self.rental_manager.is_renting(user.id, bike.id))
            }
        }

    @with_bike
    @docs(summary="Delete A Bike")
    @expects(MasterKeySchema())
    @returns(bad_master=(JSendSchema(), web.HTTPBadRequest))
    async def delete(self, bike: Bike):
        """Deletes a bike by its id."""
        try:
            await delete_bike(bike, self.request["data"]["master_key"])
        except BadKeyError as error:
            return "bad_master", {
                "status": JSendStatus.FAIL,
                "data": {
                    "message": "The supplied master key is invalid.",
                    "errors": error.args
                }
            }
        else:
            raise web.HTTPNoContent

    @with_user
    @with_bike
    @docs(summary="Update A Bike")
    @requires(UserIsAdmin() | UserIsRentingBike() & BikeIsConnected())
    @expects(BikeModifySchema())
    @returns(JSendSchema.of(bike=BikeSchema(exclude=("public_key", ))))
    async def patch(self, user: User, bike: Bike):
        """
        Allows users to update the status of a bike.

        - locked status
        - circulation status
        """
        if "locked" in self.request["data"]:
            await self.bike_connection_manager.set_locked(
                bike.id, self.request["data"]["locked"])

        if user.type is not UserType.USER and "in_circulation" in self.request[
                "data"]:
            bike = await set_bike_in_circulation(
                bike, self.request["data"]["in_circulation"])

        return {
            "status": JSendStatus.SUCCESS,
            "data": {
                "bike":
                bike.serialize(self.bike_connection_manager,
                               self.rental_manager,
                               self.reservation_manager,
                               include_location=True)
            }
        }