-
Notifications
You must be signed in to change notification settings - Fork 97
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
base: master
Are you sure you want to change the base?
Changes from all commits
8302ea3
2fa5202
3a0f0ca
3ebaa2c
aae9d69
b2ac6ec
dd1c8c1
f24fbd0
5666d17
4dc8ada
0297299
d149904
6ed91ca
c849296
83387e7
c7369b0
695f9c4
b532840
bf6ed51
d3c1eb2
5e22b5a
f2e2866
6f8e8f3
b716171
dae4290
6e5c832
b3ac445
1d2d3ad
49ea494
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
web: gunicorn 'app:create_app()' |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice work with this decorator! Setting up |
||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very minor suggestion, |
||
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 |
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Generic single-database configuration. |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat solution!