diff --git a/src/tjts5901/app.py b/src/tjts5901/app.py index e75ff7dca31f826ec1477968efe3c5e4ee0e0b89..09bcb2ff81bc758e63207836faac4f5948622642 100644 --- a/src/tjts5901/app.py +++ b/src/tjts5901/app.py @@ -62,6 +62,7 @@ def create_app(config: Optional[Dict] = None) -> Flask: # Register blueprints from . import views flask_app.register_blueprint(views.bp, url_prefix='') + flask_app.register_blueprint(views.api) # a simple page that says hello @flask_app.route('/hello') diff --git a/src/tjts5901/views.py b/src/tjts5901/views.py index cd7002eaab2ece09901b2ae0da5d755e9e506008..43a1f6379cc653bcd493795948fecd84296d23a2 100644 --- a/src/tjts5901/views.py +++ b/src/tjts5901/views.py @@ -5,17 +5,66 @@ Basic views for Application import base64 import random +import logging +from typing import Optional from datetime import datetime, timedelta -from flask import Blueprint, flash, redirect, render_template, request, url_for -from .models import Item, User +from flask import Blueprint, flash, redirect, render_template, request, url_for, jsonify +from .models import Item, User, Bid from .auth import login_required, current_user # Main blueprint. bp = Blueprint('views', __name__) +api = Blueprint('api', __name__, url_prefix='/api') + +logger = logging.getLogger(__name__) # Blueprint for documentation. docs_bp = Blueprint('docs', __name__) +MIN_BID_INCREMENT = 1 + + +def get_winning_bid(item: Item) -> Optional[Bid]: + """ + Return the (currently) winning bid for the given item. + + If there are no bids, or the item is not yet closed, return None. + + :param item: The item to get the winning bid for. + :return: The winning bid, or None. + """ + + winning_bid = None + try: + winning_bid = Bid.objects(item=item) \ + .filter(created_at__lt=item.closes_at) \ + .order_by('-amount') \ + .first() + except Exception as exc: + logger.warning("Error getting winning bid: %s", exc, exc_info=True, extra={ + 'item_id': item.id, + }) + + return winning_bid + + +def get_item_price(item: Item) -> int: + """ + Return the current price of the given item. + + If there are no bids, return the starting bid. + + :param item: The item to get the price for. + :return: The current price. + """ + + winning_bid = get_winning_bid(item) + if winning_bid: + return winning_bid.amount + MIN_BID_INCREMENT + else: + return item.starting_bid + + @bp.route("/") def index() -> str: """ @@ -137,7 +186,7 @@ def bid(id): amount = request.form['amount'] if amount < min_amount: - flash(_("Bid must be at least %(min_amount)s", min_amount)) + flash(("Bid must be at least %(min_amount)s", min_amount)) return redirect(url_for('views.list_bid')) if item.closes_at < datetime.utcnow(): @@ -163,3 +212,96 @@ def bid(id): return "ok" +@api.route('<id>/bids', methods=('GET',)) +@login_required +def api_item_bids(id): + """ + Get the bids for an item. + + :param id: The id of the item to get bids for. + :return: A JSON response containing the bids. + """ + + item = Item.objects.get_or_404(id=id) + bids = [] + for bid in Bid.objects(item=item).order_by('-amount'): + bids.append(bid.to_json()) + + return jsonify({ + 'success': True, + 'bids': bids + }) + + +@api.route('<id>/bids', methods=('POST',)) +@login_required +def api_item_place_bid(id): + """ + Place a bid on an item. + + If the bid is valid, create a new bid and return the bid. + Otherwise, return an error message. + + Only accepts `REF_CURRENCY` bids. + + :param id: The id of the item to bid on. + :return: A JSON response containing the bid. + """ + + item = Item.objects.get_or_404(id=id) + min_amount = get_item_price(item) + + try: + amount = int(request.form['amount']) + except KeyError: + return jsonify({ + 'success': False, + 'error': ("Missing required argument %(argname)s", argname:='amount') + }) + except ValueError: + return jsonify({ + 'success': False, + 'error': ("Invalid value for argument %(argname)s", argname:='amount') + }) + except Exception as exc: + return jsonify({ + 'success': False, + 'error': ("Error parsing argument %(argname)s: %(exc)s", argname:='amount', exc:=exc) + }) + + if amount < min_amount: + return jsonify({ + 'success': False, + 'error': ("Bid must be at least %(min_amount)s", min_amount:=min_amount) + }) + + if item.closes_at < datetime.utcnow(): + return jsonify({ + 'success': False, + 'error': ("This item is no longer on sale.") + }) + + try: + bid = Bid( + item=item, + bidder=current_user, + amount=amount, + ) + bid.save() + except Exception as exc: + logger.error("Error placing bid: %s", exc, exc_info=True, extra={ + 'item_id': item.id, + 'bidder_id': current_user.id, + 'amount': amount, + }) + + return jsonify({ + 'success': False, + 'error': ("Error placing bid: %(exc)s", exc:=exc) + }) + + print(type(bid)) + return jsonify({ + 'success': True, + 'bid': bid.to_json() + })