-
Notifications
You must be signed in to change notification settings - Fork 146
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
TaskListProject_Cheetahs_Tapasya #118
base: master
Are you sure you want to change the base?
Changes from all commits
55ba893
3a99621
a985295
c6cdcd5
fb8fbb1
a1a869c
5231e73
df459dd
0ba5e71
5950819
b9a5729
58bacf6
531def4
bfb2f85
6f1bbc1
a65afe6
f62b53f
d172a60
f1434ac
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 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,99 @@ | ||||||||||||||
|
||||||||||||||
from datetime import datetime | ||||||||||||||
from flask import Blueprint, jsonify, make_response, request, abort | ||||||||||||||
from app import db | ||||||||||||||
from app.models.task import Task | ||||||||||||||
from app.models.goal import Goal | ||||||||||||||
from sqlalchemy import desc #(used in read_all_tasks for sorting) | ||||||||||||||
import os | ||||||||||||||
from .task_routes import validate_model | ||||||||||||||
|
||||||||||||||
goals_bp = Blueprint("goal", __name__, url_prefix="/goals") | ||||||||||||||
|
||||||||||||||
#CREATE Routes (Wave 5: CRUD Routes for goal model) | ||||||||||||||
|
||||||||||||||
@goals_bp.route('', methods=['POST']) | ||||||||||||||
def create_goal(): | ||||||||||||||
request_body = request.get_json() | ||||||||||||||
try: | ||||||||||||||
new_goal = Goal.from_dict(request_body) | ||||||||||||||
except KeyError: | ||||||||||||||
return jsonify ({"details": "Invalid data"}), 400 | ||||||||||||||
|
||||||||||||||
db.session.add(new_goal) | ||||||||||||||
db.session.commit() | ||||||||||||||
|
||||||||||||||
return jsonify({"goal": new_goal.to_dict()}), 201 | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
@goals_bp.route('', methods=["GET"]) | ||||||||||||||
def get_all_goals(): | ||||||||||||||
all_goals = Goal.query.all() | ||||||||||||||
|
||||||||||||||
result = [item.to_dict() for item in all_goals] | ||||||||||||||
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 use of a list comprehension & helper method. |
||||||||||||||
|
||||||||||||||
return jsonify(result), 200 | ||||||||||||||
|
||||||||||||||
@goals_bp.route("<goal_id>", methods=["GET"]) | ||||||||||||||
def read_goal_by_id(goal_id): | ||||||||||||||
chosen_goal = validate_model(Goal, goal_id) | ||||||||||||||
|
||||||||||||||
return jsonify({"goal": chosen_goal.to_dict()}), 200 | ||||||||||||||
|
||||||||||||||
@goals_bp.route('/<goal_id>', methods=['PUT']) | ||||||||||||||
def update_one_goal(goal_id): | ||||||||||||||
update_goal = validate_model(Goal, goal_id) | ||||||||||||||
|
||||||||||||||
request_body = request.get_json() | ||||||||||||||
|
||||||||||||||
try: | ||||||||||||||
update_goal.title = request_body["title"] | ||||||||||||||
except KeyError: | ||||||||||||||
return jsonify({"msg": "Missing needed data"}), 400 | ||||||||||||||
Comment on lines
+49
to
+52
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. Another similar try-except block. |
||||||||||||||
|
||||||||||||||
db.session.commit() | ||||||||||||||
return jsonify({"msg": f"Successfully updated goal with id {update_goal.goal_id}"}), 200 | ||||||||||||||
|
||||||||||||||
@goals_bp.route('/<goal_id>', methods=['DELETE']) | ||||||||||||||
def delete_one_goal(goal_id): | ||||||||||||||
goal_to_delete = validate_model(Goal, goal_id) | ||||||||||||||
|
||||||||||||||
db.session.delete(goal_to_delete) | ||||||||||||||
db.session.commit() | ||||||||||||||
|
||||||||||||||
return jsonify({"details": f'Goal {goal_to_delete.goal_id} "{goal_to_delete.title}" successfully deleted'}), 200 | ||||||||||||||
|
||||||||||||||
#Wave 6: Nested Routes | ||||||||||||||
@goals_bp.route('/<goal_id>/tasks', methods=['POST']) | ||||||||||||||
def add_task_ids_to_goal(goal_id): | ||||||||||||||
chosen_goal = validate_model(Goal, goal_id) | ||||||||||||||
|
||||||||||||||
request_body = request.get_json() | ||||||||||||||
task_ids = request_body["task_ids"] | ||||||||||||||
|
||||||||||||||
for id in task_ids: | ||||||||||||||
#task = Task.query.get(int(id)) | ||||||||||||||
task = validate_model(Task, id) | ||||||||||||||
if task not in chosen_goal.tasks: | ||||||||||||||
chosen_goal.tasks.append(task) | ||||||||||||||
#db.session.add(task) | ||||||||||||||
db.session.commit() | ||||||||||||||
|
||||||||||||||
return jsonify({ | ||||||||||||||
"id" : int(goal_id), | ||||||||||||||
"task_ids": task_ids | ||||||||||||||
}), 200 | ||||||||||||||
|
||||||||||||||
@goals_bp.route('/<goal_id>/tasks', methods=['GET']) | ||||||||||||||
def get_tasks_by_goal_id(goal_id): | ||||||||||||||
chosen_goal = validate_model(Goal, goal_id) | ||||||||||||||
|
||||||||||||||
return jsonify(chosen_goal.to_dict_incl_tasks()), 200 | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Comment on lines
+93
to
+99
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.
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,43 @@ | |
|
||
|
||
class Goal(db.Model): | ||
goal_id = db.Column(db.Integer, primary_key=True) | ||
goal_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.goal_id, | ||
"title": self.title | ||
} | ||
|
||
def to_dict_incl_tasks(self): | ||
tasks = self.get_task_items() | ||
|
||
return { | ||
"id": self.goal_id, | ||
"title": self.title, | ||
"tasks": tasks | ||
} | ||
Comment on lines
+14
to
+27
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. These seem redundant |
||
|
||
#Helper method to use in to_dict_incl_tasks() | ||
def get_task_items(self): | ||
if self.tasks is None: | ||
return None | ||
list_of_tasks = [item.to_dict_incl_goal_id() for item in self.tasks] | ||
return list_of_tasks | ||
|
||
@classmethod | ||
def from_dict(cls, dict): | ||
return cls ( | ||
title = dict["title"], | ||
) if len(dict) == 1 else cls ( | ||
title = dict["title"], | ||
description = dict["description"], | ||
tasks = dict["tasks"] | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,60 @@ | ||
from app import db | ||
|
||
#Wave 1: CRUD for One Model | ||
|
||
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) | ||
description = db.Column(db.String) | ||
completed_at = db.Column(db.DateTime, | ||
nullable = True, | ||
default = None) | ||
goal_id = db.Column( | ||
db.Integer, | ||
db.ForeignKey("goal.goal_id"), | ||
nullable = True) | ||
goal = db.relationship( | ||
"Goal", | ||
back_populates='tasks') | ||
|
||
def to_dict(self): | ||
return { | ||
"id": self.id, | ||
"title": self.title, | ||
"description": self.description, | ||
"is_complete": False if self.completed_at is None else True | ||
} | ||
# Source: | ||
# https://stackoverflow.com/questions/52325025/use-of-if-else-inside-a-dict-to-set-a-value-to-key-using-python | ||
|
||
def to_dict_incl_goal_id(self): | ||
return{ | ||
"id": self.id, | ||
"goal_id": self.goal_id, | ||
"title": self.title, | ||
"description": self.description, | ||
"is_complete": False if self.completed_at is None else True | ||
} | ||
@classmethod | ||
def from_dict(cls, dict): | ||
return cls ( | ||
title = dict["title"], | ||
description = dict["description"] | ||
) if len(dict) == 2 else cls ( | ||
title = dict["title"], | ||
description = dict["description"], | ||
completed_at = dict["completed_at"] | ||
) | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
from datetime import datetime | ||
from flask import Blueprint, jsonify, make_response, request, abort | ||
from app import db | ||
from app.models.task import Task | ||
from app.models.goal import Goal | ||
from sqlalchemy import desc #(used in read_all_tasks for sorting) | ||
import requests | ||
import os | ||
# from slackclient import SlackClient #Alternate method 3 (below to call external api) | ||
|
||
SLACK_TOKEN = os.environ.get('SLACK_TOKEN', None) | ||
# slack_client = SlackClient(SLACK_TOKEN) #Alternate method 3 | ||
|
||
|
||
tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") | ||
|
||
#CREATE Routes (Wave 1: CRUD Routes) | ||
@tasks_bp.route("", methods=["POST"]) | ||
def create_task(): | ||
request_body = request.get_json() | ||
|
||
try: | ||
new_task = Task.from_dict(request_body) | ||
except KeyError: | ||
return jsonify ({"details": "Invalid data"}), 400 | ||
Comment on lines
+22
to
+25
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 use of a try-except, but it would be good to include details about what data is invalid for the user. |
||
|
||
db.session.add(new_task) | ||
db.session.commit() | ||
|
||
return jsonify({"task": new_task.to_dict()}), 201 | ||
|
||
#READ Routes (Wave 1: CRUD routes) | ||
@tasks_bp.route("", methods=["GET"]) | ||
def read_all_tasks(): | ||
#(Wave 2: Query param: sort) | ||
if "sort"in request.args or "SORT" in request.args: | ||
sort_query_val = request.args.get("sort") if "sort"in request.args else \ | ||
request.args.get("SORT") | ||
|
||
if sort_query_val.lower() == "asc": | ||
tasks = Task.query.order_by(Task.title).all() | ||
Comment on lines
+36
to
+41
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 query params. |
||
|
||
elif sort_query_val.lower() == "desc": | ||
tasks = Task.query.order_by(Task.title.desc()).all() | ||
# Source: https://stackoverflow.com/questions/4186062/sqlalchemy-order-by-descending | ||
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 documentation! |
||
|
||
else: | ||
return jsonify({"msg": f"Invalid query value: {sort_query_val}"}), 400 | ||
|
||
else: | ||
tasks = Task.query.all() | ||
|
||
tasks_response = [task.to_dict() for task in tasks] | ||
|
||
# for task in tasks: | ||
# tasks_response.append(task.to_dict()) | ||
|
||
return jsonify(tasks_response), 200 | ||
|
||
@tasks_bp.route("<task_id>", methods=["GET"]) | ||
def read_task_by_id(task_id): | ||
task = validate_model(Task, task_id) | ||
if task.goal_id is None: | ||
return jsonify({"task":task.to_dict()}), 200 | ||
return jsonify({"task":task.to_dict_incl_goal_id()}), 200 | ||
|
||
#Helper function for use in READ route: read_task_by_id and UPDATE route: update_task | ||
def validate_model(cls, id): | ||
try: | ||
model_id = int(id) | ||
except: | ||
abort(make_response(jsonify({"msg":f"invalid id: {model_id}"}), 400)) | ||
|
||
chosen_object = cls.query.get(model_id) | ||
|
||
if not chosen_object: | ||
abort(make_response(jsonify({"msg": f"No {cls.__name__.lower()} found with given id: {model_id}"}), 404)) | ||
|
||
return chosen_object | ||
Comment on lines
+61
to
+79
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. Great helper functions |
||
|
||
#UPDATE Routes (Wave 1: CRUD Routes) | ||
@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"] | ||
|
||
if len(request_body) > 2 and request_body["completed_at"]: | ||
task.completed_at = request_body["completed_at"] | ||
|
||
db.session.commit() | ||
|
||
return jsonify({"task": { | ||
"id": task.id, | ||
"title": task.title, | ||
"description": task.description, | ||
"is_complete": False if task.completed_at is None else True | ||
}}), 200 | ||
|
||
#UPDATE Routes (Wave 3: PATCH Routes) | ||
@tasks_bp.route("/<task_id>/<mark>", methods=["PATCH"]) | ||
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. I like how you worked the "mark" into the route. |
||
def update_task_mark_complete_or_incomplete(task_id, mark): | ||
task = validate_model(Task, task_id) | ||
|
||
if mark == "mark_complete": | ||
task.completed_at = datetime.utcnow().date() | ||
#Source: https://stackoverflow.com/questions/27587127/how-to-convert-datetime-date-today-to-utc-time | ||
|
||
|
||
channel = "text-notifications" | ||
message = f"Someone just completed the task {task.title}" | ||
access_token = SLACK_TOKEN | ||
my_headers = {'Authorization' : f'Bearer {access_token}'} | ||
|
||
#Method 1: | ||
response = requests.post(f'https://slack.com/api/chat.postMessage', | ||
data={"channel": channel,"text": message}, | ||
headers=my_headers,json=True) | ||
|
||
#Alternate Method 2: | ||
#response = requests.post(f'https://slack.com/api/chat.postMessage?channel={channel}&text={message}&pretty=1', headers=my_headers) | ||
#Source: https://www.nylas.com/blog/use-python-requests-module-rest-apis/ | ||
|
||
#Alternate Method 3: | ||
# channel_id = "C04A2GQ53NF" | ||
# send_message(channel_id=channel_id, message=message) | ||
#Source: https://realpython.com/getting-started-with-the-slack-api-using-python-and-flask/ | ||
|
||
elif mark == "mark_incomplete": | ||
task.completed_at = None | ||
|
||
db.session.commit() | ||
|
||
return jsonify({"task":task.to_dict()}), 200 | ||
|
||
# DELETE Routes (Wave 1: CRUD Routes) | ||
@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 jsonify({"details": f'Task {task_id} "{task.title}" successfully deleted'}), 200 | ||
|
||
#Helper Function to call slack api (Alternate Method 3) | ||
# def send_message(channel_id, message): | ||
# slack_client.api_call( | ||
# "chat.postMessage", | ||
# channel=channel_id, | ||
# text=message, | ||
# ) | ||
#Source: https://api.slack.com/methods/chat.postMessage/code | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Generic single-database configuration. |
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.
Nice work with try-except, but it would be good to provide the user information about what data is invalid.