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

TaskListProject_Cheetahs_Tapasya #118

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
55ba893
wave1: added create-task method and passed test_create_task
takhat Nov 4, 2022
3a99621
Wave 1: added READ routes and passed READ tests
takhat Nov 5, 2022
a985295
Wave 1: added UPDATE route: update_task and passed UPDATE tests
takhat Nov 5, 2022
c6cdcd5
Wave 1 done: added DELETE routes and passed DELETE route tests
takhat Nov 5, 2022
fb8fbb1
Wave 2 complete: added query param: sort, added addl. tests to ignore…
takhat Nov 6, 2022
a1a869c
Wave 3 complete: added PATCH(update) route, passed related tests
takhat Nov 6, 2022
5231e73
wave 4 complete: using an external web api (slack api)
takhat Nov 9, 2022
df459dd
refactored Wave 4: call external apis using responses library instead…
takhat Nov 9, 2022
0ba5e71
minor refactor
takhat Nov 9, 2022
5950819
Wave 5 complete: added Goal Model to define one-to-many relationship …
takhat Nov 9, 2022
b9a5729
Wave 6 complete: added one-to-many relationship between goal and task…
takhat Nov 10, 2022
58bacf6
adding Procfile
takhat Nov 10, 2022
531def4
refactored from_dict method to allow creating task with goal_id added…
takhat Nov 10, 2022
bfb2f85
refactored create_task() to return new_task dict with goal_id attribu…
takhat Nov 10, 2022
6f1bbc1
refactored create_task() to return new_task dict with goal_id attribu…
takhat Nov 10, 2022
a65afe6
undo last 3 commits(refactor)
takhat Nov 10, 2022
f62b53f
add slack_sdk in req
takhat Nov 10, 2022
d172a60
refactored add_task_ids_to_goal
takhat Nov 10, 2022
f1434ac
refactored add_task_ids_to_goal
takhat Nov 10, 2022
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()'
4 changes: 4 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@ def create_app(test_config=None):
migrate.init_app(app, db)

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

from .goal_routes import goals_bp
app.register_blueprint(goals_bp)
return app
99 changes: 99 additions & 0 deletions app/goal_routes.py
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
Comment on lines +18 to +21

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.


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]

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

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

Suggested change

41 changes: 40 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The 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"]
)
57 changes: 56 additions & 1 deletion app/models/task.py
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"]
)










1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

157 changes: 157 additions & 0 deletions app/task_routes.py
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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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"])

Choose a reason for hiding this comment

The 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

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.
Loading