Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spruce - Cabebe & Zandra #39

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c197008
adds Customer and Video models
cabebe-bloop Nov 8, 2021
dfa2b26
modify Customers & Video Models. Add POST method to customers_routes.py
zandra2 Nov 9, 2021
6208865
adds Video endpoints for GET and starts POST
cabebe-bloop Nov 9, 2021
f2b2c05
merges conflicts, adds Video GET endpoints
cabebe-bloop Nov 9, 2021
979373d
edits Video's to_dict method and adds Video POST route
cabebe-bloop Nov 9, 2021
a5df823
modify Customer's Model and add lots of buggie functions. Don't hate …
zandra2 Nov 9, 2021
871a768
edits POST video route and starts DELETE video route
cabebe-bloop Nov 9, 2021
656ab7c
edits POST video route and starts DELETE video route
cabebe-bloop Nov 9, 2021
8488bb1
adds f-string to DELETE customer route response body
cabebe-bloop Nov 9, 2021
df74858
removes f string from DELETE customer route
cabebe-bloop Nov 9, 2021
7a35f94
make change to POST method. Still need more work.
zandra2 Nov 10, 2021
912faad
Merge branch 'master' of https://github.com/zandra2/retro-video-store
zandra2 Nov 10, 2021
8a30508
adds PUT video route
cabebe-bloop Nov 10, 2021
ce7116c
made change to POST and GET methods. PUT is work in progress
zandra2 Nov 10, 2021
487e1a9
Merge branch 'master' of https://github.com/zandra2/retro-video-store
zandra2 Nov 10, 2021
e361dce
adds 201 to POST customer HTTP response
cabebe-bloop Nov 10, 2021
ea9febe
debugs Customer routes. Code passing 20 tests
cabebe-bloop Nov 10, 2021
0131c77
make change to GET methods
zandra2 Nov 10, 2021
46251d2
resolves merge conflicts for Customer routes
cabebe-bloop Nov 10, 2021
7d771e1
accepts both changes to PUT customers route
cabebe-bloop Nov 10, 2021
c9aca98
pair programs with Z to get all wave 1 tests
cabebe-bloop Nov 10, 2021
de5f440
add rental model and pseudocode checkout route
zandra2 Nov 11, 2021
a55248d
add rentals_checkout function. of course more garbage code.
zandra2 Nov 11, 2021
73c188b
messes around with models
cabebe-bloop Nov 11, 2021
f4b07cc
add checkout route. need to pass 1 more test
zandra2 Nov 11, 2021
25a25af
add comments and params in GET functions
zandra2 Nov 12, 2021
b68b11b
adds back_populate and ideates check-out response_body logic
cabebe-bloop Nov 12, 2021
524926b
resolves merge conflicts for rental_routes
cabebe-bloop Nov 12, 2021
b400e27
adds POST check-in and GET rentals by customer routes. 16/20 wave 2 t…
cabebe-bloop Nov 12, 2021
3123fd4
adds GET rentals by video routes. Passes 19/20 wave 2 tests.
cabebe-bloop Nov 12, 2021
0ec6c1b
modify checked_out & check_in endpoints
zandra2 Nov 12, 2021
145f114
clean up check-out and check-in methods
zandra2 Nov 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
migrate = Migrate()
load_dotenv()


def create_app(test_config=None):
app = Flask(__name__)
app.url_map.strict_slashes = False
Expand All @@ -21,7 +22,6 @@ def create_app(test_config=None):
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_TEST_DATABASE_URI")


# import models for Alembic Setup
from app.models.customer import Customer
from app.models.video import Video
Expand All @@ -31,6 +31,12 @@ def create_app(test_config=None):
db.init_app(app)
migrate.init_app(app, db)

#Register Blueprints Here
# Register Blueprints Here
from .video_routes import video_bp
app.register_blueprint(video_bp)
from .customer_routes import customers_bp
app.register_blueprint(customers_bp)
from .rental_routes import rentals_bp
app.register_blueprint(rentals_bp)

return app
return app
95 changes: 95 additions & 0 deletions app/customer_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from app import db
from app.models.customer import Customer
from flask import Blueprint, jsonify, request
from datetime import datetime

customers_bp = Blueprint("customers_bp", __name__, url_prefix="/customers")


@customers_bp.route("", methods=["POST"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def customer_create():
request_body = request.get_json()

if "name" not in request_body or "phone" not in request_body or "postal_code" not in request_body:
response_body ={}
if "name" not in request_body:
response_body["details"] = "Request body must include name."
elif "phone" not in request_body:
response_body["details"] = "Request body must include phone."
elif "postal_code" not in request_body:
response_body["details"] = "Request body must include postal_code."
return jsonify(response_body), 400

new_customer = Customer(
name=request_body["name"],
phone=request_body["phone"],
postal_code=request_body["postal_code"],

)
db.session.add(new_customer)
db.session.commit()

return jsonify({"id": new_customer.customer_id}), 201


@customers_bp.route("", methods=["GET"])
def handle_customers():
customers = Customer.query.all()
response_body = []
for customer in customers:
response_body.append(customer.customer_dict())

Comment on lines +38 to +41

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List comprehension could be used here

Suggested change
response_body = []
for customer in customers:
response_body.append(customer.customer_dict())
response_body = [ customer.customer_dict() for customer in customers]

return jsonify(response_body), 200


@customers_bp.route("/<customer_id>", methods=["GET"])
def customer_get(customer_id):
try:
customer = Customer.query.get(customer_id)
except:
return jsonify({"message": f"Customer {customer_id} was not found"}), 400

if customer is None:
return jsonify({"message": f"Customer {customer_id} was not found"}), 404

return jsonify(customer.customer_dict()), 200

return jsonify(customer.customer_dict()), 200
Comment on lines +55 to +57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate return statement should be removed.

Suggested change
return jsonify(customer.customer_dict()), 200
return jsonify(customer.customer_dict()), 200
return jsonify(customer.customer_dict()), 200


# WORK IN PROGRESS
@customers_bp.route("/<customer_id>", methods=["PUT"])
def customer_put(customer_id):
customer = Customer.query.get(customer_id)
if customer == None:
return jsonify({"message": f"Customer {customer_id} was not found"}), 404

request_body = request.get_json()
if "name" not in request_body or "phone" not in request_body or "postal_code" not in request_body:
return jsonify(), 400

customer.name = request_body["name"]
customer.phone = request_body["phone"]
customer.postal_code = request_body["postal_code"]

if "name" in request_body or "phone" in request_body or "postal_code" in request_body:
response_body ={}
response_body["name"] = customer.name
response_body["phone"] = customer.phone
response_body["postal_code"] = customer.postal_code
Comment on lines +67 to +78

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard clause in lines 67-68 already checks for non-existent name, phone, or postal_code values by exiting the function early. This means we can refactor by removing the check for these same fields on line 74.

Suggested change
if "name" not in request_body or "phone" not in request_body or "postal_code" not in request_body:
return jsonify(), 400
customer.name = request_body["name"]
customer.phone = request_body["phone"]
customer.postal_code = request_body["postal_code"]
if "name" in request_body or "phone" in request_body or "postal_code" in request_body:
response_body ={}
response_body["name"] = customer.name
response_body["phone"] = customer.phone
response_body["postal_code"] = customer.postal_code
if "name" not in request_body or "phone" not in request_body or "postal_code" not in request_body:
return jsonify(), 400
customer.name = request_body["name"]
customer.phone = request_body["phone"]
customer.postal_code = request_body["postal_code"]
response_body ={}
response_body["name"] = customer.name
response_body["phone"] = customer.phone
response_body["postal_code"] = customer.postal_code

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can refactor this section even more by building the response dictionary in the return statement using the Customer customer_dict method:

Suggested change
if "name" not in request_body or "phone" not in request_body or "postal_code" not in request_body:
return jsonify(), 400
customer.name = request_body["name"]
customer.phone = request_body["phone"]
customer.postal_code = request_body["postal_code"]
if "name" in request_body or "phone" in request_body or "postal_code" in request_body:
response_body ={}
response_body["name"] = customer.name
response_body["phone"] = customer.phone
response_body["postal_code"] = customer.postal_code
if "name" not in request_body or "phone" not in request_body or "postal_code" not in request_body:
return jsonify(), 400
customer.name = request_body["name"]
customer.phone = request_body["phone"]
customer.postal_code = request_body["postal_code"]
db.session.commit()
return jsonify(customer.customer_dict()), 200


db.session.commit()

return jsonify(response_body), 200


@customers_bp.route("/<customer_id>", methods=["DELETE"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def customer_delete(customer_id):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

customer = Customer.query.get(customer_id)

if customer == None:
return jsonify({"message": f"Customer {customer_id} was not found"}), 404

db.session.delete(customer)
db.session.commit()

return jsonify({"id": customer.customer_id}), 200
29 changes: 28 additions & 1 deletion app/models/customer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
from app import db
from sqlalchemy.sql.functions import func

class Customer(db.Model):
id = db.Column(db.Integer, primary_key=True)
customer_id = db.Column(db.Integer, primary_key=True, autoincrement=True)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer primary key columns will auto-increment by default so it's not required to provide that setting.

We can read more about the autoincrement attribute in the SQL_Alchemy documentation

name = db.Column(db.String)
phone = db.Column(db.String)
postal_code = db.Column(db.String)
# server_default tells sqlA to pass the default value as part of the CREATE TABLE
# func.now() or func.current_timestamp() - they are aliases of each other. This tells DB to calcaate the timestamp itself
registered_at = db.Column(db.DateTime(timezone=True), server_default=func.now())
Comment on lines +6 to +11

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, any of these columns can contain null values. Consider revisiting each column to determine which column should truly have null values versus not. For example, is it useful to store a Customer without a name or phone number? Those two columns can be considered required information to store about a customer, despite our requirements not strictly describing that behavior.

#I DON'T KNOW IF THIS IS CORRECT
#adding videos attribute to Customer Model
videos = db.relationship("Video", secondary="rental", backref="customers")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secondary attribute can be removed from this relationship. The secondary attribute is used with pure join tables (whose sole purpose is just to map out the relationship between two tables). In this case, rental is actually not a pure join table and contains its own columns we want to keep track of ( due_date ).

So in this case, adding the relationship with backref to rental in both Video and Customer models would suffice.

rentals = db.relationship("rentals",  backref="customers")

#videos = db.relationship("Rental", back_populates="customer")


def customer_dict(self):
dict = {
"id": self.customer_id,
"name": self.name,
"phone": self.phone,
"postal_code": self.postal_code,
# weekday|day of month (16)|month name|year|local version of time|, UTC offset +HHMM or -HHMM
"registered_at": self.registered_at.strftime("%a, %-d %b %Y %X %z")
}
# if self.registered_at is not None:
# dict["registered_at"] = self.registered_at.strfttime("%a, %-d %b %Y %X %z")

return dict
Comment on lines +18 to +30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great helper function!


11 changes: 10 additions & 1 deletion app/models/rental.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
from app import db
from datetime import datetime, timedelta

class Rental(db.Model):
id = db.Column(db.Integer, primary_key=True)
rental_id = db.Column(db.Integer, primary_key=True, autoincrement=True)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

customer_id = db.Column(db.Integer, db.ForeignKey('customer.customer_id'), primary_key=True,nullable=False)
video_id = db.Column(db.Integer, db.ForeignKey('video.video_id'), primary_key=True,nullable=False)
due_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow() + timedelta(days = 7))
#bool is truthy and falsey
checked_out = db.Column(db.Boolean, default=False)
#need to update check_in and check_out routes. Change check_in to True and False for checkout
#video = db.relationship("Video", back_populates="customers")
#customer = db.releationship("Customer", back_populates="videos")
15 changes: 14 additions & 1 deletion app/models/video.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
from app import db

class Video(db.Model):
id = db.Column(db.Integer, primary_key=True)
video_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String)
release_date = db.Column(db.DateTime)
#how many I own
total_inventory = db.Column(db.Integer)
#customers = db.relationship("Rental", back_populates="video")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Video should have a relationship with Rental so this line shouldn't be commented. We want to be able to identify videos that are 'rented' :

Suggested change
#customers = db.relationship("Rental", back_populates="video")
rentals = db.relationship("Rental", backref="video")

#checked_out column
def to_dict(self):
return {
"id": self.video_id,
"title": self.title,
"release_date": self.release_date,
"total_inventory": self.total_inventory
}
174 changes: 174 additions & 0 deletions app/rental_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from flask.wrappers import Response
from app import db
from app.customer_routes import customers_bp
from app.video_routes import video_bp
from app.models.customer import Customer
from app.models.video import Video
from app.models.rental import Rental
from flask import Blueprint, jsonify, request
from datetime import datetime

rentals_bp = Blueprint("rentals_bp", __name__, url_prefix="/rentals")


@rentals_bp.route("/check-out", methods=["POST"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def rentals_checkout():
#{"c_id": [1], "v_id": [4]}
request_body = request.get_json()

# is this key string not in the request_body return 400
if "customer_id" not in request_body or "video_id" not in request_body:
return jsonify(), 400

# get instance of a customer and an instance of a video and return 404 if it's None
customer = Customer.query.get(request_body["customer_id"])
video = Video.query.get(request_body["video_id"])
if video is None or customer is None:
return jsonify(), 404

# filtering the video ID & checked_out attributes and count the records
rentals = Rental.query.filter_by(
video_id=video.video_id, checked_out=True).count()

# finding avaliable inventory
available_inventory = video.total_inventory - rentals

if available_inventory == 0:
return jsonify({"message": "Could not perform checkout"}), 400

# Instantiate a new instance for rental
new_rental = Rental(
video_id=video.video_id,
customer_id=customer.customer_id
)

# staging rental instance to database
db.session.add(new_rental)
# set rental instance to True
new_rental.checked_out = True
# commit to database
db.session.commit()

# count() - tells how many rentals are currently checked out
videos_checked_out = Rental.query.filter_by(
video_id=video.video_id, checked_out=True).count()

available_inventory = video.total_inventory - videos_checked_out

# return the response body and status code
return jsonify({
"video_id": new_rental.video_id,
"customer_id": new_rental.customer_id,
"videos_checked_out_count": videos_checked_out,
"available_inventory": available_inventory
}), 200


@rentals_bp.route("/check-in", methods=["POST"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def rentals_checkin():
request_body = request.get_json()

# checks that the required request body parameters are in request
if "customer_id" not in request_body or "video_id" not in request_body:
return jsonify(), 400

# store customer/video instance into variables. If customer/video does not exist, returns 404
customer = Customer.query.get(request_body["customer_id"])
video = Video.query.get(request_body["video_id"])
if customer is None or video is None:
return jsonify(), 404

# query through Rental to find matching customer and video ids and return first on the list
rental = Rental.query.filter_by(
customer_id=customer.customer_id, video_id=video.video_id).first()
# (customer_id=customer.customer_id, video_id=video.video_id, checked_out=True).first() #this also passed the test
Comment on lines +82 to +84

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the event that a customer were to check out multiple copies of the same video, including the checked_out argument in the filter_by would help narrow down which 'copy' to check-in.

 rental = Rental.query.filter_by(customer_id=customer.customer_id, video_id=video.video_id, checked_out=True).first()
``


# "is" (compare location in memory) is 50% faster than "==" (comparing values)
if rental is None:
return jsonify({"message": f"No outstanding rentals for customer {customer.customer_id} and video {video.video_id}"}), 400

# rental.checked_out = True
db.session.commit()

# return response body
videos_checked_out = Rental.query.filter_by(
video_id=video.video_id, checked_out=False).count()

# finding avaiable inventory
available_inventory = video.total_inventory - videos_checked_out

return jsonify({
"video_id": video.video_id,
"customer_id": customer.customer_id,
"videos_checked_out_count": videos_checked_out,
"available_inventory": available_inventory
})


@customers_bp.route("/<customer_id>/rentals", methods=["GET"])
def customer_read(customer_id):
""" List the videos a customer currently has checked out """
request_body = request.get_json()

# checks to see if customer exists. If not, returns 404
customer = Customer.query.get(customer_id)
if customer is None:
return jsonify({"message": f"Customer {customer_id} was not found"}), 404

# sets up empty list to store a customer's checked out videos & iterates through customer.videos to retreive all videos a customer currently has
checked_out = []
for video in customer.videos:
checked_out.append(video)

# gets rental instance for each video a customer has
rentals = Rental.query.all()
customer_rentals = []
for video in checked_out:
for rental in rentals:
if video.video_id == rental.video_id and customer.customer_id == rental.customer_id:
customer_rentals.append(rental)
Comment on lines +124 to +129

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also look for customer rentals by querying the rental table using the customer id.

customer_rentals = Rental.query.filter_by(customer_id=customer_id, checked_in_status=False).all()


# create response body
response_body = []
for rental in customer_rentals:
response_body.append({
"release_date": video.release_date,
"title": video.title,
"due_date": rental.due_date
})
return jsonify(response_body)


@video_bp.route("/<video_id>/rentals", methods=["GET"])
def video_read(video_id):
""" List the customers who currently have the video checked out """
request_body = request.get_json()

# checks to see if video exists. If not, returns 404
video = Video.query.get(video_id)
if video is None:
return jsonify({"message": f"Video {video_id} was not found"}), 404

# sets up empty list to store the video's current customers & iterates through video.customers to retreive all customers that currently have the video
current_customers = []
for customer in video.customers:
current_customers.append(customer)

# gets rental instance for each customer a video has
rentals = Rental.query.all()
video_rentals = []
for customer in current_customers:
for rental in rentals:
if video.video_id == rental.video_id and customer.customer_id == rental.customer_id:
video_rentals.append(rental)
Comment on lines +158 to +163

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than querying the entire rentals table, we could filter rental records to a specific video id and appending each video found to the video_rentals list.:

customer_rentals = Rental.query.filter_by(video_id=video.video_id).all()


# create response body
response_body = []
for rental in video_rentals:
response_body.append({
"due_date": rental.due_date,
"name": customer.name,
"phone": customer.phone,
"postal_code": customer.postal_code
})
return jsonify(response_body)
Empty file removed app/routes.py
Empty file.
Loading