-
Notifications
You must be signed in to change notification settings - Fork 43
Dragonfly - Lina Martinez #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
a7bff98
ab1e16e
53e3252
8dd614d
8a71103
a9ab640
59c898d
dc54503
66b0112
cbf6b29
5dd7e02
e6fb97c
6d12bbe
fdf2185
32f4cdb
6e9ce52
57b6fe3
6569f97
4c8e36c
9eb0475
b8b0787
1c3ccbe
a6bea76
860d4c3
615c005
6fdd934
fcabbad
6a8b803
2bfe64c
beb1fdf
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,32 @@ | ||||||
| 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 | ||||||
|
|
||||||
|
|
||||||
| 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, | ||||||
| } | ||||||
|
|
||||||
| if self.tasks: | ||||||
| goal_dict["tasks"] = [task.to_dict() for task in self.tasks] | ||||||
|
|
||||||
| return goal_dict | ||||||
|
|
||||||
|
|
||||||
| @classmethod | ||||||
| def from_dict(cls, goal_data): | ||||||
| new_goal= cls(title=goal_data["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. Nitpick: spacing
Suggested change
|
||||||
|
|
||||||
| return new_goal | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,43 @@ | ||||||
| from sqlalchemy.orm import Mapped, mapped_column | ||||||
| from sqlalchemy import ForeignKey | ||||||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||||||
| from ..db import db | ||||||
| from datetime import datetime | ||||||
| from typing import Optional | ||||||
|
|
||||||
|
|
||||||
| from typing import TYPE_CHECKING | ||||||
| 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]] = 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. Using
Suggested change
Here's the SQLAlchemy documentation about nullability
|
||||||
| 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, | ||||||
| "description": self.description, | ||||||
| "title": self.title, | ||||||
| "is_complete": self.completed_at is not None, | ||||||
| } | ||||||
|
|
||||||
| if self.goal_id: | ||||||
| task_dict["goal_id"] = self.goal_id | ||||||
|
|
||||||
| return task_dict | ||||||
|
|
||||||
|
|
||||||
| @classmethod | ||||||
| def from_dict(cls, task_data): | ||||||
| new_task = cls(title=task_data["title"], | ||||||
| description=task_data["description"], | ||||||
| completed_at=task_data.get("completed_at", None) | ||||||
| ) | ||||||
|
|
||||||
| return new_task | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1 +1,89 @@ | ||||||||||||||||||||||||||||
| from flask import Blueprint | ||||||||||||||||||||||||||||
| from flask import Blueprint, request | ||||||||||||||||||||||||||||
| from app.models.goal import Goal | ||||||||||||||||||||||||||||
| from app.models.task import Task | ||||||||||||||||||||||||||||
| from app.routes.route_utilities import create_model, get_models_with_filters, validate_model, update_model, delete_model, validate_multiple_models | ||||||||||||||||||||||||||||
| from ..db import db | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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(): | ||||||||||||||||||||||||||||
| return get_models_with_filters(Goal, request.args) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @bp.get("/<goal_id>") | ||||||||||||||||||||||||||||
| def get_one_goal(goal_id): | ||||||||||||||||||||||||||||
| goal = validate_model(Goal, goal_id) | ||||||||||||||||||||||||||||
| return {"goal": goal.to_dict()} | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @bp.put("/<goal_id>") | ||||||||||||||||||||||||||||
| def update_goal(goal_id): | ||||||||||||||||||||||||||||
| goal = validate_model(Goal, goal_id) | ||||||||||||||||||||||||||||
| request_body = request.get_json() | ||||||||||||||||||||||||||||
| return update_model(goal, request_body) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @bp.delete("/<goal_id>") | ||||||||||||||||||||||||||||
| def delete_goal(goal_id): | ||||||||||||||||||||||||||||
| goal = validate_model(Goal, goal_id) | ||||||||||||||||||||||||||||
| return delete_model(goal) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Nested Routes: One goal(parent) to many tasks(child) | ||||||||||||||||||||||||||||
| @bp.get("/<goal_id>/tasks") | ||||||||||||||||||||||||||||
| def get_tasks_by_goal(goal_id): | ||||||||||||||||||||||||||||
| goal = validate_model(Goal, goal_id) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return dict( | ||||||||||||||||||||||||||||
| id = goal.id, | ||||||||||||||||||||||||||||
| title = goal.title, | ||||||||||||||||||||||||||||
| tasks = [task.to_dict() for task in goal.tasks] | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+50
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. Could we use the
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @bp.post("/<goal_id>/tasks") | ||||||||||||||||||||||||||||
| def assign_tasks_to_goal(goal_id): | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
| Associates the given task IDs with a goal by its ID, and unlinks any previously associated tasks not in the list. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Validates all task IDs and updates the database accordingly. Returns the goal ID and the updated list of task IDs. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Request: { "task_ids": [1, 2, 3] } | ||||||||||||||||||||||||||||
| Response: { "id": 1, "task_ids": [1, 2, 3] } | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| goal = validate_model(Goal, goal_id) | ||||||||||||||||||||||||||||
| request_body = request.get_json() | ||||||||||||||||||||||||||||
| task_ids = request_body.get("task_ids", []) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Validate list taks ids in one query to db | ||||||||||||||||||||||||||||
| valid_tasks = validate_multiple_models(Task, task_ids) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Unlink tasks that are no longer assigned to this goal | ||||||||||||||||||||||||||||
| for task_in_goal in goal.tasks: | ||||||||||||||||||||||||||||
| if task_in_goal.id not in task_ids: | ||||||||||||||||||||||||||||
| task_in_goal.goal_id = None | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| valid_tasks_ids = [] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Assign valid tasks to this goal | ||||||||||||||||||||||||||||
| for task in valid_tasks: | ||||||||||||||||||||||||||||
| valid_tasks_ids.append(task.id) | ||||||||||||||||||||||||||||
| if task.goal_id != goal.id: | ||||||||||||||||||||||||||||
| task.goal_id = goal.id | ||||||||||||||||||||||||||||
|
Comment on lines
+72
to
+82
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. What if we don't need to unlink the tasks because we can just replace them?
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| db.session.commit() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||
| "id": goal.id, | ||||||||||||||||||||||||||||
| "task_ids": valid_tasks_ids | ||||||||||||||||||||||||||||
|
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.
Suggested change
|
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||
| from flask import abort, make_response, Response | ||||||
| from ..db import db | ||||||
|
|
||||||
|
|
||||||
| def validate_model(cls, model_id): | ||||||
| try: | ||||||
| model_id = int(model_id) | ||||||
| except: | ||||||
| response = {"message": f"{cls.__name__} id{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": "Invalid data"} | ||||||
|
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. It might be nice to provide even more details in the response you send back so that the client knows how to fix up the request body they will re-send.
Suggested change
For example, this would evaluate to "title" for example if I sent a bad request body to create a Task. |
||||||
| abort(make_response(response, 400)) | ||||||
|
|
||||||
| db.session.add(new_model) | ||||||
| db.session.commit() | ||||||
|
|
||||||
| return {cls.__name__.lower(): new_model.to_dict()}, 201 | ||||||
|
|
||||||
|
|
||||||
| def get_models_with_filters(cls, filters=None): | ||||||
| query = db.select(cls) | ||||||
| sort = None | ||||||
|
|
||||||
| if filters: | ||||||
| for attribute, value in filters.items(): | ||||||
| if attribute == "sort": | ||||||
| sort = value | ||||||
| continue | ||||||
|
|
||||||
| if hasattr(cls, attribute): | ||||||
| query = query.where(getattr(cls, attribute).ilike(f"%{value}%")) | ||||||
|
|
||||||
| if sort == "asc": | ||||||
| query = query.order_by(cls.title.asc()) | ||||||
| elif sort == "desc": | ||||||
| query = query.order_by(cls.title.desc()) | ||||||
| else: | ||||||
| query = query.order_by(cls.id) | ||||||
|
|
||||||
| models = db.session.scalars(query) | ||||||
|
|
||||||
| return [model.to_dict() for model in models] | ||||||
|
|
||||||
|
|
||||||
| def update_model(model, model_data): | ||||||
| for attribute, value in model_data.items(): | ||||||
| setattr(model, attribute, value) | ||||||
|
|
||||||
| db.session.commit() | ||||||
|
|
||||||
| return Response(status=204, mimetype="application/json") | ||||||
|
|
||||||
|
|
||||||
| def delete_model(model): | ||||||
| db.session.delete(model) | ||||||
| db.session.commit() | ||||||
|
|
||||||
| return Response(status=204, mimetype="application/json") | ||||||
|
|
||||||
|
|
||||||
| def validate_multiple_models(cls, id_list): | ||||||
|
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 what you're going for here! I think this helper function works, but is unnecessary because we can use the For example: for task_id in request_body['task_ids']:
task = validate_model(Task, task_id) |
||||||
| """ | ||||||
| Validates that all IDs in the given list exist for the specified model class in one. | ||||||
|
|
||||||
| Parameters: | ||||||
| - cls: model class | ||||||
| - id_list: list of model ids | ||||||
|
|
||||||
| Returns: | ||||||
| - A list of model instances matching the given IDs. | ||||||
|
|
||||||
| Aborts: | ||||||
| - 400 if any ID is not a valid integer. | ||||||
| - 404 if one or more IDs are not found in the database. | ||||||
| """ | ||||||
|
|
||||||
| try: | ||||||
| id_list = [int(id) for id in id_list] | ||||||
| except: | ||||||
| response = {"message": f"One or more {cls.__name__} IDs are not valid integers" } | ||||||
| abort(make_response(response, 400)) | ||||||
|
|
||||||
| # Query for all models matching the given IDs | ||||||
| models = db.session.scalars(db.select(cls).where(cls.id.in_(id_list))).all() | ||||||
|
|
||||||
| # Query for all models matching the given IDs | ||||||
| if len(models) != len(set(id_list)): | ||||||
| found_ids = {model.id for model in models} | ||||||
| missing_ids = set(id_list) - found_ids | ||||||
|
|
||||||
| response = {"message": f"{cls.__name__} IDs not found: {missing_ids}"} | ||||||
| abort(make_response(response, 404)) | ||||||
|
|
||||||
| return models | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1 +1,84 @@ | ||||||||||||
| from flask import Blueprint | ||||||||||||
| from datetime import datetime | ||||||||||||
| import os | ||||||||||||
| import requests # HTTP library to make a request to Slack API | ||||||||||||
| from flask import Blueprint, Response, request | ||||||||||||
| from app.models.task import Task | ||||||||||||
| from app.routes.route_utilities import validate_model, get_models_with_filters, create_model, update_model, delete_model | ||||||||||||
| from ..db import db | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| bp = Blueprint("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 gets_one_task(task_id): | ||||||||||||
| task = validate_model(Task, task_id) | ||||||||||||
|
|
||||||||||||
| return {"task": task.to_dict()} | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @bp.put("/<task_id>") | ||||||||||||
| def update_task(task_id): | ||||||||||||
| task = validate_model(Task, task_id) | ||||||||||||
| request_body = request.get_json() | ||||||||||||
|
|
||||||||||||
| return update_model(task, request_body) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @bp.delete("/<task_id>") | ||||||||||||
| def delete_task(task_id): | ||||||||||||
| task = validate_model(Task, task_id) | ||||||||||||
|
|
||||||||||||
| return delete_model(task) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @bp.patch("/<task_id>/mark_complete") | ||||||||||||
| def mark_task_complete(task_id): | ||||||||||||
| """ | ||||||||||||
| Marks a task as complete and sends a Slack notification. | ||||||||||||
|
|
||||||||||||
| If the task is not already completed, sets the `completed_at` timestamp and posts a message to Slack. | ||||||||||||
| Returns a 204 No Content response. | ||||||||||||
| """ | ||||||||||||
| task = validate_model(Task, task_id) | ||||||||||||
|
|
||||||||||||
| path = "https://slack.com/api/chat.postMessage" | ||||||||||||
|
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 string is a constant value so we should name the variable with all capital letters.
Suggested change
|
||||||||||||
| API_KEY = os.environ.get("API_KEY") | ||||||||||||
| headers = {"Authorization": f"Bearer {API_KEY}"} | ||||||||||||
| body ={ | ||||||||||||
| "channel": "task-notifications", | ||||||||||||
|
Comment on lines
+60
to
+61
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. Nitpick: spacing Also prefer the channel to be referenced by a constant variable too.
Suggested change
|
||||||||||||
| "text": f"Someone just completed the task {task.title}" | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if not task.completed_at: | ||||||||||||
| task.completed_at = datetime.now() | ||||||||||||
| slack_post = requests.post(path, headers=headers, json=body ) | ||||||||||||
|
Comment on lines
+57
to
+67
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. Prefer all the logic related to calling Slack to be in a helper function (maybe like 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. Nitpick: spacing
Suggested change
|
||||||||||||
|
|
||||||||||||
| db.session.commit() | ||||||||||||
|
|
||||||||||||
| return Response(status=204, mimetype="application/json") | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @bp.patch("/<task_id>/mark_incomplete") | ||||||||||||
| def mark_task_incomplete(task_id): | ||||||||||||
| task = validate_model(Task, task_id) | ||||||||||||
|
|
||||||||||||
| if task.completed_at: | ||||||||||||
| task.completed_at = None | ||||||||||||
|
|
||||||||||||
| db.session.commit() | ||||||||||||
|
|
||||||||||||
| return Response(status=204, mimetype="application/json") | ||||||||||||
|
|
||||||||||||
| 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.
👍