-
Notifications
You must be signed in to change notification settings - Fork 44
Phinx C22 Task-list Somy Cho #44
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
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 |
|---|---|---|
| @@ -1,5 +1,27 @@ | ||
| from sqlalchemy.orm import Mapped, mapped_column | ||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
| from .task import Task | ||
| from ..db import db | ||
| from typing import List | ||
|
|
||
| class Goal(db.Model): | ||
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||
| title: Mapped[str] | ||
| tasks: Mapped[List["Task"]] = relationship("Task", back_populates="goal") | ||
|
|
||
| def to_dict(self): | ||
| goal_as_dict = { | ||
| "id": self.id, | ||
| "title": self.title, | ||
| "tasks": [task.to_dict() for task in self.tasks] # Include tasks in the goal's dict | ||
|
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. We have an endpoint that will provide us with the same data that |
||
| } | ||
| return goal_as_dict | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, goal_data): | ||
| return cls(title=goal_data["title"]) | ||
|
|
||
| def add_tasks(self, task_ids: List[int]): | ||
| tasks = Task.query.filter(Task.id.in_(task_ids)).all() | ||
| for task in tasks: | ||
| task.goal_id = self.id # Assign each task's goal_id | ||
|
Comment on lines
+23
to
+26
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. Nice work here, Somy! This is good work! Since we are searching the database we don't have to worry about validating any of the tasks! |
||
| db.session.commit() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,29 @@ | ||
| from sqlalchemy.orm import Mapped, mapped_column | ||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
| from ..db import db | ||
| from datetime import datetime | ||
| from typing import Optional | ||
| from sqlalchemy import ForeignKey | ||
|
|
||
| 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]] = mapped_column(nullable=True) | ||
|
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. If we want a column to be |
||
| goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"), nullable=True) | ||
| goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks") | ||
|
|
||
| def to_dict(self): | ||
| return { | ||
| "id": self.id, | ||
| "title": self.title, | ||
| "description": self.description, | ||
| "is_complete": bool(self.completed_at) | ||
|
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. ⭐️ |
||
| } | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, task_data): | ||
| return cls( | ||
| title=task_data["title"], | ||
| description=task_data["description"], | ||
| completed_at=task_data.get("completed_at") | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,119 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint, abort, make_response, request, Response | ||
| from app.models.goal import Goal | ||
| from ..db import db | ||
| from datetime import datetime, timezone | ||
|
|
||
| goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") | ||
|
|
||
| ##refactor validate!! | ||
| @goals_bp.post("/<goal_id>/tasks") | ||
| def create_goal_with_task(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
|
|
||
| request_body = request.get_json() | ||
| request_body["goal_id"] = goal.id | ||
|
|
||
| try: | ||
| new_task = Task.from_dict(request_body) | ||
| except KeyError as error: | ||
| response = {"message": f"Invalid requests: missing {error.ergs[0]}"} | ||
| abort(make_response(response, 400)) | ||
|
Comment on lines
+16
to
+20
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. This logic looks very similar to the logic we wrote in our create routes, what refactoring could be done here to move this code out of our routes into a helper function to support maintainability and scalability? (Hint: We did a refactor like this in Flasky). |
||
| db.session.add(new_task) | ||
| db.session.commit() | ||
|
|
||
| return make_response(new_task.to_dict(), 201) | ||
|
|
||
| @goals_bp.post("") | ||
| def create_goal(): | ||
| request_body = request.get_json() | ||
|
|
||
| # Validate request data | ||
| if "title" not in request_body: | ||
| response = {"details": "Invalid data"} | ||
| return make_response(response, 400) | ||
|
|
||
| title = request_body["title"] | ||
|
|
||
| # Create new goal instance | ||
| new_goal = Goal(title=title) | ||
|
Comment on lines
+28
to
+38
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. Another place where our refactored mentioned above could be of use. |
||
|
|
||
| # Add new goal to the database session and commit | ||
| db.session.add(new_goal) | ||
| db.session.commit() | ||
|
|
||
| response = { | ||
| "goal": { | ||
|
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. We essentially just want an outer dictionary structure with a |
||
| "id": new_goal.id, | ||
| "title": new_goal.title, | ||
| } | ||
| } | ||
|
|
||
| return response, 201 | ||
|
|
||
| @goals_bp.get('/<goal_id>') | ||
| def get_one_goal(goal_id): | ||
| goal = validate_goal(goal_id) | ||
|
|
||
| return { | ||
| "goal":{ | ||
| "id": goal.id, | ||
| "title": goal.title, | ||
| } | ||
| } | ||
|
|
||
|
|
||
| @goals_bp.get("") | ||
| def get_all_goals(): | ||
| query = db.select(Goal).order_by(Goal.id) | ||
| goals = db.session.scalars(query) | ||
|
|
||
| goals_response = [] | ||
| for goal in goals: | ||
| goals_response.append( | ||
| { | ||
| "id": goal.id, | ||
| "title": goal.title, | ||
| } | ||
| ) | ||
| return goals_response | ||
|
|
||
| @goals_bp.put("/<goal_id>") | ||
| def update_goal(goal_id): | ||
| goal = validate_goal(goal_id) | ||
| request_body = request.get_json() | ||
|
|
||
| goal.title = request_body["title"] | ||
|
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. Isn't this code similar to our other |
||
|
|
||
| db.session.commit() | ||
|
|
||
| return { | ||
| "goal": { | ||
| "id": goal.id, | ||
| "title": goal.title, | ||
| } | ||
| }, 200 | ||
|
|
||
| @goals_bp.delete("<goal_id>") | ||
| def delete_task(goal_id): | ||
| goal = validate_goal(goal_id) | ||
| db.session.delete(goal) | ||
| db.session.commit() | ||
|
|
||
| return {"details":f'Goal {goal_id} "{goal.title}" successfully deleted'},200 | ||
| # Response(status=204, mimetype="application/json") | ||
|
|
||
| def validate_goal(goal_id): | ||
| try: | ||
| goal_id = int(goal_id) | ||
| except ValueError: | ||
| response = {"message": f"goal {goal_id} invalid"} | ||
| abort(make_response(response,400)) | ||
|
|
||
| query = db.select(Goal).where(Goal.id == goal_id) | ||
| goal = db.session.scalar(query) | ||
|
|
||
| if not goal: | ||
| response = {"message": f"goal {goal_id} not found"} | ||
| abort(make_response(response, 404)) | ||
|
|
||
| return goal | ||
|
Comment on lines
+105
to
+119
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. I see we have a |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| from flask import abort, make_response | ||
| from ..db import db | ||
|
|
||
| def validate_model(cls, model_id): | ||
| try: | ||
| model_id = int(model_id) | ||
| except: | ||
| 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 | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,173 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint, abort, make_response, request, Response | ||
| from app.models.task import Task | ||
| from ..db import db | ||
| from datetime import datetime, timezone | ||
|
|
||
| tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") | ||
|
|
||
|
|
||
| @tasks_bp.post("") | ||
| def create_task(): | ||
| request_body = request.get_json() | ||
|
|
||
| #validate request data | ||
| if "title" not in request_body or "description" not in request_body: | ||
| response = {"details": "Invalid data"} | ||
| return make_response(response,400) | ||
|
|
||
| title = request_body["title"] | ||
| description = request_body["description"] | ||
| completed_at = request_body.get("completed_at") | ||
|
|
||
| new_task = Task(title=title, description=description, completed_at=completed_at) | ||
| db.session.add(new_task) | ||
| db.session.commit() | ||
|
|
||
| response = { | ||
| "task":{ | ||
| "id": new_task.id, | ||
| "title": new_task.title, | ||
| "description": new_task.description, | ||
| "is_complete": bool(new_task.completed_at) | ||
| } | ||
| } | ||
| return response, 201 | ||
|
|
||
| @tasks_bp.get('/<task_id>') | ||
| def get_one_task(task_id): | ||
| task = validate_task(task_id) | ||
|
|
||
| return { | ||
| "task":{ | ||
| "id": task.id, | ||
| "title": task.title, | ||
| "description": task.description, | ||
| "is_complete": bool(task.completed_at) | ||
| } | ||
| } | ||
|
|
||
| @tasks_bp.get("") | ||
| def get_all_tasks(): | ||
|
|
||
| sort_param = request.args.get("sort", "").lower() | ||
|
|
||
| if sort_param and sort_param.lower() == "asc": | ||
| query = db.select(Task).order_by(Task.title) | ||
|
|
||
| elif sort_param and sort_param.lower() == "desc": | ||
| query = db.select(Task).order_by(Task.title.desc()) | ||
| else: | ||
| query = db.select(Task).order_by(Task.title) | ||
|
|
||
| tasks = db.session.scalars(query) | ||
|
Comment on lines
+52
to
+62
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. Remember in class we wrote a function called |
||
|
|
||
|
|
||
| tasks_response = [] | ||
| for task in tasks: | ||
| tasks_response.append( | ||
| { | ||
| "id": task.id, | ||
| "title": task.title, | ||
| "description": task.description, | ||
| "is_complete": bool(task.completed_at) | ||
|
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. ✨ |
||
| } | ||
| ) | ||
| return tasks_response | ||
|
|
||
| @tasks_bp.put("/<task_id>") | ||
| def update_task(task_id): | ||
| task = validate_task(task_id) | ||
| request_body = request.get_json() | ||
|
|
||
| # Validate request data | ||
| if "title" not in request_body or "description" not in request_body: | ||
| return make_response({"details": "Invalid data"}, 400) | ||
|
|
||
| task.title = request_body["title"] | ||
| task.description = request_body["description"] | ||
|
|
||
| # Keep completed_at unchanged if not provided | ||
| if "completed_at" in request_body: | ||
| task.completed_at = request_body["completed_at"] | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return { | ||
| "task": { | ||
| "id": task.id, | ||
| "title": task.title, | ||
| "description": task.description, | ||
| "is_complete": bool(task.completed_at) # Convert completed_at to boolean | ||
| } | ||
| }, 200 | ||
|
Comment on lines
+95
to
+102
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. Is there a reason we don't use our |
||
|
|
||
| @tasks_bp.delete("<task_id>") | ||
| def delete_task(task_id): | ||
| task = validate_task(task_id) | ||
| db.session.delete(task) | ||
| db.session.commit() | ||
|
|
||
| return {"details":f'Task {task_id} "{task.title}" successfully deleted'},200 | ||
| # Response(status=204, mimetype="application/json") | ||
|
|
||
| def validate_task(task_id): | ||
| try: | ||
| task_id = int(task_id) | ||
| except ValueError: | ||
| response = {"message": f"task {task_id} invalid"} | ||
| abort(make_response(response,400)) | ||
|
|
||
| query = db.select(Task).where(Task.id == task_id) | ||
| task = db.session.scalar(query) | ||
|
|
||
| if not task: | ||
| response = {"message": f"task {task_id} not found"} | ||
| abort(make_response(response, 404)) | ||
|
|
||
| return task | ||
|
|
||
| @tasks_bp.patch("/<task_id>/mark_complete") | ||
| def mark_complete(task_id): | ||
| task = validate_task(task_id) | ||
|
|
||
| # Update completed_at to mark the task as completed | ||
| task.completed_at = datetime.now(timezone.utc) | ||
|
|
||
| # Commit the changes to the database | ||
| db.session.commit() | ||
|
|
||
| # Prepare the response | ||
| response = { | ||
| "task": { | ||
| "id": task.id, | ||
| "title": task.title, | ||
| "description": task.description, | ||
| "is_complete": bool(task.completed_at) | ||
| } | ||
| } | ||
| return response, 200 | ||
|
Comment on lines
+129
to
+148
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. Looks like you didn't do the slack implementation for this route. |
||
|
|
||
| @tasks_bp.patch("/<task_id>/mark_incomplete") | ||
| def mark_incomplete(task_id): | ||
| task = validate_task(task_id) | ||
|
|
||
| # Update completed_at to mark the task as completed | ||
| task.completed_at = None | ||
|
|
||
| # Commit the changes to the database | ||
| db.session.commit() | ||
|
|
||
| # Prepare the response | ||
| response = { | ||
| "task": { | ||
| "id": task.id, | ||
| "title": task.title, | ||
| "description": task.description, | ||
| "is_complete": bool(task.completed_at) | ||
| } | ||
| } | ||
| return response, 200 | ||
|
|
||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Single-database configuration for Flask. |
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.
Don't forget that the convention for naming blueprints is to name them
bp. With each blueprint being named the same thing we will need to import them under an alias like so: