-
Notifications
You must be signed in to change notification settings - Fork 36
Crow- Helen L. #41
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?
Crow- Helen L. #41
Changes from all commits
6e4ec1d
3f0d82c
0d42eec
ceaeafb
8e6cb8c
44b7941
7267e60
ae5b427
48c2591
af7b120
5a83cb7
71082e4
0111144
6613431
11f2925
f7fc77a
726de20
74e9ba1
b443e0b
d3563ab
f8d3c46
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,24 @@ | ||
| 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 | ||
|
Comment on lines
+3
to
+6
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. While TYPE_CHECKING isn't necessarily required here because we have a one-to-many relationship, it is best practice and is necessary to avoid circular imports in a many-to-many relationship, so great job using it here. Also, as stated in our Learn lessons, it does allow us to get rid of the yellow underline on line 11! |
||
|
|
||
| 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, | ||
| } | ||
| return goal_dict | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, data): | ||
|
|
||
| new_goal = cls(title=data["title"]) | ||
| return new_goal | ||
|
Comment on lines
+13
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. Your |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,37 @@ | ||||||||||||||||||||||||||||
| 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, TYPE_CHECKING | ||||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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]] | ||||||||||||||||||||||||||||
| goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id")) | ||||||||||||||||||||||||||||
| goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks") | ||||||||||||||||||||||||||||
|
Comment on lines
11
to
+16
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. Just for clarity's sake, it is often a good idea to move any relationships/columns associated with the relationships to the bottom of the attributes list here like you have done! One small common pattern that you will often see is something like the following:
Suggested change
|
||||||||||||||||||||||||||||
| def to_dict(self): | ||||||||||||||||||||||||||||
| task_dict = { | ||||||||||||||||||||||||||||
| "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. This looks great! |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if self.goal: | ||||||||||||||||||||||||||||
| task_dict["goal_id"] = self.goal.id | ||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+25
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 job adding the goal id to the dictionary if it exists! |
||||||||||||||||||||||||||||
| return task_dict | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||
| def from_dict(cls, data): | ||||||||||||||||||||||||||||
| goal_id = data.get("goal_id") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| new_task = cls( | ||||||||||||||||||||||||||||
| title=data["title"], | ||||||||||||||||||||||||||||
| description=data["description"], | ||||||||||||||||||||||||||||
| goal_id=goal_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. Slight nitpick, but feel free to move the ending parenthesis to the next line! |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return new_task | ||||||||||||||||||||||||||||
|
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. A couple things about this particular file!
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| from flask import request | ||
| from app.db import db | ||
| from .route_utilities import validate_model, create_model as util_create_model | ||
|
|
||
| def create_model_endpoint(model_cls, required_keys=("title",)): | ||
| body = request.get_json() or {} | ||
| return util_create_model(model_cls, body) | ||
|
|
||
| def list_models_endpoint(model_cls): | ||
| return [m.to_dict() for m in model_cls.query.all()] | ||
|
|
||
| def get_one_endpoint(model_cls, model_id): | ||
| instance = validate_model(model_cls, model_id) | ||
| return instance.to_dict(), 200 | ||
|
|
||
| def update_fields_endpoint(instance, fields): | ||
| body = request.get_json() or {} | ||
| for f in fields: | ||
| if f in body: | ||
| setattr(instance, f, body[f]) | ||
| db.session.commit() | ||
| return instance.to_dict(), 200 | ||
|
|
||
| def delete_model_endpoint(instance, success_msg_template): | ||
| db.session.delete(instance) | ||
| db.session.commit() | ||
| return {"details": success_msg_template.format(id=instance.id, title=getattr(instance, "title", ""))}, 200 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,80 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint, request, Response | ||
| from app.models.goal import Goal | ||
| from app.models.task import Task | ||
| from ..db import db | ||
| from .route_utilities import validate_model, create_model, get_models_with_filters | ||
|
|
||
| bp = Blueprint("goals_bp", __name__, url_prefix="/goals") | ||
|
|
||
| @bp.post("") | ||
| def create_goal(): | ||
| request_body = request.get_json() | ||
|
|
||
| return create_model(Goal, request_body) | ||
|
Comment on lines
+10
to
+13
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. Perfect! |
||
|
|
||
| @bp.post("/<goal_id>/tasks") | ||
| def create_task_with_goal(goal_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. This is a small tweak, but the name |
||
|
|
||
| goal = validate_model(Goal, goal_id) | ||
|
|
||
| request_body = request.get_json() | ||
| task_ids = request_body["task_ids"] | ||
|
|
||
| task_list = [] | ||
| for id in task_ids: | ||
| task = validate_model(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. Nice use of the |
||
| task_list.append(task) | ||
|
|
||
| goal.tasks = task_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. What I really love about this particular approach is that rather than have to zero out the tasks already associated with a goal, you simply create the new task_list and then update the goal.tasks to match that list! Well done! |
||
| db.session.commit() | ||
|
|
||
| return { | ||
|
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 that Flask's default return status code for a view function is 200. As a result, we don't need to explicitly return it! |
||
| "id": goal.id, | ||
| "task_ids": task_ids | ||
| }, 200 | ||
|
|
||
|
|
||
| @bp.get("/<goal_id>/tasks") | ||
| def get_tasks_by_goal(goal_id): | ||
|
|
||
| goal = validate_model(Goal, goal_id) | ||
| tasks = [task.to_dict() for task in goal.tasks] | ||
|
|
||
| return { | ||
| "id": goal.id, | ||
| "title": goal.title, | ||
| "tasks": tasks | ||
|
Comment on lines
+41
to
+46
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 wonder if there is a way for us to include the tasks as part of the |
||
| }, 200 | ||
|
|
||
|
|
||
| @bp.get("") | ||
| def get_all_goals(): | ||
|
|
||
| return get_models_with_filters(Goal, request.args) | ||
|
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 helper function usage! |
||
|
|
||
| @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() | ||
|
|
||
| goal.title = request_body["title"] | ||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
|
|
||
| @bp.delete("<goal_id>") | ||
| def delete_model(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
|
|
||
| db.session.delete(goal) | ||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/jason") | ||
|
Comment on lines
+55
to
+80
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 rest of these view functions look really good! Great job!! |
||
|
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 set of helper functions looks practically perfect! Well done, Helen! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| from flask import abort, make_response, request | ||
| 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} 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": f"Invalid data"} | ||
| abort(make_response(response, 400)) | ||
|
|
||
| db.session.add(new_model) | ||
| db.session.commit() | ||
|
|
||
| return new_model.to_dict(), 201 | ||
|
|
||
|
|
||
| def get_models_with_filters(cls, filters=None): | ||
| query = db.select(cls) | ||
|
|
||
| if filters: | ||
| for attribute, value in filters.items(): | ||
| if hasattr(cls, attribute): | ||
| query = query.where(getattr(cls, attribute).ilike(f"%{value}%")) | ||
|
|
||
| if attribute == "sort": | ||
| if value == "desc": | ||
| query = query.order_by(cls.title.desc()) | ||
| elif value == "asc": | ||
| query = query.order_by(cls.title.asc()) | ||
|
Comment on lines
+39
to
+48
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. You are the first person who I have seen successfully incorporate both filtering and sorting by ascending or descending within the same function! Well done! |
||
|
|
||
| models = db.session.scalars(query.order_by(cls.id)) | ||
| models_response = [model.to_dict() for model in models] | ||
|
|
||
| return models_response | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,84 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint, request, Response | ||
| from app.models.task import Task | ||
| from ..db import db | ||
| from datetime import datetime | ||
| import requests | ||
| import os | ||
| from .route_utilities import validate_model, create_model, get_models_with_filters | ||
|
|
||
| bp = Blueprint("tasks_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 get_one_task(task_id): | ||
|
|
||
| task = validate_model(Task, task_id) | ||
| return task.to_dict() | ||
|
|
||
|
|
||
| @bp.put("/<task_id>") | ||
| def update_task(task_id): | ||
| task = validate_model(Task, task_id) | ||
| request_body = request.get_json() | ||
|
|
||
| task.title = request_body["title"] | ||
| task.description = request_body["description"] | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
|
|
||
| @bp.patch("/<task_id>/mark_complete") | ||
| def mark_complete_task(task_id): | ||
| task = validate_model(Task, task_id) | ||
|
|
||
| task.completed_at = datetime.now() | ||
| db.session.commit() | ||
|
|
||
|
|
||
| url = "https://slack.com/api/chat.postMessage" | ||
| data = { | ||
| "text": f"Someone just completed the task {task.title}", | ||
| "channel": "test-slack-api" | ||
| } | ||
| headers = { | ||
| 'Content-Type': 'application/json', | ||
| 'Authorization': f"Bearer {os.environ.get('SLACK_BOT_TOKEN')}" | ||
| } | ||
| requests.post(url, headers=headers, json=data) | ||
|
Comment on lines
+52
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. This approach to making a Slack API request looks really good, but I do think it could be removed to its own helper function elsewhere! |
||
|
|
||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
|
|
||
| @bp.patch("/<task_id>/mark_incomplete") | ||
| def mark_incomplete_task(task_id): | ||
| task = validate_model(Task, task_id) | ||
|
|
||
| task.completed_at = None | ||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
|
|
||
| @bp.delete("/<task_id>") | ||
| def delete_task(task_id): | ||
| task = validate_model(Task, task_id) | ||
|
|
||
| db.session.delete(task) | ||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/jason") | ||
| 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.
Great job aliasing your blueprints!