-
Couldn't load subscription status.
- Fork 44
Bumblebee - Gitika K #34
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
53a2029
f561d26
e2f8b41
4234fd6
35e23e4
72e2dd0
18345ef
93db443
c712b7b
e7bc872
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,20 @@ | ||
| from sqlalchemy.orm import Mapped, mapped_column | ||
| from ..db import db | ||
| from sqlalchemy.orm import relationship | ||
|
|
||
| class Goal(db.Model): | ||
|
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. 👍 Your Goal implementation is consistent with your Task model. Check the Task feedback for anything that could apply here. |
||
| 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 | ||
| } | ||
|
|
||
| @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,34 @@ | ||
| from sqlalchemy.orm import Mapped, mapped_column | ||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
| from ..db import db | ||
| from typing import Optional | ||
| from datetime import datetime | ||
| 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) | ||
| goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"), nullable=True) | ||
|
Comment on lines
+11
to
+12
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 the |
||
| goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks") | ||
|
|
||
| def to_dict(self): | ||
| task_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. 👍 Not including the outer task "envelope" wrapper in our |
||
| "id": self.id, | ||
| "title": self.title, | ||
| "description": self.description, | ||
| "is_complete": self.completed_at is not None, | ||
|
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. Even though we only use the calculated |
||
| } | ||
|
|
||
| if self.goal_id is not None: | ||
| task_dict["goal_id"] = self.goal_id | ||
|
Comment on lines
+23
to
+24
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 logic to add the goal data to the task dictionary only when it's present. |
||
|
|
||
| return task_dict | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, task_data): | ||
| return cls( | ||
| title=task_data["title"], # this will raise KeyError if missing | ||
| description=task_data["description"], | ||
|
Comment on lines
+31
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. 👍 By reading title and description directly as keys we can trigger a |
||
| completed_at=task_data.get("completed_at") # this will not raise KeyError if missing | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,114 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint,make_response, abort, request, Response | ||
| from ..models.goal import Goal | ||
| from ..models.task import Task | ||
| from app.routes.route_utilities import validate_model | ||
| from app import db | ||
|
|
||
| goal_bp = Blueprint("goal", __name__, url_prefix="/goals") | ||
|
|
||
| @goal_bp.post("") | ||
| def create_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. 👍 Your Goal CRUD routes are consistent with your Task CRUD routes. Check the Task feedback for anything that could apply here. |
||
|
|
||
| request_body = request.get_json() | ||
|
|
||
| try: | ||
| new_goal = Goal.from_dict(request_body) | ||
| except KeyError as error: | ||
| message = { | ||
| # "message": f"Missing '{error.args[0]}' attribute" | ||
| "details": "Invalid data" | ||
| } | ||
| abort(make_response(message, 400)) | ||
| db.session.add(new_goal) | ||
| db.session.commit() | ||
|
|
||
| response = {"goal": new_goal.to_dict()} | ||
| return response, 201 | ||
|
|
||
| @goal_bp.get("") | ||
| def get_all_goals(): | ||
| query = db.select(Goal) | ||
| goals = db.session.scalars(query) #This executes the SQL query | ||
|
|
||
| goals_response = [] | ||
| for goal in goals: | ||
| goals_response.append(goal.to_dict()) | ||
| return goals_response | ||
|
|
||
| @goal_bp.get("/<goal_id>") | ||
| def get_one_goal(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
| return {"goal": goal.to_dict()} | ||
|
|
||
| @goal_bp.put("/<goal_id>") | ||
| def update_goal(goal_id): | ||
|
|
||
| goal = validate_model(Goal,goal_id) | ||
|
|
||
| request_body = request.get_json() | ||
|
|
||
| try: | ||
| goal.title = request_body["title"] | ||
| except KeyError as error: | ||
| message = { | ||
| "message": f"Missing '{error.args[0]}' attribute" | ||
| } | ||
| abort(make_response(message, 400)) | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @goal_bp.delete("/<goal_id>") | ||
| def remove_task(goal_id): | ||
|
|
||
| goal = validate_model(Goal,goal_id) | ||
|
|
||
| db.session.delete(goal) | ||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @goal_bp.post("/<goal_id>/tasks") | ||
| def assign_tasks_to_goal(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
|
|
||
| request_body = request.get_json() | ||
| task_ids = request_body.get("task_ids", []) | ||
|
|
||
| # Clear existing task associations first | ||
| for task in goal.tasks: | ||
| task.goal_id = None | ||
|
|
||
| tasks = [] | ||
| for task_id in task_ids: | ||
| task = validate_model(Task, task_id) | ||
| task.goal_id = goal.id | ||
| tasks.append(task) | ||
|
Comment on lines
+79
to
+87
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. Once we have a list of validated goal.tasks = taskswhich avoids the need to manually clear the goal association from the existing tasks. |
||
|
|
||
| db.session.commit() | ||
|
|
||
| return { | ||
| "id": goal.id, | ||
| "task_ids": [task.id for task in tasks] | ||
| }, 200 | ||
|
|
||
| @goal_bp.get("/<goal_id>/tasks") | ||
| def get_tasks_of_one_goal(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
|
|
||
| task_list = [task.to_dict() for task in goal.tasks] | ||
| tasks_response = [] | ||
| for task in goal.tasks: | ||
| task_dict = task.to_dict() | ||
| task_dict["goal_id"] = goal.id # add goal_id if not in to_dict | ||
| tasks_response.append(task_dict) | ||
|
Comment on lines
+101
to
+105
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. Your Task |
||
|
|
||
| return { | ||
| "id": goal.id, | ||
| "title": goal.title, | ||
|
Comment on lines
+108
to
+109
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. Notice that the id and title keys here are the same as for a regular Goal GET, for which we wrote the |
||
| "tasks": tasks_response | ||
|
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. The |
||
| }, 200 | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| from flask import abort, make_response | ||
| from app import db | ||
|
|
||
| def validate_model(cls, model_id): | ||
| try: | ||
| model_id = int(model_id) | ||
| except ValueError: | ||
| response = {"message" : f"id {model_id} invalid"} | ||
| abort(make_response(response, 400)) | ||
|
|
||
| query = db.select(cls).where(cls.id == model_id) | ||
| instance = db.session.scalar(query) | ||
|
|
||
| if instance is None: | ||
| abort(make_response({"message": f"{cls.__name__} ID ({model_id}) not found."}, 404)) | ||
| return instance | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,139 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint,make_response, abort, request, Response | ||
| from ..models.task import Task | ||
| from app.routes.route_utilities import validate_model | ||
| from app import db | ||
| from sqlalchemy import asc, desc | ||
| from datetime import datetime, timezone | ||
| import os | ||
| import requests | ||
|
|
||
| task_bp = Blueprint("task", __name__, url_prefix="/tasks") | ||
|
|
||
| @task_bp.post("") | ||
| def create_task(): | ||
|
|
||
| request_body = request.get_json() | ||
|
|
||
| try: | ||
| new_task = Task.from_dict(request_body) | ||
| except KeyError as error: | ||
| message = { | ||
| # "message": f"Missing '{error.args[0]}' attribute" | ||
| "details": "Invalid data" | ||
| } | ||
| abort(make_response(message, 400)) | ||
| db.session.add(new_task) | ||
| db.session.commit() | ||
|
|
||
| response = {"task": new_task.to_dict()} | ||
| return response, 201 | ||
|
Comment on lines
+17
to
+29
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 could have adapted |
||
|
|
||
| @task_bp.get("") | ||
| def get_all_tasks(): | ||
| sort_param = request.args.get("sort") | ||
|
|
||
| query = db.select(Task) | ||
| if sort_param == "asc": | ||
| query = query.order_by(asc(Task.title)) | ||
| elif sort_param == "desc": | ||
| query = query.order_by(desc(Task.title)) | ||
|
Comment on lines
+35
to
+39
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 gradual build-up of the 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 no sort is specified, it's still a good idea to sort on the record ids, as this will give us a consistent ordering if records are modified. Otherwise, records will be returned in the internal ordering used by a table, which could change from request to request. |
||
|
|
||
| tasks = db.session.scalars(query) #This executes the SQL query | ||
|
|
||
| tasks_response = [] | ||
| for task in tasks: | ||
| tasks_response.append(task.to_dict()) | ||
|
Comment on lines
+35
to
+45
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. Rather than coding this sort logic directly into the route, we could take an approach similar to
Comment on lines
+43
to
+45
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 pattern (empty result, iterate, process each record, append) can be expressed cleanly in a list comprehension. tasks_response = [task.to_dict() for task in tasks] |
||
| return tasks_response | ||
|
|
||
| @task_bp.get("/<task_id>") | ||
| def get_one_task(task_id): | ||
| task = validate_model(Task,task_id) | ||
| # return task.to_dict() | ||
| return {"task": task.to_dict()} | ||
|
|
||
|
|
||
| # 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)) | ||
|
|
||
| # # task = Task.query.get(task_id) | ||
| # query = db.select(Task).where(Task.id == task_id) | ||
| # task = db.session.scalar(query) | ||
| # # db.session.get(Task, task_id) | ||
|
|
||
| # if task is None: | ||
| # message = { | ||
| # "message": f"task ID ({task_id}) not found." | ||
| # } | ||
| # abort(make_response(message, 404)) | ||
| # return task | ||
|
|
||
| @task_bp.put("/<task_id>") | ||
| def update_task(task_id): | ||
|
|
||
| task = validate_model(Task,task_id) | ||
|
|
||
| request_body = request.get_json() | ||
|
|
||
| try: | ||
| task.title = request_body["title"] | ||
| task.description = request_body["description"] | ||
| except KeyError as error: | ||
| message = { | ||
| "message": f"Missing '{error.args[0]}' attribute" | ||
| } | ||
| abort(make_response(message, 400)) | ||
|
|
||
| db.session.commit() | ||
|
Comment on lines
+81
to
+90
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. Notice how similar updating is to creating. After validating the record to update, we require certain keys to be present, others can be optional, then after updating and committing the model, we return a common response. How could we add model or route helpers to simplify our PUT route? |
||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @task_bp.delete("/<task_id>") | ||
| def remove_task(task_id): | ||
|
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. Notice how similar deleting is to getting. After validating the record to delete, we delete and commit it, then return a common response. How could we add model or route helpers to simplify our DELETE route? |
||
|
|
||
| task = validate_model(Task,task_id) | ||
|
|
||
| db.session.delete(task) | ||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| def send_slack_message(task_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. This function encapsulates the responsibility of sending a completion notification about the provided task title. Notice that we could make a further helper function that wraps the responsibility of sending a general message to a specified channel. This function would then be responsible for the logic of building the messaging, and knowing what channel to use. Even the logic to build the notification message based on the task could be in its own helper. Think about whether such a potential function would be a model method, or some other method to which we pass a Task. |
||
| slack_token = os.environ.get("SLACK_BOT_TOKEN") | ||
|
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 |
||
| channel = os.environ.get("SLACK_CHANNEL", "task-notifications") | ||
|
|
||
| url = "https://slack.com/api/chat.postMessage" | ||
| headers = { | ||
| "Authorization": f"Bearer {slack_token}", | ||
|
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. 👍 The Slack API documentation prefers passing the API key in the header rather than in the request body. Since we're passing the body as JSON (by using the |
||
| "Content-Type": "application/json" | ||
| } | ||
| payload = { | ||
| "channel": f"#{channel}", | ||
| "text": f"Someone just completed the task {task_title}" | ||
| } | ||
|
|
||
| requests.post(url, json=payload, headers=headers) | ||
|
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. 👍 Here, we send the request and then move on with our logic without checking the result from Slack. This is fine here, since sending the message is just a side effect of completing the task. For example, it's unlikely we'd want to fail to complete a task in the event that Slack wasn't reachable. In a fuller application, we might write out the result of this call to a log file so that if a problem with our calls to Slack does occur, we'd have a record to investigate. |
||
|
|
||
| @task_bp.patch("/<task_id>/mark_complete") | ||
| def mark_complete(task_id): | ||
| task = validate_model(Task,task_id) | ||
|
|
||
| task.completed_at = datetime.now(timezone.utc) | ||
| db.session.commit() | ||
|
Comment on lines
+122
to
+125
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 route to mark the task complete. We can use our validation helper to get the same behavior as the other id-based routes, leaving our route responsible only for updating the record with the current datetime, saving it, and generating the response. One thing we might still consider is moving the actual update logic into a model helper so that the Task model class itself is responsible for "knowing" how to complete a Task. |
||
|
|
||
| send_slack_message(task.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. 👍 Nice use of a helper function to hold the logic for performing the notification. |
||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @task_bp.patch("/<task_id>/mark_incomplete") | ||
| def mark_incomplete(task_id): | ||
| task = validate_model(Task,task_id) | ||
|
|
||
| task.completed_at = None | ||
| db.session.commit() | ||
|
Comment on lines
+133
to
+136
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 route to mark the task incomplete. We can use our validation helper to get the same behavior as the other id-based routes, leaving our route responsible only for clearing the completion date, saving it, and generating the response. One thing we might still consider is moving the actual update logic into a model helper so that the Task model class itself is responsible for "knowing" how to mark a Task incomplete. |
||
|
|
||
| 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.
It should not be necessary to add this explicit call to
load_dotenvhere. In all the places where we expect Flask to locate these settings in the.envfile, Flask will do so properly (for instance, we don't need this call in deployment, since we load the values directly in the deployed environment).