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 - Jessica - Task List #74

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ed23c08
Add migrations folder to tracking
Jessicawyn Oct 28, 2021
425ae71
Add Task model and table
Jessicawyn Oct 28, 2021
11fd4e7
Create and register task bp
Jessicawyn Oct 28, 2021
c3b6c8e
Add to_dict method for Task class
Jessicawyn Oct 28, 2021
a5c7820
Add create_task route
Jessicawyn Oct 28, 2021
387f540
Add read all tasks route
Jessicawyn Oct 28, 2021
6dd7de0
Add check for title and description for POST
Jessicawyn Oct 28, 2021
709f945
Add is_complete to Task model and table
Jessicawyn Oct 28, 2021
787f870
Add nullable constraints to Task class
Jessicawyn Oct 29, 2021
f3001eb
Add code to pass create task test cases
Jessicawyn Oct 29, 2021
69c07ca
Add code to pass Get one task test
Jessicawyn Oct 29, 2021
1c00ed9
Add read_one_task route
Jessicawyn Oct 29, 2021
e33e799
Add update_task route
Jessicawyn Oct 29, 2021
ebf73ea
Refactor to_dict method to use complete
Jessicawyn Oct 29, 2021
21786bd
Add Delete Route
Jessicawyn Oct 29, 2021
4f826f4
Add sorting by title to pass wave 2
Jessicawyn Oct 30, 2021
5b2e592
Add Mark Complete Route
Jessicawyn Oct 30, 2021
c5cbd1d
Modify create task to account for completed_at
Jessicawyn Oct 31, 2021
b2ba54a
Add mark task incomplete route
Jessicawyn Nov 1, 2021
cbb8dd8
Add slack messaging to mark complete route
Jessicawyn Nov 2, 2021
2344341
Add Goal Model
Jessicawyn Nov 2, 2021
13765c5
Refactor folder to put routes file into routes folder and create task…
Jessicawyn Nov 2, 2021
3fe2824
Add and register Goal blueprint
Jessicawyn Nov 2, 2021
aedee15
Fix import of Goal class in goal_routes
Jessicawyn Nov 2, 2021
538cfdb
Add get all goals route
Jessicawyn Nov 2, 2021
d6a1116
Add get one goal route
Jessicawyn Nov 2, 2021
e5c9ea3
Add delete goal route
Jessicawyn Nov 2, 2021
254f2b5
Add PUT goal route
Jessicawyn Nov 2, 2021
715c9ef
Fix typo in PUT goal route
Jessicawyn Nov 2, 2021
77f5dc6
Cleanup line spaces
Jessicawyn Nov 3, 2021
bed7ae4
Complete tests for wave 5
Jessicawyn Nov 3, 2021
984bbd1
Add utils file for shared valid input function
Jessicawyn Nov 3, 2021
7dfadb0
Add foreign key relationship for task to goals
Jessicawyn Nov 4, 2021
763f162
Add link task to goal route
Jessicawyn Nov 4, 2021
3b29755
Add read tasks for goal and refactor to_dict functions to account for…
Jessicawyn Nov 4, 2021
c4adf6f
Cleanup working comments
Jessicawyn Nov 4, 2021
dc4813a
Add migrations versions folder
Jessicawyn Nov 5, 2021
4aca36c
Add Procfile
Jessicawyn Nov 5, 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
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()'
5 changes: 5 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,10 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .routes.task_routes import task_bp
app.register_blueprint(task_bp)

from .routes.goal_routes import goal_bp
app.register_blueprint(goal_bp)

return app
26 changes: 25 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
from re import T

Choose a reason for hiding this comment

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

You're the second person I've seen import this but I'm not sure what effect it actually has (it doesn't seem explicitly referenced).

I'm curious, why the import?

from flask import current_app
from flask.helpers import make_response
from sqlalchemy.orm import backref, lazyload
from app import db


class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
goal_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String, nullable=False)
tasks = db.relationship("Task", backref="goal", lazy=True)

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

def create_task_list(self):
task_list = []
for task in self.tasks:
task_list.append(task.to_dict())
return task_list

def to_dict_with_tasks(self):
return {
"id": self.goal_id,
"title": self.title,
"tasks": [task.to_dict() for task in self.tasks]
}
27 changes: 26 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
from flask import current_app
from sqlalchemy.orm import lazyload
from app import db
from app.models.goal import Goal


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, nullable=False)
description = db.Column(db.String, nullable=False)
completed_at = db.Column(db.DateTime, nullable=True)
goal_id = db.Column(db.Integer, db.ForeignKey(Goal.goal_id), nullable=True)

def to_dict(self):
complete = False
if self.completed_at:
complete = True
if self.goal_id:
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": complete,
"goal_id": self.goal_id
}
return {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": complete
}
2 changes: 0 additions & 2 deletions app/routes.py

This file was deleted.

95 changes: 95 additions & 0 deletions app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from flask import Blueprint, jsonify, make_response, request, abort
from flask.helpers import make_response
from flask.json import tojson_filter
from flask.signals import request_tearing_down
from werkzeug.utils import header_property
from app.models.goal import Goal
from app import db
from datetime import datetime
from app.routes.utils import valid_int
from app.models.task import Task


goal_bp = Blueprint("goal", __name__,url_prefix ="/goals")

# Helper Functions
def get_goal_from_id(goal_id):
valid_int(goal_id, "goal_id")
return Goal.query.get_or_404(goal_id, description="{goal not found}")
Comment on lines +16 to +18

Choose a reason for hiding this comment

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

Since get_or_404 returns HTML and not JSON by default you might want like this instead:

Suggested change
def get_goal_from_id(goal_id):
valid_int(goal_id, "goal_id")
return Goal.query.get_or_404(goal_id, description="{goal not found}")
def get_goal_from_id(goal_id):
valid_int(goal_id, "goal_id")
goal = Goal.query.get(goal_id)
if goal:
return goal
else:
abort(make_response({"description": "goal not found"}, 404))



# Routes
@goal_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
if "title" not in request_body:
return make_response({"details": "Invalid data"}, 400)

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

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

return make_response({"goal": new_goal.to_dict()}, 201)

@goal_bp.route("", methods=["GET"])
def read_all_goals():

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

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

goal_response = []
for goal in goals:
goal_response.append(
goal.to_dict()
)
return make_response(jsonify(goal_response), 200)

@goal_bp.route("/<goal_id>", methods=["GET"])
def read_one_goal(goal_id):
goal = get_goal_from_id(goal_id)
return make_response({"goal": goal.to_dict()}, 200)

@goal_bp.route("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):
goal = get_goal_from_id(goal_id)
request_body = request.get_json()
goal.title=request_body["title"]
db.session.commit()
return make_response({"goal": goal.to_dict()}, 200)

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

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

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

Choose a reason for hiding this comment

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

This is a really good message! Very clear. 😄


@goal_bp.route("/<goal_id>/tasks", methods=["POST"])
def link_tasks_to_goals(goal_id):
request_body = request.get_json()
task_ids = request_body["task_ids"]
for t_id in task_ids:
task = Task.query.get(t_id)
task.goal_id = goal_id
db.session.commit()
response_body = {
"id": int(goal_id),

Choose a reason for hiding this comment

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

Remember, if goal_id is invalid this will cause an exception.

It may be worth using your valid_int helper here.

"task_ids": task_ids
}
return make_response(response_body, 200)

@goal_bp.route("/<goal_id>/tasks", methods=["GET"])
def read_tasks_for_goal(goal_id):
goal = get_goal_from_id(goal_id)
return make_response(goal.to_dict_with_tasks(), 200)
113 changes: 113 additions & 0 deletions app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from flask import Blueprint, jsonify, make_response, request, abort
from flask.helpers import make_response
from flask.json import tojson_filter
from flask.signals import request_tearing_down
from werkzeug.utils import header_property
from app.models.task import Task
from app import db
from datetime import datetime
from dotenv import load_dotenv
import requests, os
from app.routes.utils import valid_int

load_dotenv()

task_bp = Blueprint("task", __name__,url_prefix ="/tasks")

# Helper Functions
def get_task_from_id(task_id):
valid_int(task_id, "task_id")
return Task.query.get_or_404(task_id, description="{task not found}")

def post_slack_message(message):
token = os.environ.get('SLACK_TOKEN')
CHANNEL_ID = "C02KD4B5A07"

Headers = {"Authorization": "Bearer xoxb-" + token}

Choose a reason for hiding this comment

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

This line causes a failure if the SLACK_TOKEN doesn't exist in the environment (which is why the automated tests fail).

It's perfectly reasonable to assume you will have a key (and the tests pass if given the token of token so would work if the token was expired) this was just a quirk of our Learn setup.

If you wanted to fix the test failures you could use interpolation to do this instead:

Suggested change
Headers = {"Authorization": "Bearer xoxb-" + token}
Headers = {"Authorization": f"Bearer xoxb-{token}"}

Again, this isn't a bug, just a quirk of our Learn setup.

It's just the kind of thing where I would want to know why it failed on Learn and not on my local machine.

data = {
"channel": CHANNEL_ID,
"text": message
}
response = requests.post("https://slack.com/api/chat.postMessage", headers=Headers, json=data)
return response


# Routes
@task_bp.route("", methods=["POST"])
def create_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)

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

db.session.add(new_task)
db.session.commit()

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

@task_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())
elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc())
else:
tasks = Task.query.all()

task_response = []
for task in tasks:
task_response.append(
task.to_dict()
)
return make_response(jsonify(task_response), 200)

@task_bp.route("/<task_id>", methods=["GET"])
def read_one_task(task_id):
task = get_task_from_id(task_id)
return make_response({"task": task.to_dict()}, 200)

@task_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):
task = get_task_from_id(task_id)
request_body = request.get_json()
task.title=request_body["title"]
task.description=request_body["description"]
db.session.commit()
return make_response({"task": task.to_dict()}, 200)

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

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

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

@task_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def mark_task_complete(task_id):
task = get_task_from_id(task_id)
task.completed_at = datetime.utcnow()

db.session.commit()
message = f"Someone just completed the task {task.title}"
post_slack_message(message)

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

@task_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def mark_task_incomplete(task_id):
task = get_task_from_id(task_id)
task.completed_at = None
db.session.commit()

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

7 changes: 7 additions & 0 deletions app/routes/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from flask import make_response, abort

def valid_int(number, parameter_type):
try:
int(number)
except:
abort(make_response({"error": f"{parameter_type} must be an int"}, 400))
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