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

Setharika Sok task-list-api #118

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def create_app(test_config=None):

if test_config is None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
"RENDER_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
Expand All @@ -30,5 +30,9 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from .routes import task_bp, goal_bp

app.register_blueprint(task_bp)
app.register_blueprint(goal_bp)

return app
11 changes: 10 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@


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)
tasks = db.relationship("Task", back_populates="goal")

# @classmethod
def goal_dict(self):
return {
'id':self.goal_id,
'title':self.title
}
43 changes: 42 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,45 @@


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)
description = db.Column(db.String)
completed_at = db.Column(db.DateTime, nullable=True)
is_complete = db.Column(db.Boolean, default=False)
goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"))
goal = db.relationship("Goal", back_populates="tasks")

@classmethod
def from_dict(cls, task_data):
new_task = Task(
title = task_data['title'],
description = task_data['description']
)

return new_task

def to_dict(self):
return {
'id':self.task_id,
'title':self.title,
'description':self.description,
'is_complete': True if self.completed_at else False,

Choose a reason for hiding this comment

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

Love the ternary you implemented here!

# 'completed_at':self.completed_at
}

def to_dict_one_task(self):
return {
'task':
self.to_dict()
# {
# 'id':self.task_id,
# 'title':self.title,
# 'description':self.description,
# 'is_complete': True if self.completed_at else False


# # 'completed_at':self.completed_at
# }
}


276 changes: 275 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,275 @@
from flask import Blueprint
from flask import Blueprint, jsonify, abort, make_response, request
from app import db
from app.models.task import Task
from app.models.goal import Goal
from datetime import datetime
import os
import requests

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

def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({"details":"Invalid data"}, 400))

model = cls.query.get(model_id)

if not model:
abort(make_response({"details": f"{cls.__name__} {model_id} is not found"}, 404))

return model

def validate_request(cls, request):
request_body = request.get_json()

if not request_body.get("title"):
abort(make_response({"details":"Invalid data"}, 400))

elif not request_body.get("description") and cls == Task:
Comment on lines +27 to +30

Choose a reason for hiding this comment

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

We could write the conditions for this logic in one line like this:

if not (request_body.get('title') or request_body.get('description')):

Choose a reason for hiding this comment

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

We could also generalize your validate_request function to check for any request body like this:

def validate_request_body(request_body, keys):
    for key in keys:
        if not request_body.get(key): 
            abort(make_response({
                'Invalid Data': f'missing key: {key}'
            }, 400))

    return True

We can pass in the request_body and a list of strings that are keys and then check to see if those keys are present.

abort(make_response({"details":f"Invalid data"}, 400))

return request_body


@task_bp.route("", methods=["POST"])
def create_task():
request_body = validate_request(Task, request)

new_task = Task.from_dict(request_body)

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

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

@task_bp.route("", methods=["GET"])
def read_all_tasks():

sort_query = request.args.get("sort", "asc")
title_query = request.args.get("title")
description_query = request.args.get("description")


if sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc())

elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc())

elif title_query:
tasks = Task.query.filter_by(title = title_query)
Comment on lines +55 to +62

Choose a reason for hiding this comment

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

Nice work on getting the database to do most of the lifting when it comes to sorting the tasks


elif description_query:

Choose a reason for hiding this comment

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

Nice job!

tasks = Task.query.filter_by(description = description_query)

else:
tasks = Task.query.all()

tasks_response = []

for task in tasks:
tasks_response.append(task.to_dict())
return jsonify(tasks_response), 200


@task_bp.route("/<id>", methods=["GET"])
def read_single_task(id):
task = validate_model(Task, id)

goal = Goal.query.get(id)

if goal != None:
task_dict = task.to_dict()
task_dict["goal_id"] = goal.goal_id

else:
task_dict = task.to_dict()

return ({"task":task_dict}),200


@task_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"]
Comment on lines +99 to +100

Choose a reason for hiding this comment

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

Right now, if a user sends a request without the keys title or description your server would crash. There's a couple of ways to handle this, you could call the validate_request function before you access the keys in request_body or you could implement a try/except block.


db.session.commit()

return task.to_dict_one_task(), 200

@task_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


@task_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def mark_incomplete(task_id):
task = validate_model(Task, task_id)

if task.completed_at is not None:
task.completed_at = None

task.is_complete = False
db.session.commit()
return jsonify({"task": task.to_dict()}), 200


@task_bp.route("<task_id>/mark_complete", methods=['PATCH'])
def mark_task_complete_slack(task_id):
task = validate_model(Task, task_id)

if task.completed_at is not None:
return jsonify({"task": task.to_dict()}), 200

task.completed_at = datetime.utcnow()
db.session.commit()

SLACK_BOT_TOKEN = os.environ.get('SLACK_BOT_TOKEN')
SLACK_CHANNEL = '#task-notifications'

#respone notification to Slack

message = f"Someone just completed the task{task.title}"
slack_data = {'text': message, 'channel': SLACK_CHANNEL}
headers = {'Authorization': f'Bearer {SLACK_BOT_TOKEN}'}

try:
response = requests.post('https://slack.com/api/chat.postMessage', json=slack_data, headers=headers)
response.raise_for_status()

except requests.exceptions.RequestException as e:
return jsonify({'message': 'Failed to send Slack notification: {e}'})
Comment on lines +147 to +152

Choose a reason for hiding this comment

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

Nice work with the try/except clause and on having your Slack post sent after the logic of marking a task complete! We don't want to send out any false positive alerts just in case our logic fails during the update!


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

goal_bp = Blueprint("goals", __name__, url_prefix="/goals")
@goal_bp.route("", methods=['POST'])

# creating a goal resource
def create_goal():
request_body = validate_request(Goal, request)

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

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

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

@goal_bp.route("", methods=['GET'])
def read_all_goals():
goals = Goal.query.all()

goals_response = []

for goal in goals:
goals_response.append({ "title": goal.title, "id": goal.goal_id})
Comment on lines +177 to +179

Choose a reason for hiding this comment

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

Do you think that this could be a class method? If so, how would your code look different?


return jsonify(goals_response), 200

@goal_bp.route("/<goal_id>", methods=['GET'])
def read_one_goal(goal_id):

goal = validate_model(Goal, goal_id)

return ({"goal": goal.goal_dict()}), 200

@goal_bp.route("/<goal_id>", methods=['PUT'])
def update_goal(goal_id):
goal = validate_model(Goal, goal_id)

request_body = request.get_json()

goal.title = request_body["title"]
db.session.commit()

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

@goal_bp.route("/<goal_id>", methods=['DELETE'])
def delete_task(goal_id):
goal = validate_model(Goal, goal_id)

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

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


@goal_bp.route("/<goal_id>/tasks", methods=["POST"])

def add_tasks_to_goal(goal_id):
goal = validate_model(Goal, goal_id)

if 'task_ids' not in request.json:
abort(400)

task_ids = request.json['task_ids']

tasks = Task.query.filter(Task.task_id.in_(task_ids)).all()

if len(tasks) != len(task_ids):
abort(400)

goal.task_ids = task_ids

Choose a reason for hiding this comment

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

So here you are making a new attribute on this goal instance that will hold a list of task ids that were last added. This doesn't actually make an association between the two models. Lines 228 and 229 take care of that.


for task in tasks:
task.goal_id = goal_id

db.session.commit()

return jsonify({'id': goal.goal_id, 'task_ids': goal.task_ids})

@goal_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_tasks_for_specific_goal(goal_id):
goal = validate_model(Goal, goal_id)

tasks_response = []

for task in goal.tasks:
task_dict = task.to_dict()
task_dict["goal_id"] = goal.goal_id
tasks_response.append(task_dict)

goal_dict = goal.goal_dict()
goal_dict["tasks"] = tasks_response

return jsonify(goal_dict), 200





Choose a reason for hiding this comment

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

Well done on this project, I didn't have much to comment on and that is a good thing! Keep up the good work! Really looking forward to what you create in the frontend! Please feel free to reach out if you have any questions about the feedback that I left! ✨💫🤭






















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