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

C19 Kunzite Shay, Amber #110

Open
wants to merge 29 commits into
base: main
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
b597d9a
initial setup
Maashad May 4, 2023
5376fef
create task model
Maashad May 5, 2023
2d9f87a
validate model/task in .routes
Maashad May 9, 2023
11d7486
successful simple get with postman
Maashad May 9, 2023
3de4377
successfully create new task
Maashad May 9, 2023
b4b15eb
successfully read all saved tasks
Maashad May 9, 2023
add8427
update
Maashad May 10, 2023
78c3334
dictionary response for task creation
Maashad May 10, 2023
e39e3f2
create task returns task dictionary
Maashad May 10, 2023
4b2410d
get all tasks and get one task complete
Maashad May 10, 2023
6dde8eb
pass 6 of 11 tests
Maashad May 10, 2023
f134c99
wave 1 complete, all tests pass
Maashad May 10, 2023
0ff118e
wave 2 complete, all tests pass
Maashad May 10, 2023
9a0da93
gah
Maashad May 11, 2023
464b153
Merge branches 'main' and 'main' of https://github.com/Maashad/task-l…
Maashad May 11, 2023
1cb93c7
wave 1 tests passing again
Maashad May 11, 2023
d82308a
fixed w1 and w2 test issues
Maashad May 11, 2023
b926255
started wave 3
Maashad May 11, 2023
e47e8e0
wave 3 complete with passing tests
Maashad May 11, 2023
f265d54
creating slack bot
Maashad May 11, 2023
71bc18e
install slack-sdk dependencies
Maashad May 11, 2023
fb965f5
wave 4 complete, all tests pass
Maashad May 11, 2023
147d77f
working bot post to slack
Maashad May 12, 2023
834ce13
w5 tests 1-5 pass
Maashad May 12, 2023
b1900cd
wave5 complete, all tests pass
Maashad May 12, 2023
8e7ac87
connect goal and task models as relationships
Maashad May 12, 2023
c083977
move goal routes to goal_routes.py
Maashad May 12, 2023
6c7b13f
wave 6, 1 test passing
Maashad May 12, 2023
ff7f05e
first submission
Maashad May 12, 2023
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
9 changes: 7 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv
import os


db = SQLAlchemy()
Expand All @@ -29,6 +29,11 @@ def create_app(test_config=None):
db.init_app(app)
migrate.init_app(app, db)

# Register Blueprints here
# Register Blueprints
from .routes import task_list_bp
app.register_blueprint(task_list_bp)

from .goal_routes import goal_bp
app.register_blueprint(goal_bp)

return app
124 changes: 124 additions & 0 deletions app/goal_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from app import db
from app.models.task import Task
from app.models.goal import Goal
from flask import Blueprint, jsonify, make_response, request, abort
from sqlalchemy.types import DateTime
from sqlalchemy.sql.functions import now
Comment on lines +5 to +6

Choose a reason for hiding this comment

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

We should kept sqlalchemy model functionality and import in the class files themselves, so let's great rid of these.

Suggested change
from sqlalchemy.types import DateTime
from sqlalchemy.sql.functions import now

import requests, json
import os

goal_bp = Blueprint("goal_bp", __name__, url_prefix="/goals")

def validate_model_goal(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({"message": f"{cls.__name__} {model_id} invalid"}, 400))

goal = cls.query.get(model_id)

if not goal:
abort(make_response({"message": f"{cls.__name__} {model_id} not found"}, 404))

return goal
Comment on lines +12 to +23

Choose a reason for hiding this comment

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

This is identical to the model validation function for tasks as well. We should consider defining this once, naming it something more generic, and using it for both models.


@goal_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
is_valid_goal = "title" in request_body
if not is_valid_goal:
Comment on lines +28 to +29

Choose a reason for hiding this comment

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

This works, but it is more Pythonic to combine this:

    if "title" not in request_body:

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

Choose a reason for hiding this comment

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

How could we make this error message more descriptive for the user to understand what is missing? Maybe we can say that title is missing or something!


new_goal = Goal.goal_from_dict(request_body)

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

response_body = {
"goal": new_goal.goal_to_dict()
}

return make_response(jsonify(response_body), 201)

@goal_bp.route("", methods=["GET"])
def get_all_goals():
# goal = validate_model_goal()
sort_query = request.args.get("sort")
if sort_query == "asc":
goals = Goal.query.order_by(Goal.title)
elif sort_query == "desc":
goals = Goal.query.order_by(Goal.title.desc())
else:
goals = Goal.query.all()

goals_response = [goal.goal_to_dict() for goal in goals]
return jsonify(goals_response)

Choose a reason for hiding this comment

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

Oops, don't forget your status code! Technically, it will add one by default, but consider being explicit and always adding the status code.


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

Choose a reason for hiding this comment

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

👍

def get_one_goal(goal_id):
goal = validate_model_goal(Goal, goal_id)
response_body = {
"goal": goal.goal_to_dict()
}
return jsonify(response_body)

@goal_bp.route("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):
goal = validate_model_goal(Goal, goal_id)
request_body = request.get_json()
goal.title = request_body["title"]

db.session.commit()

response_body = {
"goal": goal.goal_to_dict()
}
return response_body

Choose a reason for hiding this comment

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

Careful here! While this does work, it is very different form the previous routes you've done. Try to keep them as consistent as possible. If you used jsonfy in a previous route response, do it for all of them (with the few exceptions that there may be)


@goal_bp.route("/<goal_id>", methods=["DELETE"])
def delete_goal(goal_id):
goal = validate_model_goal(Goal, goal_id)
response_body = {
"details": f'Goal {goal.goal_id} \"{goal.title}\" successfully deleted'

Choose a reason for hiding this comment

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

Nice! We can also do this:

        {"details": f'Goal {goal_id} "{goal.title}" successfully deleted'}

}

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

return response_body

@goal_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_task(goal_id):
# validate goal_id
goal = validate_model_goal(Goal, goal_id)

request_body = request.get_json()

db.session.add(request_body)
db.session.commit()

tasks_resp = [tasks_resp for task in ask.task_with_goal_to_dict]
goal.goal_to_dict_with_task() // t

Choose a reason for hiding this comment

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

Hm, what are you dividing by here? What is t in reference to, or is this maybe a mistype?


return jsonify(response_body)

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

goal = validate_model_goal(Goal, goal_id)
Comment on lines +106 to +108

Choose a reason for hiding this comment

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

Let's make sure our code block is flush against the function definition!

Suggested change
def get_tasks_one_goal(goal_id):
goal = validate_model_goal(Goal, goal_id)
def get_tasks_one_goal(goal_id):
goal = validate_model_goal(Goal, goal_id)


tasks_response = []
for task in goal.tasks:
tasks_response.append(
{
"id": task.task_id,
"title": task.title,
"description": task.description,
"completed_at": None
"goal_id":
Comment on lines +117 to +118

Choose a reason for hiding this comment

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

Ah, looks like we forgot to finish out this dictionary. I reran the tests after fixing this, and they all passed, so maybe sometimes during editing this got erased.

Suggested change
"completed_at": None
"goal_id":
"completed_at": None,
"goal_id": goal_id

}
)
#
# tasks_response = [response_dict for task in goal.tasks]
Comment on lines +121 to +122

Choose a reason for hiding this comment

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

Let's get rid of this since we aren't using it!

Suggested change
#
# tasks_response = [response_dict for task in goal.tasks]


return jsonify(tasks_response)
31 changes: 30 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,33 @@


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)

Choose a reason for hiding this comment

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

A cool thing we can do is make sure that data with required fields aren't created with NULL values with the argument nullable=False. This can help if no conditional in the backend checks for empty fields. Sometimes things can slip through the backend, but this argument will add another layer of defense!

tasks = db.relationship("Task", back_populates="goal", lazy=True)

@classmethod
def goal_from_dict(cls, goal_data):

Choose a reason for hiding this comment

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

Since this is an instance method designed solely for the Goal class, let's refactor the method name and take out goal, so it's something more like from_dict.

new_goal = Goal(title=goal_data["title"])
return new_goal

def goal_to_dict(self):

Choose a reason for hiding this comment

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

Same here! Let's change this to to_dict

goal_as_dict = {}

goal_as_dict["id"] = self.goal_id
goal_as_dict["title"] = self.title
if self.tasks:
task_list = []
for task in self.tasks:
task_list.append(task.task_to_dict(self))
goal_as_dict["tasks"] = task_list

return goal_as_dict

def goal_to_dict_with_task(self):
goal_as_dict = {}

goal_as_dict["id"] = self.goal_id
goal_as_dict["title"] = self.title
goal_as_dict["tasks"] = self.tasks

return goal_as_dict
Comment on lines +27 to +34

Choose a reason for hiding this comment

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

Looks like you incorporated tasks into the previous method to_dict already, so let's get rid of this one since we don't use it.

43 changes: 42 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
from app import db
from app.models.goal import Goal


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
"""Task definition"""
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)
goal_id = db.Column(db.Integer, db.ForeignKey("goal.goal_id"))
goal = db.relationship("Goal", back_populates="tasks")

@classmethod
def task_from_dict(cls, task_data):
"""Input task as a dictionary. Assumes None/null for completed_at"""

if not "completed_at" in task_data:
task_data["completed_at"] = None

new_task = Task(title=task_data["title"],

Choose a reason for hiding this comment

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

What happens if the class name is changed on line 5? We'd have to remember to change it here, too! Or! we can use the cls parameter:

        new_task = cls(title=task_data["title"],

description=task_data["description"],
completed_at=task_data["completed_at"])
return new_task

def task_to_dict(self):
"""Output task information as a dictionary"""
task_as_dict = {}

task_as_dict["id"] = self.task_id
task_as_dict["title"] = self.title
task_as_dict["description"] = self.description
task_as_dict["is_complete"] = self.completed_at != None

Choose a reason for hiding this comment

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

Good job! We can even turn this into a ternary expression:

task_as_dict["is_complete"] = True if self.completed_at else False

or with the bool function:

task_as_dict["is_complete"] = bool(self.completed_at)


return task_as_dict

def task_with_goal_to_dict(self):
task_as_dict = {}

task_as_dict["id"] = self.task_id
task_as_dict["title"] = self.title
task_as_dict["description"] = self.description
task_as_dict["is_complete"] = self.completed_at != None

Choose a reason for hiding this comment

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

Good idea using an expression to assign a true or false value! An alternative option might be :

bool(self.completed_at)

task_as_dict["goal_id"] = self.goal_id

return task_as_dict
162 changes: 161 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,161 @@
from flask import Blueprint
from app import db
from app.models.task import Task
from app.models.goal import Goal
from flask import Blueprint, jsonify, make_response, request, abort
from sqlalchemy.types import DateTime
from sqlalchemy.sql.functions import now
Comment on lines +5 to +6

Choose a reason for hiding this comment

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

We should kept sqlalchemy model functionality and import in the class files themselves, so let's great rid of these. Continue below for more feedback on this.

Suggested change
from sqlalchemy.types import DateTime
from sqlalchemy.sql.functions import now

import requests, json
import os
# import logging
# from slack_sdk import WebClient
# from slack_sdk.errors import SlackApiError
Comment on lines +9 to +11

Choose a reason for hiding this comment

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

Let's get rid of these since we aren't using them

Suggested change
# import logging
# from slack_sdk import WebClient
# from slack_sdk.errors import SlackApiError



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

def validate_model_task(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({"message": f"{cls.__name__} {model_id} invalid"}, 400))

task = cls.query.get(model_id)

if not task:
abort(make_response({"message": f"{cls.__name__} {model_id} not found"}, 404))

return task
Comment on lines +16 to +27

Choose a reason for hiding this comment

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

Let's use the one already defined in goals by importing it. Then we can get rid of this version!


@task_list_bp.route("", methods=["POST"])
def create_task():
request_body = request.get_json()
is_valid_task = "title" in request_body and "description" in request_body
if not is_valid_task:
Comment on lines +32 to +33

Choose a reason for hiding this comment

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

Oh very interesting approach! Any reason you decided to make a variable first rather than combine the two lines together? I'm curious!

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

new_task = Task.task_from_dict(request_body)

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

response_body = {
"task": new_task.task_to_dict()
}
return make_response(jsonify(response_body), 201)


@task_list_bp.route("", methods=["GET"])
def get_all_tasks():
sort_query = request.args.get("sort")
if sort_query == "asc":
tasks = Task.query.order_by(Task.title)
elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc())
else:
tasks = Task.query.all()

tasks_response = [task.task_to_dict() for task in tasks]
Comment on lines +49 to +57

Choose a reason for hiding this comment

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

This would make a good filtering helper function!

return jsonify(tasks_response)

@task_list_bp.route("/<task_id>", methods=["GET"])
def get_one_task(task_id):
task = validate_model_task(Task, task_id)
response_body = {
"task": task.task_to_dict()
}
return jsonify(response_body)

Choose a reason for hiding this comment

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

Oops, you forgot your status code! It will implicitly return a status code, but we should add it ourselves to be in control of what is returned


@task_list_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):
task = validate_model_task(Task, task_id)
request_body = request.get_json()
task.title = request_body["title"]
task.description = request_body["description"]

db.session.commit()

response_body = {
"task": task.task_to_dict()
}
return response_body

Choose a reason for hiding this comment

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

Try to keep your responses as consistent as possible. Since you've used jsonify previously, continue using it throughout.

return jsonify(response_body), 200


@task_list_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task = validate_model_task(Task, task_id)
response_body = {
"details": f'Task {task.task_id} \"{task.title}\" successfully deleted'
}

db.session.delete(task)
db.session.commit()

return response_body

@task_list_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def mark_task_complete(task_id):
request_body = request.get_json()

try:
task = Task.query.get(task_id)
task.completed_at = now()
except:
return abort(make_response({"message": f"{task_id} not found"}, 404))

db.session.commit()

response_body = {
"task": task.task_to_dict()
}

SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
my_headers = {
"Authorization": f"Bearer {SLACK_BOT_TOKEN}"
}

data_payload = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task.title}"
}

requests.post("https://slack.com/api/chat.postMessage",
headers=my_headers,
data=data_payload)
Comment on lines +110 to +122

Choose a reason for hiding this comment

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

This would be a good candidate for a helper function


return jsonify(response_body)


# ~~Below is the the code I would have used given Slack documentation for calling their API~~

Choose a reason for hiding this comment

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

Good job working with the documentation to find a solution! 👍


# client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN"))
# logger = logging.getLogger(__name__)
# channel_id = "task-notifications"

# try:
# # Call the chat.postMessage method using the WebClient
# result = client.chat_postMessage(
# channel=channel_id,
# text=f"Someone just completed the task {task.title}."
# )
# logger.info(result)

# except SlackApiError as e:
# logger.error(f"Error posting message: {e}")



@task_list_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def mark_task_incomplete(task_id):
request_body = request.get_json()
try:
task=Task.query.get(task_id)
task.completed_at = None
except:
response_body = abort(make_response({"message": f"{task_id} not found"}, 404))
return response_body
Comment on lines +153 to +154

Choose a reason for hiding this comment

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

I believe abort has an implicit return, so we can combine these two lines together:

        abort(make_response({"message": f"{task_id} not found"}, 404))


db.session.commit()
response_body = {
"task": task.task_to_dict()
}

return jsonify(response_body)
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