Skip to content

Angela Amethyst A19 #130

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 7 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
import os
from dotenv import load_dotenv

Choose a reason for hiding this comment

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

Great work! We need to import load_dotenv to set up environment variables in our .env file



db = SQLAlchemy()

Choose a reason for hiding this comment

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

Do you remember what this does?

migrate = Migrate()
load_dotenv()
Comment on lines 7 to 9

Choose a reason for hiding this comment

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

🎉



def create_app(test_config=None):

app = Flask(__name__)

app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

if test_config is None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(

Choose a reason for hiding this comment

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

Why do we need this line?

"SQLALCHEMY_DATABASE_URI")
"RENDER_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
Expand All @@ -30,5 +31,9 @@ def create_app(test_config=None):
migrate.init_app(app, db)

Choose a reason for hiding this comment

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

Great job! We need to pass our instance of our app & the instance of our SQLALchemy db to connect the db and migrate to our Flask app


# Register Blueprints here
from .routes import goals_bp
app.register_blueprint(goals_bp)
from .routes import tasks_bp
app.register_blueprint(tasks_bp)
Comment on lines 33 to +37

Choose a reason for hiding this comment

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

Great work registering these blueprints ✅


return app
10 changes: 10 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@

Choose a reason for hiding this comment

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

Great job completing Task List Angela! Your code looks clean and easily readable. I want to point out the good variable naming and your code is DRY. Great use of helper methods in your models! Excellent work using blue prints & creating RESTful CRUD routes for each model.

class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
tasks = db.relationship("Task", back_populates="goal")

def to_dict(self):
goal_as_dict = {
"id": self.goal_id,
"title": self.title
}

return goal_as_dict
Comment on lines +9 to +15

Choose a reason for hiding this comment

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

Great job creating this reusable helper function`

22 changes: 21 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
from app import db


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
description = db.Column(db.String)
completed_at = db.Column(db.DateTime, nullable=True)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)
goal = db.relationship("Goal", back_populates="tasks")
Comment on lines +8 to +9

Choose a reason for hiding this comment

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

Nice approach creating this relationship to your Goal model. Why not use lazy here?


def to_dict(self):
task_as_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": True if self.completed_at else False,

}

if self.goal_id:
task_as_dict["goal_id"] = self.goal_id

return task_as_dict


Choose a reason for hiding this comment

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

Looks like there's some extra whitespace here

236 changes: 235 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,235 @@
from flask import Blueprint
from flask import Flask, Blueprint, jsonify, abort, make_response, request

Choose a reason for hiding this comment

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

👍🏾 These imports help a great deal when it comes to our request & response cycle

from app import db
from app.models.goal import Goal
from app.models.task import Task
from datetime import date

goals_bp = Blueprint("goals", __name__, url_prefix="/goals")
tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks")

Comment on lines +7 to +9

Choose a reason for hiding this comment

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

👍🏾

#TASK ROUTES

def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({"message": f"{model_id} is not a valid type ({type(model_id)}). Must be an integer)"}, 400))

model = cls.query.get(model_id)

if not model:
abort(make_response({"message": f"{cls.__name__} {model_id} does not exist"}, 404))

return model
Comment on lines +12 to +23

Choose a reason for hiding this comment

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

Nice job creating this reusable function & keeping your code DRY. Nice approach to handling an invalid model 😁


###

@tasks_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 create_task():

request_body = request.get_json()

if "title" not in request_body or "description" not in request_body or "is_complete" == False:
abort(make_response({"details": "Invalid data"}, 400))

new_task = Task(title=request_body["title"],
description=request_body["description"])

if "completed_at" in request_body:
new_task.completed_at=request_body["completed_at"]

db.session.add(new_task)

Choose a reason for hiding this comment

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

Great job using db.session.<method-name>. Session gives us access to the follow:

  • db is our app’s instance of SQLAlchemy.
  • session represents our active database connection.
  • By referencing db.session we can use SQLAlchemy’s methods to perform tasks like:
    • committing a change to a model
    • storing a new record of a model
    • deleting a record.
  • By referencing db.session.add() you are able to use the SQLAlchemy method to store a new record of the task model

db.session.commit()

return {
"task": new_task.to_dict()
}, 201

@tasks_bp.route("", methods = ["GET"])
def read_all_tasks():

sort_query = request.args.get("sort")

if sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc()).all()
elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc()).all()
else:
tasks = Task.query.all()

tasks_response = []

for task in tasks:
tasks_response.append(task.to_dict())

return jsonify(tasks_response), 200

@tasks_bp.route("/<task_id>", methods = ["GET"])
def read_one_task(task_id):

task = validate_model(Task, task_id)

return jsonify({"task":task.to_dict()}), 200


@tasks_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):

task = validate_model(Task, task_id)

request_body = request.get_json()

task.title = request_body["title"]
task.description = request_body["description"]

db.session.add(task)
db.session.commit()

return jsonify({"task":task.to_dict()}), 200

@tasks_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task = validate_model(Task, task_id)

db.session.delete(task)
db.session.commit()

return make_response({"details": f"Task {task_id} \"{task.title}\" successfully deleted"})



@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def mark_task_complete(task_id):

task = validate_model(Task, task_id)

task.completed_at = date.today()

db.session.add(task)
db.session.commit()

return jsonify({"task":task.to_dict()}), 200

@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def mark_task_incomplete(task_id):

task = validate_model(Task, task_id)

task.completed_at = None

db.session.add(task)
db.session.commit()

return jsonify({"task":task.to_dict()}), 200

################################# GOAL ROUTES #######################################

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

def create_goal():

request_body = request.get_json()

if "title" not in request_body:
abort(make_response({"details": "Invalid data"}, 400))

new_goal = Goal(title=request_body["title"])

db.session.add(new_goal)
db.session.commit()

return {
"goal": new_goal.to_dict()
}, 201
Comment on lines +142 to +144

Choose a reason for hiding this comment

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

Usually a resource creation relates to a 201 response code 👍🏾





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

def read_all_goals():
goals = Goal.query.all()

goals_response = []

for goal in goals:
goals_response.append(goal.to_dict())

return jsonify(goals_response), 200



@goals_bp.route("/<goal_id>", methods = ["GET"])
def read_one_goal(goal_id):

goal = validate_model(Goal, goal_id)

return jsonify({"goal":goal.to_dict()}), 200



@goals_bp.route("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):

goal = validate_model(Goal, goal_id)

request_body = request.get_json()

goal.title = request_body["title"]

db.session.add(goal)
db.session.commit()

return jsonify({"goal":goal.to_dict()}), 200


@goals_bp.route("/<goal_id>", methods=["DELETE"])
def delete_goal(goal_id):
goal = validate_model(Goal, goal_id)

db.session.delete(goal)
db.session.commit()

return make_response({"details": f"Goal {goal_id} \"{goal.title}\" successfully deleted"})

############################## MANY TO MANY RELATIONSHIP #######################
@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_list_of_tasks(goal_id):
print("PRINT")
goal = validate_model(Goal, goal_id)

request_body = request.get_json()

for task_id in request_body["task_ids"]:
task = validate_model(Task, task_id)
if task not in goal.tasks:
goal.tasks.append(task)


task_ids=[]
for task in goal.tasks:
task_ids.append(task.task_id)

db.session.commit()
Comment on lines +204 to +214

Choose a reason for hiding this comment

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


return {
"id": int(goal_id),
"task_ids": task_ids }, 200

@goals_bp.route("/<goal_id>/tasks", methods=["GET"])

def read_task_of_one_goal(goal_id):

goal = validate_model(Goal, goal_id)

tasks_response = []

for task in goal.tasks:
tasks_response.append(task.to_dict())

return {
"id": int(goal_id),
"title": goal.title,
"tasks": tasks_response
}, 200
1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
45 changes: 45 additions & 0 deletions migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# A generic, single database configuration.

[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Loading