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

Cedar - Ana S. & Maria O. #37

Open
wants to merge 57 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8d4e73a
initial commit - CREATE migrations
Anagabsoares Nov 8, 2021
9dbf424
Create Customer model with id, name, postal_code, phone_number, regis…
mobregong Nov 8, 2021
d9ca38e
Corrects customer model so that name, postal_code, and phone_number a…
mobregong Nov 8, 2021
5828c0a
Create Video Model and post and get routes
Anagabsoares Nov 9, 2021
8d0c384
Create function to delete and update videos
Anagabsoares Nov 9, 2021
a865298
delete comments
Anagabsoares Nov 9, 2021
ea6e7a0
Merge pull request #1 from mobregong/as/customers
Anagabsoares Nov 9, 2021
e89c873
create routes folder
Anagabsoares Nov 9, 2021
1a7b99f
Merge pull request #2 from mobregong/as/wave2
Anagabsoares Nov 9, 2021
e42e144
Wave 1 passing for Customer
mobregong Nov 10, 2021
8fe14eb
Creates customer file within routes folder
mobregong Nov 10, 2021
bcf8da8
Merges video and main (had accidentally merged customer and video
mobregong Nov 10, 2021
fa6f4dc
Updates routes init to properly import customers_bp
mobregong Nov 10, 2021
d5a8ae8
Gets rid of comments
mobregong Nov 10, 2021
51127ec
Created rental model
mobregong Nov 10, 2021
4a723b0
Create check-out route. Passing check-outs tests on Wave 02
mobregong Nov 11, 2021
f1a3814
Delete duplicated files
Anagabsoares Nov 11, 2021
859b3a9
Merge branch 'master' of https://github.com/mobregong/retro-video-store
Anagabsoares Nov 11, 2021
c900270
create Rental model
Anagabsoares Nov 11, 2021
9bcb065
Merge branch 'master' of https://github.com/mobregong/retro-video-sto…
Anagabsoares Nov 11, 2021
c6b1559
Refactor rental model
Anagabsoares Nov 11, 2021
ad76349
Implement handle_video_rental function
Anagabsoares Nov 11, 2021
ab64d0b
Edit delete_video and pass test_can_delete_video_with_rental
Anagabsoares Nov 11, 2021
aa4a56b
Suggestions for check_in implementation
Anagabsoares Nov 12, 2021
02ebaa0
edit to_dic()
Anagabsoares Nov 12, 2021
e51103b
Merge pull request #3 from mobregong/as/wave2
Anagabsoares Nov 12, 2021
8baa451
Edit rentals to_dict()
Anagabsoares Nov 12, 2021
e4bacac
Adds get_rentals_by_customer_id route and refactors equivalent route …
mobregong Nov 12, 2021
d39d20d
Create check_in()
Anagabsoares Nov 12, 2021
3fd276c
Merge branch 'mast
Anagabsoares Nov 12, 2021
de7c00a
Implement check_in function
Anagabsoares Nov 12, 2021
ae87331
Merge branch 'master' of https://github.com/mobregong/retro-video-sto…
Anagabsoares Nov 12, 2021
25635d1
Implement check_in and passes wave 2
Anagabsoares Nov 12, 2021
82597cd
Edit check_in and pass wave 2 tests
Anagabsoares Nov 12, 2021
7d088b7
Edit check-in
Anagabsoares Nov 12, 2021
b22e284
Merge pull request #4 from mobregong/as/wave2
Anagabsoares Nov 13, 2021
b897585
refactor videos routes
Anagabsoares Nov 13, 2021
10aa325
edit update_video()
Anagabsoares Nov 13, 2021
7392940
Merge pull request #5 from mobregong/as/wave2
Anagabsoares Nov 13, 2021
63a3dc3
Refactor valid_int
Anagabsoares Nov 13, 2021
0ce429d
Add validate_request function
Anagabsoares Nov 13, 2021
7423a6f
Refactor update video
Anagabsoares Nov 13, 2021
323c9d1
Refactor checkou_video Remove else
Anagabsoares Nov 13, 2021
e4d82d4
Create get_overdue_videos
Anagabsoares Nov 13, 2021
c198f20
Create test case : test_get_videos_overdue_is_true'
Anagabsoares Nov 13, 2021
96fe624
Refactor get_overdue_videos()
Anagabsoares Nov 13, 2021
5f29f40
Create get_video_history route
Anagabsoares Nov 13, 2021
70a72c5
Fix typos
Anagabsoares Nov 13, 2021
b5570d6
Create test case for test_get_video_history_video_not_found and test_…
Anagabsoares Nov 14, 2021
67449a7
create pytest fixture for optional enhancements
Anagabsoares Nov 15, 2021
00605ea
Create test cases for videos_sort_title and videos_sort_release_date …
Anagabsoares Nov 15, 2021
ef9f5b7
refactor GET videos route to include query parameters: pagination and…
Anagabsoares Nov 15, 2021
2c9c287
format routes
Anagabsoares Nov 15, 2021
0de537a
Merge pull request #6 from mobregong/as/wave2
Anagabsoares Nov 15, 2021
c41252c
Adds gunicorn
mobregong Nov 15, 2021
507757e
Update app/routes/videos.py
Anagabsoares Nov 19, 2021
5f0eabe
Update app/routes/rentals.py
Anagabsoares Nov 19, 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
6 changes: 6 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,11 @@ def create_app(test_config=None):
migrate.init_app(app, db)

#Register Blueprints Here
from app.routes.customers import customers_bp
app.register_blueprint(customers_bp)
from app.routes.videos import video_bp
app.register_blueprint(video_bp)
from app.routes.rentals import rentals_bp
app.register_blueprint(rentals_bp)

return app
18 changes: 17 additions & 1 deletion app/models/customer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
from app import db
import datetime

class Customer(db.Model):
id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False)
postal_code = db.Column(db.String, nullable=False)
phone = db.Column(db.String, nullable=False)
registered_at = db.Column(db.DateTime(timezone=True), default=datetime.datetime.utcnow())



def to_dict(self):
return {
"id": self.id,
"name": self.name,
"postal_code": self.postal_code,
"phone": self.phone,
"registered_at": self.registered_at
}
22 changes: 21 additions & 1 deletion app/models/rental.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
from app import db
import datetime

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

# relationship
customer_id = db.Column(db.Integer, db.ForeignKey('customer.id'), primary_key=True,nullable=False)
video_id = db.Column(db.Integer, db.ForeignKey('video.id'), primary_key=True,nullable=False)
# extra columns
due_date = db.Column(db.DateTime(timezone=True),nullable=True)
available_inventory = db.Column(db.Integer, nullable=False)
videos_checked_out_count = db.Column(db.Integer, nullable=True)
Comment on lines +11 to +12

Choose a reason for hiding this comment

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

Consider calculating available inventory based of the number of rentals a video currently has. For instance something like, for the video model: available_inventory = len([rental for rental in self.rentals if not rental.check_in_date])

This method requires that with have a rentals relationships column on our video: rentals = db.relationship('Rental', back_populates='customer', lazy=True)

This method of dynamically computing available_inventory rather than storing it in an instance variable makes it so there isn’t information stored in multiple places that could potentially conflict. Also something to note is that available_inventory is more of an attribute of a video rather than rental

Choose a reason for hiding this comment

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

Similar to available_inventory, consider how you can dynamically calculate videos_checked_out_count for each customer by checking the length of the rentals attribute for a customer instance.

checked_in = db.Column(db.DateTime(timezone=True), nullable=True)
checkout_date = db.Column(db.DateTime(timezone=True), nullable=True)


def to_dict(self):
return{
"customer_id": self.customer_id,
"video_id": self.video_id,
"due_date": self.due_date,
"available_inventory": self.available_inventory,
"videos_checked_out_count": self.videos_checked_out_count if self.videos_checked_out_count else False,
}
17 changes: 16 additions & 1 deletion app/models/video.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
from app import db

class Video(db.Model):
id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String(50), nullable=False)
release_date = db.Column(db.Date(), nullable=True)
total_inventory = db.Column(db.Integer(), nullable=False)
# relationship
customers = db.relationship("Customer", secondary="rental", backref="video",lazy=True)

Choose a reason for hiding this comment

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

Nice work creating this secondary relationship column!



def to_json(self):
return {
"id": self.id,
"title": self.title,
"release_date": self.release_date,
"total_inventory" : self.total_inventory
}

Empty file removed app/routes.py
Empty file.
89 changes: 89 additions & 0 deletions app/routes/customers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from app import db
from app.models.customer import Customer
from flask import Blueprint, jsonify, request, make_response, abort
from app.models.rental import Rental
from app.models.video import Video
from app.routes.helper_functions import *


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


@customers_bp.route("", methods=["GET"], strict_slashes=False)
def get_all_customers():
customer_objects = Customer.query.all()
response_list = []
for customer in customer_objects:
response_list.append(customer.to_dict())
Comment on lines +15 to +17

Choose a reason for hiding this comment

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

This could be a nice place to use a list comprehension

return make_response(jsonify(response_list), 200)

# Get one
@customers_bp.route("/<customer_id>", methods=["GET"])
def get_customer(customer_id):
selected_customer = get_customer_from_id(customer_id)
return make_response(selected_customer.to_dict(), 200)

# Add customer
@customers_bp.route("", methods=["POST"], strict_slashes=False)
def add_customer():
request_body = request.get_json()
if "postal_code" not in request_body:
response_body = {"details": "Request body must include postal_code."}
response = abort(make_response(response_body, 400))
if "name" not in request_body:
response_body = {"details": "Request body must include name."}
response = abort(make_response(response_body, 400))
if "phone" not in request_body:
response_body = {"details": "Request body must include phone."}
response = abort(make_response(response_body, 400))
new_customer = Customer(
name=request_body["name"],
postal_code=request_body["postal_code"],
phone=request_body["phone"]
)
db.session.add(new_customer)
db.session.commit()
response = make_response(new_customer.to_dict(), 201)
return response

# Delete customer
@customers_bp.route("/<customer_id>", methods=["DELETE"], strict_slashes=False)
def delete_customer(customer_id):
selected_customer = get_customer_from_id(customer_id)
db.session.delete(selected_customer)
db.session.commit()
return make_response({"id": int(customer_id)}, 200)

# Update customer
@customers_bp.route("/<customer_id>", methods=["PUT"], strict_slashes=False)
def update_customer(customer_id):
selected_customer = get_customer_from_id(customer_id)
request_body = request.get_json()
if "name" in request_body:
selected_customer.name = request_body["name"]
else:
abort(make_response({"error": "Invalid input"},400))
if "phone" in request_body:
selected_customer.phone = request_body["phone"]
else:
abort(make_response({"error": "Invalid input"},400))
if "postal_code" in request_body:
selected_customer.postal_code = request_body["postal_code"]
else:
abort(make_response({"error": "Invalid input"},400))
Comment on lines +62 to +73

Choose a reason for hiding this comment

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

Note the similarities between this code and the code on lines 30-38. Consider how you could use a helper function to DFRY up your code.

db.session.commit()
response_body = jsonify(selected_customer.to_dict())
return make_response(response_body, 200)

# Get rentals by customer id
@customers_bp.route("/<id>/rentals", methods=["GET"], strict_slashes=False)
def get_rentals_by_customer_id(id):
get_customer_from_id(id)
rentals = Rental.query.filter_by(customer_id=id).all()
response_body = []
for rental in rentals:
video = get_video_by_id(rental.video_id)
response_body.append({"release_date": video.release_date,
"title": video.title,
"due_date": rental.due_date})
Comment on lines +86 to +88

Choose a reason for hiding this comment

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

Consider creating an instance method to build the dictionary for you response to enhance readability and changeability.

return make_response(jsonify(response_body), 200)
42 changes: 42 additions & 0 deletions app/routes/helper_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from app import db

Choose a reason for hiding this comment

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

Great work encapsulating this functionality in helper functions and breaking it out into it's own file for use throughout your program!

from app.models.customer import Customer
from flask import make_response, abort
from app.models.video import Video



def valid_int(number):
try:
id = int(number)
return id
except:
response_body = 'Invalid Data'
abort(make_response(response_body,400))



def get_customer_from_id(customer_id):
id = valid_int(customer_id)
customer = Customer.query.filter_by(id=id).one_or_none()
if customer is None:
response_body = {"message": f"Customer {id} was not found"}
abort(make_response(response_body, 404))
return customer

def get_video_by_id(video_id):
id = valid_int(video_id)
video = Video.query.filter_by(id=id).one_or_none()
if video is None:
response_body = {"message": f"Video {id} was not found"}
abort(make_response(response_body, 404))
return video

def get_customer_and_video_id(request_body):
customer = get_customer_from_id(request_body['customer_id'])
video = get_video_by_id(request_body['video_id'])
return customer,video


def validate_request(action, request_body):
if "customer_id" not in request_body or "video_id" not in request_body:
return abort(make_response({"message": f"Could not perform {action}"}, 400))
101 changes: 101 additions & 0 deletions app/routes/rentals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from app import db
from app.models.rental import Rental
from app.models.video import Video
from flask import Blueprint, request, make_response, jsonify
from datetime import datetime, timedelta, timezone
from app.routes.helper_functions import *
rentals_bp = Blueprint('rentals', __name__, url_prefix='/rentals')



'''
handling request,
is it valid?,
getting filtered data,
checking out,
returning data
'''
@rentals_bp.route("/check-out", methods=["POST"], strict_slashes=False)
def check_out_video():
request_body = request.get_json()
validate_request("checkout", request_body)
customer, video = get_customer_and_video_id(request_body)

videos_checked_out_by_customer_count = Rental.query.filter_by(customer_id=customer.id).count()
videos_id_checked_out_count = Rental.query.filter_by(video_id=video.id).count()
available_inventory = video.total_inventory - videos_id_checked_out_count

if not available_inventory:
return make_response({"message": f"Could not perform checkout"},400)

videos_checked_out_by_customer_count += 1
available_inventory -= 1
checked_out_date =datetime.utcnow()
due_date = checked_out_date + timedelta(days=7)

new_rental = Rental(video_id=video.id,
customer_id=customer.id,
due_date=due_date,
available_inventory=available_inventory,
videos_checked_out_count=videos_checked_out_by_customer_count,
checkout_date= checked_out_date)
Comment on lines +24 to +41

Choose a reason for hiding this comment

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

Consider moving this logic into a class method on the Rental model. In general, we prefer to encapsulate logic that manipulates data in a method attached to the model, leaving the route function to process the request body, and create the response body.


db.session.add(new_rental)
db.session.commit()
response_body = new_rental.to_dict()
return make_response(response_body, 200)


# Post check-in
'''
* handling request,
* is it valid?,
* getting filtered data,
* handling test_checkin_video_not_checked_out,
* checking in rental, returning data
'''
@rentals_bp.route("/check-in", methods=["POST"], strict_slashes=False)
def check_in_video():

request_body = request.get_json()

validate_request("checkin", request_body)
customer, video = get_customer_and_video_id(request_body)
rentals = Rental.query.filter_by(video_id=video.id, customer_id=customer.id).one_or_none()

if rentals == None:
return make_response({"message": f"No outstanding rentals for customer {customer.id} and video {video.id}"}, 400)

if not rentals.checked_in:
rentals.due_date = None,
rentals.available_inventory = rentals.available_inventory + 1
rentals.videos_checked_out_count= rentals.videos_checked_out_count - 1
rentals.checked_in =datetime.utcnow()

db.session.commit()

response_body = rentals.to_dict()
return make_response(response_body, 200)

@rentals_bp.route("/overdue", methods=["GET"], strict_slashes=False)

Choose a reason for hiding this comment

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

Great work implementing this route!

def get_overdue_videos():
now = datetime.now(timezone.utc)
rentals_overdue = Rental.query.filter(Rental.due_date < now).all()
response_body = []

for rental in rentals_overdue:
customer = get_customer_from_id(rental.customer_id)
video = get_video_by_id(rental.video_id)

response_body.append(
{"customer_id": rental.customer_id,
"video_id": rental.video_id,
"Video Title": video.title,
"Costumer name": customer.name,
"postal_code": customer.postal_code,
"due_date":rental.due_date,
"checkout_date":rental.checkout_date
}
)

return make_response(jsonify(response_body), 200)
Loading