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 - Mary Tian and Ivette Fernandez #57

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

Mary-Tian
Copy link

No description provided.

Copy link

@anselrognlie anselrognlie left a comment

Choose a reason for hiding this comment

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

Well done!

Your code is easy to follow, and your tests all show green in VS Code. There are some warnings for a few of the tests when using pytest from the command line. It's always a good idea to double check the terminal output, since it's less likely to obscure such warnings.

Try looking for repetitive parts of the code and moving them to helper methods. Also, try to make your functions follow the single responsibility principle. So for example, think about splitting your route endpoint functions so that each function handles only one verb.

This also extends to the level of abstraction that we deal with in our routes. The main job of a route should be to get the data from the request, then call helper methods to do the real work (business logic), and finally take the results and package them to be sent back to the client. Currently, there's business logic (such as how to update the database to reflect checking out and in) in your routes. Think about moving that to methods in your existing model classes, or even creating entirely new classes that represent sequences of actions carried out at the level of business logic. Consider adding a class like RentalManager, that could have check_out and check_in methods to manage those workflows.

Overall, nicely done! 🎉

Comment on lines +35 to +38
from .routes import customers_bp, videos_bp, rentals_bp
app.register_blueprint(customers_bp)
app.register_blueprint(videos_bp)
app.register_blueprint(rentals_bp)

Choose a reason for hiding this comment

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

👍

One thing we can do to help our routes file from getting too large is to consider making multiple files containing routes separated by the resource type. We might have a routes folder instead of a routes.py file, and inside that folder (along with a __init__.py) we could have a file per resource, so customer.py, video.py, and rental.py. Where each would have a blueprint and endpoints for that resource. When we have one blueprint per file, we often name the blueprint simply bp rather than including the resource name as part of it.

Then here, we could import and register the blueprints like

    from .routes import customer, video, rental
    app.register_blueprint(customer.bp)
    app.register_blueprint(video.bp)
    app.register_blueprint(rental.bp)

__tablename__ = "customers_table"

Choose a reason for hiding this comment

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

There's not a strong need to choose a different name for the model table here. The primary reason we might need to do this is if we were using our model to wrap an existing database table that happens not to follow the naming defaults of SqlAlchemy (matching the model name), or if we were an SQL purist and preferred plural table names to represent that tables do store multiple rows. However, in that case we would generally avoid appending _table to the end of the table name (it's repetitive to have table in name of the table).

In general, it's good to know that the __tablename__ value exists, but we don't have much need to use it.

__tablename__ = "customers_table"
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.

According to the SqlAlchemy docs about autoincrement:

The default value is the string "auto" which indicates that a single-column primary key that is of an INTEGER type with no stated client-side or python-side defaults should receive auto increment semantics automatically; all other varieties of primary key columns will not.

This means that here, since the primary is not a compound key, the type is integer, and there's no specified default value, the desired autoincrement behavior will apply even without listing it. It doesn't hurt anything to list it, but it's not required.

Comment on lines +6 to +9
registered_at = db.Column(db.DateTime)
postal_code = db.Column(db.String)
phone = db.Column(db.String)

Choose a reason for hiding this comment

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

Keep in mind that the default value for nullable is True. When adding column definitions to our models, we should consider whether it makes sense to allow a record to exist without that data, such as a Customer without a name, a Video without a title, or a Rental without a due date. It's true that we can add checks for this in our logic that adds entries, but we should try to leverage as much validation help from the database as possible. If we tell the database that certain columns are required (not nullable), then we'll encounter an error if we accidentally try to create a row without a value for those columns (which is a good thing)!

postal_code = db.Column(db.String)
phone = db.Column(db.String)
videos = db.relationship("Video", secondary="rentals_table", backref="customers_table")

Choose a reason for hiding this comment

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

Notice that your code doesn't use this relationship in the logic anywhere. Using the secondary attribute on a relationship is more appropriate when we have a simple many-to-many relationship, such as with a pure join table. In that case, a relationship like this would allow SQLAlchemy to automatically manage the join table between the two models being linked and we could establish those links by using the relationship like a collection, by appending, removing, etc. In that case, the backref attribute supplies the equivalent name to attach to the model on the other end of the relationship. Since the local name here is videos, a closer match in semantics for the other side might be customers rather than customer_table.

However, in this application, our rental relationship between videos and customers isn't a pure join table. It has its own information that also needs to be tracked, and which SQLAlchemy can't automatically manage (such as the checked in status, and due date). In this case, using reciprocal one-to-many relationships can bring us some of the benefits of moving around the relationship links, while still leaving it to us to explicitly set up the rental model ourselves. Consider a configuration like the following:

    rentals = db.relationship("Rental", backref="customer")

If we had a customer model in the variable customer, then we could access the rentals associated with the customer as customer.rentals. If there were a similar relationship on the video side, then to get the video title of a particular customer rental, we could write code like customer.rentals[0].video.title. SQLAlchemy takes care of using the foreign key information to fill in the relationships.

All this being said, having this secondary relationship is what allows the deletion of a video or customer which has particiapted in a rental (whether current or in the past). When a video or customer is deleted, SQLAlchemy is tracking that and will delete any rental that also refers to that video or customer. Without this relationship, we might need to look into cascading delete behavior for one-to-many relationships (or try to avoid actually deleting data at all, since data = money).

Comment on lines +202 to +206
return jsonify({
"id" : video.id,
"title" : video.title,
"release_date" : video.release_date,
"total_inventory" : video.total_inventory

Choose a reason for hiding this comment

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

This is another spot that could benefit from moving the dictionary generation into a helper in the video model.

Comment on lines +223 to +226
if not (title and release_date and total_inventory):
return jsonify({
"details" : "Invalid data"
}), 400

Choose a reason for hiding this comment

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

👍

Good job performing similar validations as for creating a video record through POST.

Comment on lines +235 to +238
"id" : video.id,
"title" : video.title,
"release_date" : video.release_date,
"total_inventory" : video.total_inventory,

Choose a reason for hiding this comment

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

Here's a third repetition. As I said, if we write the same code three times, that's a great time to start thinking about drying it up.

"total_inventory" : video.total_inventory,
}), 200

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

Choose a reason for hiding this comment

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

Similar feedback as for the video routes apply here for the customer routes.

@@ -0,0 +1,54 @@
"""empty message

Choose a reason for hiding this comment

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

Remember to add a message to your migrations with -m when running the migration step.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants