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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Local development database (PostgreSQL)
SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://user:password@localhost:5432/task_list_api_development

# Test database
SQLALCHEMY_TEST_DATABASE_URI=postgresql+psycopg2://user:password@localhost:5432/task_list_api_test

# Slack API token (optional for Slack integration features)
slack_token=your-slack-token-here
86 changes: 46 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,63 @@
# Task List API

## Skills Assessed
> **Note**: This is a course project from [Ada Developer Academy](https://adadeveloperacademy.org/). The project framework and requirements were provided by Ada, and I have implemented all the functionality across waves 1-7 to meet the course specifications.

- Gathering technical requirements from written documentation
- Reading, writing, and using tests
- Demonstrating understanding of the client-server model, request-response cycle and conventional RESTful routes
- Driving development with independent research, experimentation, and collaboration
- Reading and using existing external web APIs
- Using Postman as part of the development workflow
- Using git as part of the development workflow

Working with the Flask package:
## Local Development Setup

- Creating models
- Creating conventional RESTful CRUD routes for a model
- Reading query parameters to create custom behavior
- Create unconventional routes for custom behavior
- Apply knowledge about making requests in Python, to call an API inside of an API
- Apply knowledge about environment variables
- Creating a one-to-many relationship between two models
### Prerequisites
- Python 3.13+
- PostgreSQL
- pip and virtual environment

## Goals
### Installation Steps

There's so much we want to do in the world! When we organize our goals into smaller, bite-sized tasks, we'll be able to track them more easily, and complete them!
1. **Clone the repository**
```bash
git clone <repository-url>
cd task-list-api
```

If we make a web API to organize our tasks, we'll be able to create, read, update, and delete tasks as long as we have access to the Internet and our API is running!
2. **Create and activate virtual environment**
```bash
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```

We also want to do some interesting features with our tasks. We want to be able to:
3. **Install dependencies**
```bash
pip install -r requirements.txt
```

- Sort tasks
- Mark them as complete
- Get feedback about our task list through Slack
- Organize tasks with goals
4. **Set up environment variables**
```bash
cp .env.example .env
```
Then edit `.env` with your local database credentials and Slack token (if needed).

... and more!
5. **Create local database**
```bash
createdb task_list_api_development
createdb task_list_api_test
```

## How to Complete and Submit
6. **Run database migrations**
```bash
flask db upgrade
```

Go through the waves one-by-one and build the features of this API.
7. **Run the application**
```bash
flask run
```

At submission time, no matter where you are, submit the project via Learn.
The API will be available at `http://localhost:5000`

## Project Directions
### Running Tests

This project is designed to fulfill the features described in detail in each wave. The tests are meant to only guide your development.
```bash
pytest
# Or use the provided test script
./test.sh
```

1. [Setup](ada-project-docs/setup.md)
1. [Testing](ada-project-docs/testing.md)
1. [Wave 1: CRUD for one model](ada-project-docs/wave_01.md)
1. [Wave 2: Using query params](ada-project-docs/wave_02.md)
1. [Wave 3: Creating custom endpoints](ada-project-docs/wave_03.md)
1. [Wave 4: Using an external web API](ada-project-docs/wave_04.md)
1. [Wave 5: Creating a second model](ada-project-docs/wave_05.md)
1. [Wave 6: Establishing a one-to-many relationship between two models](ada-project-docs/wave_06.md)
1. [Wave 7: Deployment](ada-project-docs/wave_07.md)
1. [Optional Enhancements](ada-project-docs/optional-enhancements.md)
12 changes: 11 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
from flask import Flask
from flask_cors import CORS
from .db import db, migrate
from .models import task, goal
from app.routes.task_routes import bp as tasks_bp
from app.routes.goal_routes import bp as goals_bp
import os


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

CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'

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

if config:
# Merge `config` into the app's configuration
Expand All @@ -18,5 +26,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
22 changes: 21 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.task import Task
from ..db import db


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):
return {
"id": self.id,
"title": self.title
}

def to_dict_with_tasks(self):
dict_with_tasks = Goal.to_dict(self)
dict_with_tasks["tasks"] = [task.to_dict() for task in self.tasks]

return dict_with_tasks

@classmethod
def from_dict(cls, goal_data):
return cls(title=goal_data["title"])
32 changes: 31 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
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
from datetime import datetime


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")

def to_dict(self):
task_as_dict = {}
task_as_dict["id"] = self.id
task_as_dict["title"] = self.title
task_as_dict["description"] = self.description
task_as_dict["is_complete"] = self.completed_at is not None

if self.goal_id:
task_as_dict["goal_id"] = self.goal_id

return task_as_dict


@classmethod
def from_dict(cls, data):
return cls(
title=data["title"],
description=data["description"],
completed_at=data.get("completed_at")
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 use of get to access the optional attribute.

)
75 changes: 74 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,74 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, request, Response
from app.db import db
from app.models.goal import Goal
from app.models.task import Task
from app.routes.route_utilities import validate_model, create_model
import requests
import os

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


@bp.post("")
def create_goal():
request_body = request.get_json()
return create_model(Goal, request_body)


@bp.get("")
def get_all_goals():
query = db.select(Goal).order_by(Goal.id)
goals = db.session.scalars(query)
return [goal.to_dict() for goal in goals]


@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()

if not request_body or "title" not in request_body:
response = {"details": "Invalid data"}
abort(make_response(response, 400))

goal.title = request_body["title"]
db.session.commit()
return Response(status=204, mimetype="application/json")


@bp.delete("/<goal_id>")
def delete_goal_by_id(goal_id):
goal = validate_model(Goal, goal_id)
db.session.delete(goal)
db.session.commit()
return Response(status=204, mimetype="application/json")


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

for id in task_id_list:
task = validate_model(Task, id)
task.goal_id = goal_id

db.session.commit()

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


Comment on lines +68 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Love to see the consistent 2-lines spacing around your top level functions!

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

return goal.to_dict_with_tasks()
32 changes: 32 additions & 0 deletions app/routes/route_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from flask import abort, make_response
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} 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": "Invalid data"}
abort(make_response(response, 400))

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

return new_model.to_dict(), 201
Comment on lines +22 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since this both creates a model and creates an http response, I'd consider renaming this to create_model_and_response. Or to better follow single responsibility principles, we could split this into 2 functions:

  • one function that only manages creating and returns the new model
  • one function that uses the function above to get the model, then creates and returns the response based on that model.

Loading