-
Notifications
You must be signed in to change notification settings - Fork 36
Xin.L-Task_list_api #29
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
base: main
Are you sure you want to change the base?
Changes from all commits
f991ba6
0b1d5e6
47dbd85
69b31b5
6857462
8bcff55
fabe8fe
a0bc2b9
1d2fc84
b9a6eff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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) |
| 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"]) |
| 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") | ||
| ) | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great use of
getto access the optional attribute.