Skip to content
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
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn 'app:create_app()'
1 change: 0 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
migrate = Migrate()
load_dotenv()


def create_app(test_config=None):
app = Flask(__name__)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
Expand Down
30 changes: 30 additions & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv
db = SQLAlchemy()
migrate = Migrate()
load_dotenv()


def create_app(test_config=None):
app = Flask(__name__)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
if test_config == None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_TEST_DATABASE_URI")
from app.models.task import Task
from app.models.goal import Goal

db.init_app(app)
migrate.init_app(app, db)

from .routes import tasks_bp, goals_bp
from .routes import tasks_bp
from .routes import goals_bp
app.register_blueprint(tasks_bp)
app.register_blueprint(goals_bp)
return app
Comment on lines +1 to +30

Choose a reason for hiding this comment

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

All this information stays in the root __init__.py file. Refer back to Flasky, Solar System API, and Book Review. This file should be completely empty.

Comment on lines +1 to +30

Choose a reason for hiding this comment

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

Suggested change
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv
db = SQLAlchemy()
migrate = Migrate()
load_dotenv()
def create_app(test_config=None):
app = Flask(__name__)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
if test_config == None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("SQLALCHEMY_TEST_DATABASE_URI")
from app.models.task import Task
from app.models.goal import Goal
db.init_app(app)
migrate.init_app(app, db)
from .routes import tasks_bp, goals_bp
from .routes import tasks_bp
from .routes import goals_bp
app.register_blueprint(tasks_bp)
app.register_blueprint(goals_bp)
return app

19 changes: 19 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
from app import db
from flask import current_app, jsonify

Choose a reason for hiding this comment

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

I don't see anywhere we use these in the Goal model. Let's remove this line.

Suggested change
from flask import current_app, jsonify

from sqlalchemy.orm import backref

Choose a reason for hiding this comment

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

we shouldn't need to import this explicitly. It is already accessible through the db import.

Suggested change
from sqlalchemy.orm import backref



class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)

Choose a reason for hiding this comment

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

See task.py for details about setting required attributes to nullable=False

tasks = db.relationship("Task", backref= "goal")

Choose a reason for hiding this comment

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

As much as I love backref 😭 , consider using back_populates from now on, as backref will soon be deprecated.


def goal_dict(self):
return{
"id":self.goal_id,
"title":self.title
}
@classmethod
def goal_arguments(cls,title_from_url):

Choose a reason for hiding this comment

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

Maybe we can make this method name a little more description. A model might have several arguments that will do different things. This looks to be filtering data, so perhaps we can call it filter or filter_goals.

Choose a reason for hiding this comment

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

I don't see this being used anywhere. We filter by ascending in one of the routes, that would be a perfect time to modify this and use it!

if title_from_url:
goals = Goal.query.filter_by(title=title_from_url).all()
if not goals:
goals = Goal.query.filter(Goal.title.contains(title_from_url))
else:
goals = Goal.query.all()
return goals
16 changes: 15 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
from app import db
from flask import current_app,request
from sqlalchemy import desc,asc
from dotenv import load_dotenv
Comment on lines +2 to +4

Choose a reason for hiding this comment

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

Always be aware of your imports and if you need them. Sometimes when typing, VSCode will try and help us out an auto-complete. But if we don't need it, don't see it used in our resources on Learn or in previous projects, we shouldn't keep it in.

Suggested change
from flask import current_app,request
from sqlalchemy import desc,asc
from dotenv import load_dotenv



class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
task_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String)
description = db.Column(db.String)
completed_at = db.Column(db.DateTime, nullable = True)
Comment on lines +9 to +11

Choose a reason for hiding this comment

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

It turns out that nullable=True is the default value for nullable. So all of the columns for Task here are currently marked as nullable. But should title or description be allowed to be NULL? (Does that make sense from a data standpoint?) Consider adding nullable=False to those columns.

The way the project emphasized that completed_at needs to accept NULL values may make it seem like we needed to explicitly call out that nullable should be True, but it turns out this is the default for nullable. Instead, we should think about the other data in our model and consider whether it makes sense for any of it to be NULL. If not, we can have the database help us protect against that happening!

goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"), nullable = True)

def task_dict(self):
task_dict = {"id": self.task_id, "title": self.title, "description": self.description, "is_complete": False if self.completed_at != None else True}

Choose a reason for hiding this comment

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

Good job using a ternary operator for is_complete. THis is a little hard to read, though, so maybe refactor it like so:

task_dict = {
    "id": self.task_id, 
    "title": self.title, 
    "description": self.description, 
    "is_complete": False if self.completed_at != None else True
}


return task_dict


191 changes: 190 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,190 @@
from flask import Blueprint
from flask import Blueprint, request, jsonify, make_response, abort
from app.models.task import Task
from app import db
from app.models.goal import Goal
from sqlalchemy import desc
from datetime import datetime
import requests
import os

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

@tasks_bp.route("", methods=["POST"])
def new_task():

request_body = request.get_json()
if 'title' not in request_body or 'description' \
not in request_body or 'completed_at' not in request_body:
return make_response({"details": "Invalid data"}, 400)
else:

new_task = Task(title=request_body["title"],
description=request_body["description"],
completed_at=request_body["completed_at"])
Comment on lines +16 to +24

Choose a reason for hiding this comment

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

This would make a good helper method inside the model class.

db.session.add(new_task)
db.session.commit()
is_complete = new_task.completed_at != None
return make_response({"task": {"id": new_task.task_id, "title": new_task.title, "description": new_task.description, "is_complete": is_complete}}, 201)

Choose a reason for hiding this comment

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

This works, but it is a little difficult to read the response body and quickly ascertain where one argument ends and another begins. Maybe turn this into a separate variable that is passed in.


@tasks_bp.route("", methods=["GET"])
def all_tasks():
title_query = request.args.get('title')
order_by_query = request.args.get('sort')
if title_query:
tasks = Task.query.filter_by(title = title_query)
elif order_by_query == 'asc':
tasks = Task.query.order_by(Task.title).all()

elif order_by_query == 'desc':
tasks = Task.query.order_by(desc(Task.title)).all()
else:
tasks = Task.query.order_by(Task.title).all()
Comment on lines +32 to +42

Choose a reason for hiding this comment

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

Looks like you have a helper method for filtering in the Goal model. You could do the same with Task.


task_response = []
for task in tasks:
is_complete = task.completed_at != None
task_response.append({'id': task.task_id, 'title': task.title, 'description': task.description,'is_complete': is_complete})
Comment on lines +44 to +47

Choose a reason for hiding this comment

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

This would also make another good helper method.


return jsonify(task_response), 200

@tasks_bp.route("/<task_id>", methods=["GET", "PUT", "DELETE", "PATCH"])
def one_task(task_id):
task = Task.query.get(task_id)
if task == None:
abort(404)
is_complete = task.completed_at != None
if request.method == "GET":
if task.goal_id:
return make_response({"task": {"id": task.task_id,"title": task.title, "description": task.description, "goal_id": task.goal_id, "is_complete": is_complete}}, 200)
else:
return make_response({"task": {"id": task.task_id, "title": task.title, "description": task.description, "is_complete": is_complete}}, 200)
elif request.method == "PUT":
form_data = request.get_json()
task.title = form_data["title"]
task.description = form_data["description"]
task.completed_at = task.completed_at
Comment on lines +64 to +66

Choose a reason for hiding this comment

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

Same as above! We can turn this into an "update" helper method in the Task model and get it out of the route


db.session.commit()

return make_response({"task": {"id": task.task_id, "title": task.title, "description": task.description, "is_complete": is_complete}}, 200)

elif request.method == "PATCH":
form_data = request.get_json()
if "title" in form_data:
task.title = form_data["title"]
if "description" in form_data:
task.description = form_data["description"]

db.session.commit()
return make_response({"task": {"id": task.task_id, "title": task.title, "description": task.description, "is_complete": is_complete}}, 200)
elif request.method == "DELETE":
db.session.delete(task)
db.session.commit()
return make_response({'details':f'Task {task.task_id} "{task.title}" successfully deleted'}, 200)

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

time_stamp = datetime.now()

Choose a reason for hiding this comment

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

Let's save ourselves a line of code here and create the timestamp down below where it's inevitably assigned.

Suggested change
time_stamp = datetime.now()

task = Task.query.get(task_id)
if task == None:
abort(404)
else:
task.completed_at = time_stamp

Choose a reason for hiding this comment

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

Now let's turn this into: task.completed_at = datetime.now()

db.session.commit()
is_complete = task.completed_at != None

try:
header= {"Authorization"}
post_body = requests.post(header)
except TypeError:
pass
Comment on lines +98 to +102

Choose a reason for hiding this comment

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

I see you started implementing the slack bot API here. When you get time, maybe over winter break, I highly recommend trying to finish it. Working with third-party APIs like this one will be something you'll be doing during your capstone.

return make_response({"task": {"id": task.task_id, "title": task.title, "description": task.description, "is_complete": is_complete}}, 200)

@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def task_not_complete(task_id):
task = Task.query.get(task_id)
if task == None:
abort(404)
else:
task.completed_at = None
db.session.commit()
is_complete = task.completed_at != None
return make_response({"task": {"id": task.task_id, "title": task.title, "description": task.description, "is_complete": is_complete}}, 200)

@goals_bp.route("", methods=["POST"])
def post_goal():
request_body = request.get_json()
if 'title' not in request_body :
return make_response({"details": "Invalid data"}, 400)
else:
new_goal = Goal(title=request_body["title"])

db.session.add(new_goal)
db.session.commit()
return make_response({"goal": {"id": new_goal.goal_id, "title": new_goal.title}}, 201)

@goals_bp.route("", methods=["GET"])
def all_goals():
title_query = request.args.get('title')
order_by_query = request.args.get('sort')
if title_query:
goals = Goal.query.filter_by(title = title_query)
elif order_by_query == 'asc':
goals = Goal.query.order_by(Goal.title).all()
elif order_by_query == 'desc':
goals = Goal.query.order_by(desc(Goal.title)).all()
else:
goals = Goal.query.order_by(Goal.title).all()

goal_response = []
for goal in goals:
goal_response.append({'id': goal.goal_id, 'title': goal.title})
Comment on lines +141 to +143

Choose a reason for hiding this comment

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

This would make a good helper method as well in the Goal model

return jsonify(goal_response), 200

@goals_bp.route("/<goal_id>", methods=["GET", "PUT", "DELETE"])
def one_goal(goal_id):
goal = Goal.query.get(goal_id)
if goal == None:
abort(404)
if request.method == "GET":
return make_response({"goal": {"id": goal.goal_id, "title": goal.title}}, 200)
elif request.method == "PUT":
form_data = request.get_json()
goal.title = form_data["title"]
db.session.commit()

return make_response({"goal": {"id": goal.goal_id, "title": goal.title}}, 200)
elif request.method == "DELETE":
db.session.delete(goal)
db.session.commit()
return make_response({'details':
f'Goal {goal.goal_id} "{goal.title}" successfully deleted'}, 200)

@goals_bp.route("/<goal_id>/tasks", methods=["POST", "GET"])
def task_and_goal(goal_id):
request_body = request.get_json()
tasks = Task.query.all()
goal = Goal.query.get(goal_id)
if goal == None:
abort(404)
Comment on lines +169 to +171

Choose a reason for hiding this comment

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

Here is another place we can create a helper function. Let's validate the parameter passed in through the route to check if it's the right data type, exists in our db, etc. We should do this for bouth our models.

Here's a class resource we made about this from Flasky

if request.method == "POST":
for task in tasks:
if task.task_id in request_body["task_ids"]:
task.goal_id = int(goal_id)
db.session.commit()
return make_response({"id": int(goal_id), "task_ids": request_body["task_ids"]}, 200)
elif request.method == "GET":
goals_tasks = Goal.query.get(goal_id).tasks

task_response = []
for task in goals_tasks:
is_complete = task.completed_at != None
task_response.append({'id': task.task_id,
'goal_id': task.goal_id,
'title': task.title,
'description': task.description,
'is_complete': is_complete})
Comment on lines +182 to +188

Choose a reason for hiding this comment

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

Another good candidate for a helper method in our Task model


return make_response({"id": goal.goal_id, "title": goal.title, "tasks": task_response}, 200)
49 changes: 18 additions & 31 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
alembic==1.5.4
attrs==20.3.0
autopep8==1.5.5
blinker==1.4
certifi==2020.12.5
chardet==4.0.0
click==7.1.2
Flask==1.1.2
Flask-Migrate==2.6.0
Flask-SQLAlchemy==2.4.4
alembic==1.8.1
attrs==21.4.0
click==8.1.3
Flask==2.2.2
Flask-Migrate==4.0.0
Flask-SQLAlchemy==3.0.2
gunicorn==20.1.0
idna==2.10
iniconfig==1.1.1
itsdangerous==1.1.0
Jinja2==2.11.3
Mako==1.1.4
MarkupSafe==1.1.1
packaging==20.9
pluggy==0.13.1
psycopg2-binary==2.9.4
py==1.10.0
pycodestyle==2.6.0
pyparsing==2.4.7
itsdangerous==2.1.2
Jinja2==3.1.2
Mako==1.2.3
MarkupSafe==2.1.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.7
pytest==7.1.1
pytest-cov==2.12.1
python-dateutil==2.8.1
python-dotenv==0.15.0
python-editor==1.0.4
requests==2.25.1
six==1.15.0
SQLAlchemy==1.3.23
toml==0.10.2
urllib3==1.26.5
Werkzeug==1.0.1
Comment on lines -1 to -34

Choose a reason for hiding this comment

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

Hmm I'm not sure what happened here, but somehow your list of packages got erased. I would recommend recommitting this file with the original list of packages

python-dotenv==0.21.0
SQLAlchemy==1.4.44
tomli==2.0.1
Werkzeug==2.2.2
Loading