Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6e4ec1d
Add GET to the route to retrieve one task by id
Helenlarson Nov 5, 2025
3f0d82c
Add PUT route to update a task by id
Helenlarson Nov 5, 2025
0d42eec
Add validate_model helper and import it in task_routes
Helenlarson Nov 5, 2025
ceaeafb
Made changes on the task.py
Helenlarson Nov 5, 2025
8e6cb8c
Completed wave 1: all testes passed
Helenlarson Nov 5, 2025
44b7941
Wave 1-2: fix Task.from_dict (KeyError), unify not-found JSON (messag…
Helenlarson Nov 5, 2025
7267e60
Add assert to the wave 3 tests
Helenlarson Nov 5, 2025
ae5b427
Finished wave 3 all testes passed
Helenlarson Nov 6, 2025
48c2591
feat(tasks): send Slack notification when marking a task as complete
Helenlarson Nov 6, 2025
af7b120
Did some changes on the wave 3 at the task routes file to pass at the…
Helenlarson Nov 10, 2025
5a83cb7
Made changes on the tests to pass on Wave 5 and all passed
Helenlarson Nov 10, 2025
71082e4
models+routes: add Goal<->Task relationship, helpers e ajustes Wave 6
Helenlarson Nov 10, 2025
0111144
revert: restore Wave 5/6 tests to original
Helenlarson Nov 10, 2025
6613431
Did some changes in the routes and models to pass the tests, all test…
Helenlarson Nov 10, 2025
11f2925
Merge pull request #1 from Helenlarson/Helenlarson/task-list-api
Helenlarson Nov 10, 2025
f7fc77a
Resolve merge: keep local versions for Wave 6
Helenlarson Nov 10, 2025
726de20
Add common.py helper file to refactor shared route logic for Wave 7
Helenlarson Nov 10, 2025
74e9ba1
Revert "Did some changes in the routes and models to pass the tests, …
Helenlarson Nov 12, 2025
b443e0b
Add up_date_goal to goal routes
Helenlarson Nov 16, 2025
d3563ab
I made the final changes and all 7 waves passed
Helenlarson Nov 17, 2025
f8d3c46
Add initial project files
Helenlarson Nov 17, 2025
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
8 changes: 5 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from flask import Flask
from .db import db, migrate
from .models import task, goal
from .routes.task_routes import bp as task_bp
from .routes.goal_routes import bp as goal_bp
Comment on lines +4 to +5
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Great job aliasing your blueprints!

import os

def create_app(config=None):
Expand All @@ -10,13 +12,13 @@ def create_app(config=None):
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')

if config:
# Merge `config` into the app's configuration
# to override the app's default settings for testing
app.config.update(config)

db.init_app(app)
migrate.init_app(app, db)

# Register Blueprints here
app.register_blueprint(task_bp)
app.register_blueprint(goal_bp)

return app

21 changes: 20 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db import db
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .task import Task
Comment on lines +3 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

While TYPE_CHECKING isn't necessarily required here because we have a one-to-many relationship, it is best practice and is necessary to avoid circular imports in a many-to-many relationship, so great job using it here.

Also, as stated in our Learn lessons, it does allow us to get rid of the yellow underline on line 11!


class Goal(db.Model):
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):
goal_dict = {
"id": self.id,
"title": self.title,
}
return goal_dict

@classmethod
def from_dict(cls, data):

new_goal = cls(title=data["title"])
return new_goal
Comment on lines +13 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Your to_dict() and your from_dict() look great, Helen!

34 changes: 33 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from ..db import db
from typing import Optional, TYPE_CHECKING
from datetime import datetime

if TYPE_CHECKING:
from .goal import Goal

class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
description: Mapped[str]
completed_at: Mapped[Optional[datetime]]
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")
Comment on lines 11 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just for clarity's sake, it is often a good idea to move any relationships/columns associated with the relationships to the bottom of the attributes list here like you have done! One small common pattern that you will often see is something like the following:

Suggested change
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
description: Mapped[str]
completed_at: Mapped[Optional[datetime]]
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
description: Mapped[str]
completed_at: Mapped[Optional[datetime]]
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")

def to_dict(self):
task_dict = {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": self.completed_at is not None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks great!

}
if self.goal:
task_dict["goal_id"] = self.goal.id
Comment on lines +24 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice job adding the goal id to the dictionary if it exists!

return task_dict

@classmethod
def from_dict(cls, data):
goal_id = data.get("goal_id")

new_task = cls(
title=data["title"],
description=data["description"],
goal_id=goal_id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Slight nitpick, but feel free to move the ending parenthesis to the next line!


return new_task
27 changes: 27 additions & 0 deletions app/routes/common.py
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A couple things about this particular file!

  1. The name common.py is not a super descriptive name! If you are going to drop several functions into a file, make sure it has an easy to understand name!
  2. I can't seem to find any of these helper functions used at any point in your routes or in your code! If they are going unused, feel free to remove the file itself!

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from flask import request
from app.db import db
from .route_utilities import validate_model, create_model as util_create_model

def create_model_endpoint(model_cls, required_keys=("title",)):
body = request.get_json() or {}
return util_create_model(model_cls, body)

def list_models_endpoint(model_cls):
return [m.to_dict() for m in model_cls.query.all()]

def get_one_endpoint(model_cls, model_id):
instance = validate_model(model_cls, model_id)
return instance.to_dict(), 200

def update_fields_endpoint(instance, fields):
body = request.get_json() or {}
for f in fields:
if f in body:
setattr(instance, f, body[f])
db.session.commit()
return instance.to_dict(), 200

def delete_model_endpoint(instance, success_msg_template):
db.session.delete(instance)
db.session.commit()
return {"details": success_msg_template.format(id=instance.id, title=getattr(instance, "title", ""))}, 200
81 changes: 80 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,80 @@
from flask import Blueprint
from flask import Blueprint, request, Response
from app.models.goal import Goal
from app.models.task import Task
from ..db import db
from .route_utilities import validate_model, create_model, get_models_with_filters

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

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

return create_model(Goal, request_body)
Comment on lines +10 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Perfect!


@bp.post("/<goal_id>/tasks")
def create_task_with_goal(goal_id):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a small tweak, but the name create_task_with_goal implies that you'll be creating a task from a specific goal. While this is possible, it's not what this particular endpoint is doing. A more appropriate name for this view function might be something like add_tasks_to_goal.


goal = validate_model(Goal, goal_id)

request_body = request.get_json()
task_ids = request_body["task_ids"]

task_list = []
for id in task_ids:
task = validate_model(Task, id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice use of the validate_model function here!

task_list.append(task)

goal.tasks = task_list
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What I really love about this particular approach is that rather than have to zero out the tasks already associated with a goal, you simply create the new task_list and then update the goal.tasks to match that list! Well done!

db.session.commit()

return {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remember that Flask's default return status code for a view function is 200. As a result, we don't need to explicitly return it!

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


@bp.get("/<goal_id>/tasks")
def get_tasks_by_goal(goal_id):

goal = validate_model(Goal, goal_id)
tasks = [task.to_dict() for task in goal.tasks]

return {
"id": goal.id,
"title": goal.title,
"tasks": tasks
Comment on lines +41 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I wonder if there is a way for us to include the tasks as part of the Goal's to_dict() function?

}, 200


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

return get_models_with_filters(Goal, request.args)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice helper function usage!


@bp.get("<goal_id>")
def get_one_goal(goal_id):

goal = validate_model(Goal, goal_id)
return goal.to_dict()


@bp.put("<goal_id>")
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 Response(status=204, mimetype="application/json")


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

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

return Response(status=204, mimetype="application/jason")
Comment on lines +55 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The rest of these view functions look really good! Great job!!

53 changes: 53 additions & 0 deletions app/routes/route_utilities.py
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This set of helper functions looks practically perfect! Well done, Helen!

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from flask import abort, make_response, request
from ..db import db

def validate_model(cls, model_id):
try:
model_id = int(model_id)

except ValueError:
response = {"message": f"{cls.__name__} {model_id} is invalid"}
abort(make_response(response, 400))

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

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

return model


def create_model(cls, model_data):
try:
new_model = cls.from_dict(model_data)

except KeyError as error:
response = {"details": f"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):
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 attribute == "sort":
if value == "desc":
query = query.order_by(cls.title.desc())
elif value == "asc":
query = query.order_by(cls.title.asc())
Comment on lines +39 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You are the first person who I have seen successfully incorporate both filtering and sorting by ascending or descending within the same function! Well done!


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

return models_response
85 changes: 84 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,84 @@
from flask import Blueprint
from flask import Blueprint, request, Response
from app.models.task import Task
from ..db import db
from datetime import datetime
import requests
import os
from .route_utilities import validate_model, create_model, get_models_with_filters

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

@bp.post("")
def create_task():

request_body = request.get_json()
return create_model(Task, request_body)


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

return get_models_with_filters(Task, request.args)


@bp.get("/<task_id>")
def get_one_task(task_id):

task = validate_model(Task, task_id)
return task.to_dict()


@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 Response(status=204, mimetype="application/json")


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

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


url = "https://slack.com/api/chat.postMessage"
data = {
"text": f"Someone just completed the task {task.title}",
"channel": "test-slack-api"
}
headers = {
'Content-Type': 'application/json',
'Authorization': f"Bearer {os.environ.get('SLACK_BOT_TOKEN')}"
}
requests.post(url, headers=headers, json=data)
Comment on lines +52 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This approach to making a Slack API request looks really good, but I do think it could be removed to its own helper function elsewhere!



return Response(status=204, mimetype="application/json")


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

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

return Response(status=204, mimetype="application/json")


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

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

return Response(status=204, mimetype="application/jason")
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.
Loading