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

Lia Gaetano task-list #75

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8302ea3
Creates task_list_api_development and task_list_api_test db
lgaetano Oct 27, 2021
2fa5202
Creates Task class
lgaetano Oct 27, 2021
3a0f0ca
Moves Task into correct Models folder
lgaetano Oct 27, 2021
3ebaa2c
Updates get_tasks route with to_dict() helper.
lgaetano Oct 28, 2021
aae9d69
Creates post_task endpoint
lgaetano Oct 28, 2021
b2ac6ec
Debugs get & post endpoint and creates put_task endpoint
lgaetano Oct 29, 2021
dd1c8c1
Creates delete_task() endpoint
lgaetano Oct 29, 2021
f24fbd0
Debug tasks/PUT route
lgaetano Oct 29, 2021
5666d17
Added option for query param in task get_tasks() endpoint
lgaetano Oct 29, 2021
4dc8ada
Debug Task to_dict method
lgaetano Oct 29, 2021
0297299
Creates Task PATCH /mark_complete and /mark_incomplete endpoints
lgaetano Oct 29, 2021
d149904
Implements call to Slack API
lgaetano Oct 30, 2021
6ed91ca
Adds require_task decorator, needs debug for None json response
lgaetano Oct 31, 2021
c849296
Refactors and comments get_tasks endpoint
lgaetano Oct 31, 2021
83387e7
Refactored using @require_task_or_404
lgaetano Nov 4, 2021
c7369b0
Adds get_goals method()
lgaetano Nov 4, 2021
695f9c4
Adds update_goal and delete_goal methods
lgaetano Nov 4, 2021
b532840
Debugs goal_delete endpoint
lgaetano Nov 4, 2021
bf6ed51
Debug relationships
lgaetano Nov 5, 2021
d3c1eb2
Refactor Goal to_dict() method
lgaetano Nov 5, 2021
5e22b5a
Debugs Goal.to_dict()
lgaetano Nov 5, 2021
f2e2866
Debugs /goals/1/tasks GET
lgaetano Nov 5, 2021
6f8e8f3
Creates goals_to_dict methods for Task and Goal
lgaetano Nov 5, 2021
b716171
All tests pass
lgaetano Nov 5, 2021
dae4290
Refactor @require_instance_or_404 decorator
lgaetano Nov 5, 2021
6e5c832
OMG my utils module works
lgaetano Nov 5, 2021
b3ac445
Final commit, hopefully
lgaetano Nov 5, 2021
1d2d3ad
Adds Procfile
lgaetano Nov 5, 2021
49ea494
Debug goals/1/tasks POST
lgaetano 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()'
3 changes: 3 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .routes import tasks_bp, goals_bp
app.register_blueprint(tasks_bp)
app.register_blueprint(goals_bp)

return app
25 changes: 24 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,27 @@


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

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

def tasks_to_dict(self):
tasks = [task.goals_to_dict() for task in self.tasks]
return {
"id": self.id,
"title": self.title,
"tasks": tasks
}

def update_from_dict(self, data):

Choose a reason for hiding this comment

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

Neat solution!

# Loops through attributes provided by users
for key, value in data.items():
# Restricts to attributes that are columns
if key in Goal.__table__.columns.keys():
setattr(self, key, value)
36 changes: 35 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,38 @@


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String, nullable=False)
description = db.Column(db.String)
completed_at = db.Column(db.DateTime)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.id'), nullable=True)
goal = db.relationship("Goal", back_populates="tasks")

def check_if_completed(self):
if self.completed_at:
return True
return False

def to_dict(self):
return {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": self.check_if_completed()

Choose a reason for hiding this comment

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

👍

}

def goals_to_dict(self):
return {
"id": self.id,
"goal_id": self.goal_id,
"title": self.title,
"description": self.description,
"is_complete": self.check_if_completed()
}

def update_from_dict(self, data):
# Loops through attributes provided by user
for key, value in data.items():
# Restricts to attributes that are table columns
if key in Task.__table__.columns.keys():
setattr(self, key, value)
193 changes: 192 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,193 @@
from flask import Blueprint
from app import db
from flask import Blueprint, request, abort, jsonify, make_response
from datetime import datetime
from dotenv import load_dotenv
import os
import requests
from app.models.task import Task
from app.models.goal import Goal
from app.utils.route_wrappers import require_instance_or_404

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

@tasks_bp.route("", methods=["GET"])
def get_tasks():
"""
Retrieve all tasks. Allows for use of query parameters.
Returns JSON list of task dictionaries. """
query = Task.query # Base query

# Query params, adding to query where indicated
sort = request.args.get("sort")
if sort == "asc":
query = query.order_by(Task.title)
elif sort == "desc":
query = query.order_by(Task.title.desc())

query = query.all() # Final query

# Returns jsonified list of task dicionaries
return jsonify([task.to_dict() for task in query]), 200

@tasks_bp.route("/<task_id>", methods=["GET"])
@require_instance_or_404
def get_task(task):
"""Retrieve one stored task by id."""
if task.goal_id:

Choose a reason for hiding this comment

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

This works but to simplify the code I would recommend pushing this check to the to_dict method and add the goal_id key-value pair if goal_id exists inside the to_dict function.

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

@tasks_bp.route("", methods=["POST"])
def post_task():
"""Create a new task from JSON data."""
form_data = request.get_json()

# All fields must be provided
mandatory_fields = ["title", "description", "completed_at"]
for field in mandatory_fields:
if field not in form_data:
return jsonify({"details": "Invalid data"}), 400
Comment on lines +48 to +51

Choose a reason for hiding this comment

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

Nice!


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

db.session.add(new_task)
db.session.commit()
return {"task": new_task.to_dict()}, 201

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

Choose a reason for hiding this comment

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

Nice work with this decorator! Setting up task as the parameter in the function makes the code in the function very streamlined!

def put_task(task):
"""Updates task by id."""
form_data = request.get_json()

# Updates object from form data
task.update_from_dict(form_data)
db.session.commit()

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

@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
@require_instance_or_404
def update_task_to_complete(task):
"""Updates task at particular id to completed using PATCH."""
# Make call to Slack API if task newly completed
if not task.check_if_completed():
slack_api_url = "https://slack.com/api/chat.postMessage"
headers = {"Authorization": "Bearer " + os.environ.get("SLACK_API_KEY")}
param_payload = {
"channel": "task-notifications",
"text": f"Someone has just completed the task {task.title}"
}

try:
requests.post(slack_api_url, headers=headers, params=param_payload)

except Exception as e:
return f"Error posting message to Slack: {e}"

# Change task to completed in db
task.completed_at = datetime.now()
db.session.commit()

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

@tasks_bp.route("<task_id>/mark_incomplete", methods=["PATCH"])
@require_instance_or_404
def update_task_to_incomplete(task):
"""Updates task at particular id to incomplete using PATCH."""
task.completed_at = None
db.session.commit()
return {"task": task.to_dict()}, 200

@tasks_bp.route("/<task_id>", methods=["DELETE"])
@require_instance_or_404
def delete_task(task):
"""Deletes task by id."""
db.session.delete(task)
db.session.commit()

return {
"details": f"Task {task.id} \"{task.title}\" successfully deleted"
}, 200

@goals_bp.route("", methods=["GET"])
def get_goals():
"""Retrieve all stored goals."""
goals = Goal.query.all()

return jsonify([goal.to_dict() for goal in goals]), 200

@goals_bp.route("/<goal_id>", methods=["GET"])
@require_instance_or_404
def get_goal(goal):
"""Retrieve one stored goal by id."""
return jsonify({"goal": goal.to_dict()}), 200

@goals_bp.route("", methods=["POST"])
def create_goal():
"""Create a new goal from JSON data."""
form_data = request.get_json()

if "title" not in form_data:
return jsonify({"details": "Invalid data"}), 400

new_goal = Goal(
title=form_data["title"]
)
db.session.add(new_goal)
db.session.commit()

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

@goals_bp.route("/<goal_id>", methods=["PUT"])
@require_instance_or_404
def update_goal(goal):
"""Updates goal by id."""
form_data = request.get_json()

goal.update_from_dict(form_data)
db.session.commit()

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

@goals_bp.route("/<goal_id>", methods=["DELETE"])
@require_instance_or_404
def delete_goal(goal):
"""Deletes goal by id."""
db.session.delete(goal)
db.session.commit()

return {
"details": f"Goal {goal.id} \"{goal.title}\" successfully deleted"
}, 200

@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
@require_instance_or_404
def post_tasks_related_to_goal(goal):
"""Adds tasks to goal wiht id."""
form_data = request.get_json()

for task_id in form_data["task_ids"]:
query = Task.query.get(task_id)
if not query:

Choose a reason for hiding this comment

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

Very minor suggestion, query is a little confusing here as a variable name as the value being stored is a task not a query, I'd recommend something like task or current_task.

continue
goal.tasks.append(query)

db.session.commit()

return jsonify({
"id": goal.id,
"task_ids": [task.id for task in goal.tasks]
}), 200

@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
@require_instance_or_404
def get_tasks_related_to_goal(goal):
"""Retrieves all tasks associated with goal id."""
return jsonify(goal.tasks_to_dict()), 200
Empty file added app/utils/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions app/utils/route_wrappers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from functools import wraps
from flask import jsonify
from app.models.task import Task
from app.models.goal import Goal

def require_instance_or_404(endpoint):

Choose a reason for hiding this comment

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

💯 Fantastic!

"""
Decorator to validate that a requested id of input data exists.
Returns JSON and 404 if not found."""
@wraps(endpoint) # Makes fn look like func to return
def fn(*args, **kwargs):
if "task_id" in kwargs:
task_id = kwargs.get("task_id", None)
task = Task.query.get(task_id)

if not task:
return jsonify(None), 404 # null

kwargs.pop("task_id")
return endpoint(*args, task=task, **kwargs)

elif "goal_id" in kwargs:
goal_id = kwargs.get("goal_id", None)
goal = Goal.query.get(goal_id)

if not goal:
return jsonify(None), 404

kwargs.pop("goal_id")
return endpoint(*args, goal=goal, **kwargs)

return fn
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