Skip to content

Olga_Karaivanska_task_list_api #41

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

Open
wants to merge 2 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
2 changes: 1 addition & 1 deletion ada-project-docs/wave_04.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ Press "Send" and see the Slack message come through!

### Modify `/tasks/<task_id>/mark_complete` to Call the Slack API

Now that we've verified that we can successfully make a request to the Slack API, let's write some code to automatically call the Slack API from the API we've created!
Now that we've verified that we can successfully make a request to the Slack API, let's write some code to automatically call the Slack API from the API we've created!#

Given a task that has:

Expand Down
9 changes: 8 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from flask import Flask
from .db import db, migrate
from .models import task, goal
from .routes.task_routes import bp as tasks_bp
from .routes.goal_routes import bp as goal_bp
from flask_cors import CORS
import os

def create_app(config=None):
app = Flask(__name__)
CORS(app)

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')


if config:
# Merge `config` into the app's configuration
Expand All @@ -18,5 +23,7 @@ def create_app(config=None):
migrate.init_app(app, db)

# Register Blueprints here

app.register_blueprint(tasks_bp)
app.register_blueprint(goal_bp)
return app

15 changes: 14 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db import db

class Goal(db.Model):
__tablename__ = 'goal'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
tasks: Mapped[list["Task"]] = relationship(back_populates="goal")

def to_dict(self):
return {
"id": self.id,
"title": self.title
}

@classmethod
def from_dict(cls, goal_data):
return cls(title=goal_data["title"])
29 changes: 28 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey, DateTime
from ..db import db
from datetime import datetime
from typing import Optional


class Task(db.Model):
__tablename__ = 'task'
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
description: Mapped[str]
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True )
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")
Comment on lines +11 to +15

Choose a reason for hiding this comment

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

Nice work on getting these columns made!


def to_dict(self):
return {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": self.completed_at is not None

Choose a reason for hiding this comment

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

⭐️

}

@classmethod
def from_dict(cls, task_data):
return cls(
title=task_data["title"],
description=task_data["description"],
completed_at=datetime.now() if task_data.get("is_complete") else None,
goal_id=task_data.get("goal_id", None)
)
95 changes: 94 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,94 @@
from flask import Blueprint
from flask import Blueprint, request, abort, make_response
from app.models.goal import Goal
from app.models.task import Task
from .route_utilities import validate_model, create_model, get_models_with_filters
from ..db import db

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

@bp.post("")
def create_goal():
request_body = request.get_json()
if "title" not in request_body or not request_body["title"]:
return {"details": "Invalid data"}, 400

goal_data = {"title": request_body.get("title")}
Comment on lines +12 to +15

Choose a reason for hiding this comment

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

This logic could go inside of our create_model function, we would just have modify the logic here and in model methods to check if check if request was a bad one.

goal_dict, status_code = create_model(Goal, goal_data)

return {"goal": goal_dict}, status_code

@bp.get("")
def get_goal():

goals_response = get_models_with_filters(Goal)

Choose a reason for hiding this comment

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

Nice work!

return goals_response, 200


@bp.get("/<goal_id>")
def get_one_goal(goal_id):
goal = validate_model(Goal, goal_id)

return {"goal":goal.to_dict()}


@bp.put("<goal_id>")
def update_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()

goal.title = request_body["title"]

Choose a reason for hiding this comment

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

Isn't this code similar to our other .put method? How could we use the hasattr and setattr functions that Python provides in order to make DRY up our routes?

db.session.commit()

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

Choose a reason for hiding this comment

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

We essentially just want an outer dictionary structure with a key of the class name, perhaps we could move this into a helper function or add it to a class in order to DRY up some of our code given that we have this same logic throughout the project?



@bp.delete("<goal_id>")
def delete_goal(goal_id):
goal = validate_model(Goal, goal_id)

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

response = {
"details": f'Goal {goal.id} "{goal.title}" successfully deleted'
}
return response, 200

@bp.post("/<goal_id>/tasks")
def associate_tasks_with_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()
task_ids = request_body.get("task_ids", [])

for task_id in task_ids:
task = validate_model(Task,task_id)
task.goal_id = goal.id

db.session.commit()

return {
"id": goal.id,
"task_ids": task_ids
}, 200

@bp.get("/<goal_id>/tasks")
def get_tasks_of_goal(goal_id):
goal = validate_model(Goal, goal_id)

tasks_data = [
{
"id": task.id,
"goal_id": task.goal_id,
"title": task.title,
"description": task.description,
"is_complete": bool(task.completed_at)
}
Comment on lines +79 to +85

Choose a reason for hiding this comment

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

Couldn't we use our to_dict method here instead since the structure is so similar?

for task in goal.tasks
]

return {
"id": goal.id,
"title": goal.title,
"tasks": tasks_data
}, 200

50 changes: 50 additions & 0 deletions app/routes/route_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from flask import abort, make_response
from ..db import db

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

query = db.select(cls).where(cls.id == model_id)
model = db.session.scalar(query)

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

return model

def create_model(cls, model_data):
try:
new_model = cls.from_dict(model_data)
except (KeyError, ValueError):
response = {"details": "Invalid data"}
abort(make_response(response, 400))

db.session.add(new_model)
db.session.commit()

return new_model.to_dict(), 201

def get_models_with_filters(cls, filters=None, sort_by=None, sort_order='asc'):
query = db.select(cls)

if filters:
for attribute, value in filters.items():
if hasattr(cls, attribute):
query = query.where(getattr(cls, attribute).ilike(f"%{value}%"))
if sort_by and hasattr(cls, sort_by):
sort_attr = getattr(cls, sort_by)
if sort_order == 'asc':
query = query.order_by(sort_attr.asc())
elif sort_order == 'desc':
query = query.order_by(sort_attr.desc())
else:
query = query.order_by(cls,id)


models = db.session.scalars(query.order_by(cls.id))
models_response = [model.to_dict() for model in models]

return models_response

Choose a reason for hiding this comment

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

Nice work with these functions!

99 changes: 98 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,98 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, Response, request
import requests
from app.models.task import Task
from datetime import datetime
from sqlalchemy import asc, desc
from .route_utilities import validate_model, create_model, get_models_with_filters
from ..db import db
import os


bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks")

@bp.post("")
def create_task():
request_body = request.get_json()

if "title" not in request_body or "description" not in request_body:
return {"details": "Invalid data"}, 400

task_data = {"title": request_body.get("title"),
"description": request_body.get("description"),
"completed_at": request_body.get("completed_at")}
Comment on lines +20 to +22

Choose a reason for hiding this comment

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

We don't necessarily need to do this part, we could pass in the request body since it has the keys and the accompanying values we need for our model construction.


task_dict, status_code = create_model(Task, task_data)
return {"task": task_dict}, status_code

@bp.get("")
def get_task():
query_params = request.args.to_dict()
sort_order = query_params.pop('sort', 'asc')
sort_by = query_params.pop('sort_by', 'title')
tasks_response = get_models_with_filters(Task, sort_by=sort_by, sort_order=sort_order)
return tasks_response, 200


@bp.get("/<task_id>")
def get_one_task(task_id):
task = validate_model(Task, task_id)
response = {"task": task.to_dict()}

if task.goal_id is not None:
response["task"]["goal_id"] = task.goal_id

Choose a reason for hiding this comment

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

I would put this as None that way there isn't confusion where people might think you are adding a goal_id even though there isn't one.

return response, 200

@bp.put("/<task_id>")
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"]

db.session.commit()

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

@bp.delete("/<task_id>")
def delete_task(task_id):
task = validate_model(Task, task_id)
db.session.delete(task)
db.session.commit()

response = {
"details": f'Task {task.id} "{task.title}" successfully deleted'
}

return response, 200


@bp.patch("/<task_id>/mark_complete")
def mark_task_complete(task_id):
task = validate_model(Task, task_id)

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

slack_token = os.environ.get("SLACK_BOT_TOKEN")
url = "https://slack.com/api/chat.postMessage"
headers = {"Authorization": f"Bearer {slack_token}"}
request_body = {
"channel": "task-notifications",
"text": f"Someone just completed the task '{task.title}'"
}
requests.post(url, json=request_body, headers=headers)

return{"task": task.to_dict()}, 200
Comment on lines +76 to +85

Choose a reason for hiding this comment

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

I would move this logic into a helper function and then that helper function into our utilities file to help with separation of concerns and readability.



@bp.patch("/<task_id>/mark_incomplete")
def mark_task_incomplete(task_id):

task = validate_model(Task, task_id)

task.completed_at = None
db.session.commit()

return {"task": task.to_dict()}


1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Single-database configuration for Flask.
50 changes: 50 additions & 0 deletions migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 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,flask_migrate

[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

[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate

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