Skip to content

Commit

Permalink
feat: reviews implementation and code description
Browse files Browse the repository at this point in the history
* chore(reviews): init reviews

* chore(reviews): almost completed

* chore: completed reviews and added comments to all the python code

---------

Co-authored-by: Diego Caspi <[email protected]>
  • Loading branch information
diegocaspi and Diego Caspi authored Jun 22, 2024
1 parent 2ae5fb9 commit 8e12938
Show file tree
Hide file tree
Showing 43 changed files with 835 additions and 40 deletions.
3 changes: 3 additions & 0 deletions app/modules/auth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@


class LoginForm(FlaskForm):
"""
Form for logging in.
"""
email = StringField('Email', validators=[validators.DataRequired(), validators.Email(message="Invalid email format")])
password = PasswordField('Password', validators=[validators.DataRequired()])
remember = BooleanField('Remember me', validators=[validators.Optional()], default=False)
26 changes: 26 additions & 0 deletions app/modules/auth/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,32 @@

@login_manager.user_loader
def load_user(user_guid: str):
"""
Load a user by its guid. This function is used by Flask-Login.
:param user_guid: the guid of the user
:return: the retrieved user or None
"""
if not UUID(user_guid):
return None
return User.query.filter_by(guid=UUID(user_guid)).first()


def get_user_by_email(email: str):
"""
Get a user by its email.
:param email: the email of the user
:return: the retrieved user or None
"""
return User.query.filter_by(email=email).first()


def validate_user(email: str, password: str):
"""
Validate a user by its email and password. Return the user if the credentials are correct, None otherwise.
:param email: the email of the user
:param password: the password of the user
:return: the user if the credentials are correct, None otherwise
"""
user = User.query.filter_by(email=email).first()
if user and check_password_hash(user.password, password):
return user
Expand All @@ -28,6 +44,16 @@ def validate_user(email: str, password: str):
def register_user(
email: str, given_name: str, family_name: str, password: str, destination_address: str, card_number: str
):
"""
Register a new user.
:param email: the email of the user
:param given_name: the given name of the user
:param family_name: the family name of the user
:param password: the password of the user
:param destination_address: the destination address of the buyer
:param card_number: the card number of the buyer
:return: the created user
"""
user = User(email=email, given_name=given_name, family_name=family_name, password=generate_password_hash(password))
db.session.add(user)
buyer = Buyer(destination_address=destination_address, card_number=card_number, user=user)
Expand Down
18 changes: 18 additions & 0 deletions app/modules/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@

@auth.route("/login", methods=["GET", "POST"])
def login():
"""
Login view.
:return: The login view.
"""
form = LoginForm()

# handling form submission
if request.method == "POST":
# validate form input, throw error if invalid
if form.validate_on_submit():
subject = validate_user(form.email.data, form.password.data)
if not subject:
Expand All @@ -23,11 +29,17 @@ def login():

return redirect(url_for("home.index_view"))

# render the login view
return render_template("auth/login.html", form=form)


@auth.route("/signup", methods=["GET", "POST"])
def signup():
"""
Signup view.
:return: The signup view.
"""
# handling form submission
if request.method == "POST":
email = request.form.get('email')
given_name = request.form.get('given_name')
Expand All @@ -37,11 +49,13 @@ def signup():
destination_address = request.form.get('destination_address')
card_number = request.form.get('card_number')

# extract existing user by email and check if it exists
existing_user = get_user_by_email(email)
if existing_user:
flash("User already exists")
return render_template("auth/signup.html")

# check if passwords match
if password != password_confirmation:
flash("Passwords do not match")
return render_template("auth/signup.html")
Expand All @@ -57,5 +71,9 @@ def signup():
@auth.route("/logout", methods=["POST"])
@login_required
def logout():
"""
Logout view.
:return: Redirect to the login view.
"""
logout_user()
return redirect(url_for("auth.login"))
8 changes: 8 additions & 0 deletions app/modules/buyers/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@


def update_buyer(user_id: int, destination_address: str, card_number: str):
"""
Update the destination address and card number of a buyer.
Return the updated buyer if successful, None otherwise.
:param user_id: the user id
:param destination_address: the updated destination address of the buyer
:param card_number: the updated card number of the buyer
:return the updated buyer
"""
buyer = Buyer.query.filter_by(user_id=user_id).first()
if not buyer:
return None
Expand Down
10 changes: 10 additions & 0 deletions app/modules/buyers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
if TYPE_CHECKING:
from app.modules.users.models import User
from app.modules.carts.models import Cart
from app.modules.products.models import ProductReview
from app.modules.orders.models import OrderReport
else:
User = "User"
Cart = "Cart"
ProductReview = "ProductReview"
OrderReport = "OrderReport"


class Buyer(db.Model):
"""
The Buyer model. A buyer is a user that can buy products.
"""
__tablename__ = "buyers"

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, nullable=False, index=True)
Expand All @@ -22,6 +29,9 @@ class Buyer(db.Model):
user: Mapped[User] = db.relationship("User", back_populates="buyers")
carts: Mapped[List[Cart]] = db.relationship("Cart", back_populates="buyer")

reviews: Mapped[List[ProductReview]] = db.relationship("ProductReview", back_populates="buyer")
order_reports: Mapped[List[OrderReport]] = db.relationship("OrderReport", back_populates="buyer")

def __repr__(self):
return (f"<Buyer user={self.user.email} destination_address={self.destination_address} "
f"card_number={self.card_number}>")
50 changes: 48 additions & 2 deletions app/modules/carts/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,85 @@

from app.modules.carts.models import CartStatus, Cart, ProductReservation
from app.modules.products.models import Product
from app.modules.shared.consts import page_size
from app.modules.shared.consts import DEFAULT_PAGE_SIZE
from extensions import db


def get_cart_by_buyer(buyer_id: int) -> Cart | None:
"""
Get the cart of a buyer by its id.
:param buyer_id: the id of the buyer
:return: the cart of the buyer or None if it doesn't exist
"""
return Cart.query.filter_by(owner_buyer_id=buyer_id, status=CartStatus.ACTIVE).first()


def get_cart_or_create(buyer_id: int) -> Cart | None:
"""
Get the cart of a buyer by its id. If it doesn't exist, create a new one.
:param buyer_id: the id of the buyer
:return: the cart of the buyer
"""
cart = get_cart_by_buyer(buyer_id)
if not cart:
cart = Cart(owner_buyer_id=buyer_id)
db.session.add(cart)
return cart


def get_reservation_by_cart(cart: Cart, page: int = 1, per_page: int = page_size) -> QueryPagination:
def get_reservation_by_cart(cart: Cart, page: int = 1, per_page: int = DEFAULT_PAGE_SIZE) -> QueryPagination:
"""
Get the reservations of a cart. The reservations are paginated.
:param cart: the cart to get the reservations from
:param page: the page number
:param per_page: the number of reservations per page
:return: the reservations of the cart
"""
return (ProductReservation.query
.filter_by(cart=cart, deleted_at=None)
.order_by(ProductReservation.created_at)
.paginate(page=page, per_page=per_page))


def get_reservation_by_product(buyer_id: int, product: Product) -> (ProductReservation | None, bool):
"""
Get the reservation of a product in a cart. If the product is not in the cart, return None.
:param buyer_id: the id of the buyer
:param product: the product to get the reservation from
:return: the reservation of the product in the cart or None if it doesn't exist, and a boolean indicating if the
reservation has a different sequence than the product
"""
cart = get_cart_by_buyer(buyer_id)
if not cart:
return None, False

# get the reservation of the product in the cart
product_reservation = ProductReservation.query.filter_by(product_id=product.id, cart=cart, deleted_at=None).first()
if not product_reservation:
return None, False

# if the product sequence is different from the reservation sequence, delete the reservation and return None, True
if product.sequence != product_reservation.product_sequence:
product_reservation.deleted_at = db.func.now()
db.session.commit()
return None, True

# return the reservation and False
return product_reservation, False


def update_cart(buyer_id: int, product: Product, quantity: int) -> (Cart | None, Product | None):
"""
Update the cart of a buyer with a product and a quantity.
:param buyer_id: the id of the buyer
:param product: the product to add to the cart
:param quantity: the quantity of the product
:return: the cart of the buyer and the product if the product has a different sequence than the reservation
"""
cart = get_cart_or_create(buyer_id)
product_reservation = ProductReservation.query.filter_by(product_id=product.id, cart=cart, deleted_at=None).first()

# if the product is not in the cart, create a new reservation
if not product_reservation:
product_reservation = ProductReservation(
product_id=product.id,
Expand All @@ -56,26 +92,36 @@ def update_cart(buyer_id: int, product: Product, quantity: int) -> (Cart | None,
db.session.commit()
return cart, None

# if the product sequence is different from the reservation sequence, delete the reservation and return None, product
if product.sequence != product_reservation.product_sequence:
product_reservation.deleted_at = db.func.now()
# TODO should create a new reservation?
db.session.commit()
return None, product

# update the quantity of the reservation
product_reservation.quantity = quantity
db.session.commit()
return cart, None


def remove_from_cart(buyer_id: int, product: Product) -> Cart | None:
"""
Remove a product from the cart of a buyer. If the product is not in the cart, return None.
:param buyer_id: the id of the buyer
:param product: the product to remove from the cart
:return: the cart of the buyer or None if the product is not in the cart
"""
cart = get_cart_by_buyer(buyer_id)
if not cart:
return None

# get the reservation of the product in the cart
product_reservation = ProductReservation.query.filter_by(product_id=product.id, cart=cart, deleted_at=None).first()
if not product_reservation:
return None

# delete the reservation
product_reservation.deleted_at = db.func.now()
db.session.commit()
return cart
12 changes: 12 additions & 0 deletions app/modules/carts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@


class CartStatus(Enum):
"""
The CartStatus enum. It represents the status of a cart.
"""
ACTIVE = "active"
FINALIZED = "finalized"


class ProductReservation(db.Model):
"""
The ProductReservation model. It represents a reservation of a product in a cart.
"""
__tablename__ = "product_reservations"

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, nullable=False, index=True)
Expand Down Expand Up @@ -49,6 +55,9 @@ def __repr__(self):


class ProductReservationHistory(db.Model):
"""
The ProductReservationHistory model. It represents the history of a reservation of a product in a cart.
"""
__tablename__ = "product_reservation_history"

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, nullable=False, index=True)
Expand All @@ -67,6 +76,9 @@ def __repr__(self):


class Cart(db.Model):
"""
The Cart model. It represents a cart of a buyer.
"""
__tablename__ = "carts"

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, nullable=False, index=True)
Expand Down
12 changes: 12 additions & 0 deletions app/modules/carts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@


def validate_product(product_guid: str) -> Product:
"""
Utility function to validate a product by its guid.
:param product_guid: the guid of the product
:return: the product if it exists, otherwise abort with a 404 error
"""
try:
product_guid = UUID(product_guid)
product = get_product_by_guid(product_guid)
Expand All @@ -27,8 +32,13 @@ def validate_product(product_guid: str) -> Product:
@login_required
@buyer_required
def index_view():
"""
Cart view.
:return: The cart view.
"""
buyer_id = current_user.buyers[0].id

# handling form submission
if request.method == 'POST':
product_guid = request.form.get('product_guid')
quantity = int(request.form.get('quantity'))
Expand All @@ -48,6 +58,8 @@ def index_view():
page = request.args.get('page', 1, type=int)

cart = get_cart_by_buyer(buyer_id)

# get the reservations of the cart
reservations = get_reservation_by_cart(cart, page)
return render_template(
'carts/index.html',
Expand Down
Loading

0 comments on commit 8e12938

Please sign in to comment.