Skip to content
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
4 changes: 4 additions & 0 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 tasks_bp
from .routes.goal_routes import goals_bp
import os

def create_app(config=None):
Expand All @@ -18,5 +20,7 @@ def create_app(config=None):
migrate.init_app(app, db)

# Register Blueprints here
app.register_blueprint(tasks_bp)
app.register_blueprint(goals_bp)

return app
20 changes: 19 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db import db
from typing import Dict, Any, List

class Goal(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(nullable=False)

tasks = relationship("Task", back_populates="goal", cascade="all, delete-orphan")

@classmethod
def from_dict(cls, goal_data: Dict[str, Any]) -> "Goal":
"""Create a Goal instance from a dictionary."""
return cls(
title=goal_data["title"]
)

def to_dict(self) -> Dict[str, Any]:
"""Convert the Goal instance to a dictionary."""
return {
"id": self.id,
"title": self.title
}
33 changes: 32 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
from sqlalchemy.orm import Mapped, mapped_column
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import Text, DateTime, ForeignKey
from ..db import db


class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
description: Mapped[str] = mapped_column(Text)
completed_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)

goal_id = mapped_column(ForeignKey("goal.id"), nullable=True)
goal = relationship("Goal", 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
}

# add goal_id if the task belongs to a goal
if self.goal_id:
task_dict["goal_id"] = self.goal_id

return task_dict

@classmethod
def from_dict(cls, task_data):
return cls(
title=task_data["title"],
description=task_data["description"],
completed_at=task_data.get("completed_at")
)
140 changes: 139 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,139 @@
from flask import Blueprint
from flask import Blueprint, request, make_response, abort, Response
from ..db import db
from app.models.goal import Goal
from app.models.task import Task

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

def validate_task(task_id):
try:
task_id = int(task_id)
except ValueError:
response = {"message": "Invalid task ID"}
abort(make_response(response, 400))

query = db.select(Task).where(Task.id == task_id)
task = db.session.scalar(query)

if not task:
response = {"message": "Task not found"}
abort(make_response(response, 404))

return task


def validate_goal(goal_id):
try:
goal_id = int(goal_id)
except ValueError:
response = {"message": "Invalid goal ID"}
abort(make_response(response, 400))

query = db.select(Goal).where(Goal.id == goal_id)
goal = db.session.scalar(query)

if not goal:
response = {"message": "Goal not found"}
abort(make_response(response, 404))

return goal


@goals_bp.post("")
def create_goal():
"""Create a new goal"""
request_body = request.get_json()

# Required fields
if "title" not in request_body:
return {"details": "Invalid data"}, 400

try:
new_goal = Goal.from_dict(request_body)
except KeyError:
return {"details": "Invalid data"}, 400

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

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


@goals_bp.get("")
def get_all_goals():
"""Get all goals"""
query = db.select(Goal)
goals = db.session.scalars(query)

return [goal.to_dict() for goal in goals]


@goals_bp.get("/<goal_id>")
def get_goal(goal_id):
"""Get a specific goal by ID"""
goal = validate_goal(goal_id)

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


@goals_bp.put("/<goal_id>")
def update_goal(goal_id):
"""Update a goal by ID"""
goal = validate_goal(goal_id)
request_body = request.get_json()

if "title" in request_body:
goal.title = request_body["title"]

db.session.commit()

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


@goals_bp.delete("/<goal_id>")
def delete_goal(goal_id):
"""Delete a goal by ID"""
goal = validate_goal(goal_id)

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

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


@goals_bp.get("/<goal_id>/tasks")
def get_tasks_for_goal(goal_id):
"""Get all tasks associated with a specific goal"""
goal = validate_goal(goal_id)

response = {
"id": goal.id,
"title": goal.title,
"tasks": [task.to_dict() for task in goal.tasks]
}

return response


@goals_bp.post("/<goal_id>/tasks")
def add_tasks_to_goal(goal_id):
"""Associate multiple tasks with a goal"""
goal = validate_goal(goal_id)
request_body = request.get_json()

if "task_ids" not in request_body:
return {"message": "Missing task_ids field"}, 400

goal.tasks = []

# Add each task from the task_ids list
for task_id in request_body["task_ids"]:
task = validate_task(task_id)
goal.tasks.append(task)

db.session.commit()

return {
"id": goal.id,
"task_ids": [task.id for task in goal.tasks]
}
161 changes: 160 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,160 @@
from flask import Blueprint
from flask import Blueprint, request, make_response, abort, Response
from ..db import db
from app.models.task import Task
from datetime import datetime
import requests
import os

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

# Slack API
SLACK_API_TOKEN = os.environ.get('SLACK_API_TOKEN')
SLACK_CHANNEL = os.environ.get('SLACK_CHANNEL', 'task-notifications')

def send_slack_notification(message):
if not SLACK_API_TOKEN:
print("Warning: SLACK_API_TOKEN not configured.")
return False

url = "https://slack.com/api/chat.postMessage"
headers = {
"Authorization": f"Bearer {SLACK_API_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"channel": SLACK_CHANNEL,
"text": message
}

try:
response = requests.post(url, json=payload, headers=headers)
data = response.json()

if data.get("ok"):
return True
else:
print(f"Slack API error: {data.get('error')}")
return False
except Exception as e:
print(f"Error sending Slack notification: {str(e)}")
return False


def validate_task(task_id):
"""
Validates that a task with the given ID exists.
Returns the task if it exists, otherwise aborts with 404.
"""
try:
task_id = int(task_id)
except ValueError:
response = {"error": "Invalid task ID"}
abort(make_response(response, 400))

query = db.select(Task).where(Task.id == task_id)
task = db.session.scalar(query)

if not task:
response = {"error": "Task not found"}
abort(make_response(response, 404))

return task


@tasks_bp.post("")
def create_task():
"""Create a new task"""
request_body = request.get_json()

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

try:
new_task = Task.from_dict(request_body)
except KeyError:
return {"details": "Invalid data"}, 400

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

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


@tasks_bp.get("")
def get_all_tasks():
"""Get all tasks"""
sort_param = request.args.get("sort")

query = db.select(Task)

# sort by title
if sort_param == "asc":
query = query.order_by(Task.title.asc())
elif sort_param == "desc":
query = query.order_by(Task.title.desc())

tasks = db.session.scalars(query)

return [task.to_dict() for task in tasks]


@tasks_bp.get("/<task_id>")
def get_task(task_id):
"""Get a specific task by ID"""
task = validate_task(task_id)

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


@tasks_bp.put("/<task_id>")
def update_task(task_id):
"""Update a task by ID"""
task = validate_task(task_id)
request_body = request.get_json()

if "title" in request_body:
task.title = request_body["title"]
if "description" in request_body:
task.description = request_body["description"]

db.session.commit()

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


@tasks_bp.delete("/<task_id>")
def delete_task(task_id):
"""Delete a task by ID"""
task = validate_task(task_id)

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

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


@tasks_bp.patch("/<task_id>/mark_complete")
def mark_task_complete(task_id):
"""Mark a task as complete"""
task = validate_task(task_id)

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

# Send Slack notification when a task is marked complete
notification_message = f"Someone just completed the task {task.title}"
send_slack_notification(notification_message)

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


@tasks_bp.patch("/<task_id>/mark_incomplete")
def mark_task_incomplete(task_id):
"""Mark a task as incomplete"""
task = validate_task(task_id)

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

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