-
Notifications
You must be signed in to change notification settings - Fork 44
C22 Sphinx - Brianna R. #37
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
7386432
be9c751
690c40b
039c24d
1d93b7b
858df5a
2cc6f12
6e1dab6
53a933a
8c16efe
2e862e9
50194e1
a114dd6
41797ae
51008a2
7894b20
0b1c748
37daef1
dc81387
d2efd56
4abd413
5156434
9b0f168
26e2e2a
1bfc8f7
51b422b
fea87a2
61b99cf
3a19fe0
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,21 @@ | ||
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") | ||
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. ⭐️ |
||
|
||
def to_dict(self): | ||
return { | ||
"id":self.id, | ||
"title":self.title, | ||
} | ||
|
||
@classmethod | ||
def from_dict(cls, goal_data): | ||
new_goal = cls(title=goal_data["title"]) | ||
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.orm import Mapped, mapped_column, relationship | ||
from sqlalchemy import ForeignKey | ||
from ..db import db | ||
from typing import Optional | ||
from datetime import datetime | ||
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) | ||
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id")) | ||
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks") | ||
Comment on lines
+12
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. 👍🏿 |
||
|
||
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. Nice work here! |
||
} | ||
if self.goal_id: | ||
task_dict["goal_id"] = self.goal_id | ||
return task_dict | ||
|
||
@classmethod | ||
def from_dict(cls, task_data): | ||
completed_at = datetime if task_data.get("is_complete", False) else 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. ✨ |
||
return cls( | ||
title=task_data["title"], | ||
description=task_data["description"], | ||
completed_at=completed_at, | ||
goal_id=task_data.get("goal_id", None) | ||
) | ||
|
||
def mark_complete(self): | ||
self.completed_at = datetime.now() | ||
|
||
def mark_incomplete(self): | ||
self.completed_at = None |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,107 @@ | ||
from flask import Blueprint | ||
from flask import Blueprint, request, abort, make_response | ||
from app.models.goal import Goal | ||
from app.routes.task_routes import validate_task | ||
from ..db import db | ||
from app.models.task import Task | ||
|
||
goals_bp = Blueprint("goals_bp", __name__, url_prefix="/goals") | ||
|
||
@goals_bp.post("") | ||
def create_goal(): | ||
request_body = request.get_json() | ||
|
||
try: | ||
new_goal = Goal.from_dict(request_body) | ||
except KeyError as error: | ||
response = {"details": "Invalid data"} | ||
abort(make_response(response, 400)) | ||
Comment on lines
+13
to
+17
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_goal) | ||
db.session.commit() | ||
|
||
return {"goal":new_goal.to_dict()}, 201 | ||
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 |
||
|
||
@goals_bp.get("") | ||
def get_all_goals(): | ||
query = db.select(Goal) | ||
|
||
title_param = request.args.get("title") | ||
if title_param: | ||
query = query.where(Goal.title.ilike(f"%({title_param}%")) | ||
|
||
goals = db.session.scalars(query.order_by(Goal.id)) | ||
goals_response = [goal.to_dict() for goal in goals] | ||
|
||
return goals_response | ||
Comment on lines
+28
to
+35
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 also logic that could be refactored like how we did in class, this will help DRY up our code base even more. |
||
|
||
@goals_bp.get("/<goal_id>") | ||
def get_one_goal(goal_id): | ||
goal = validate_goal(goal_id) | ||
|
||
return {"goal": goal.to_dict()}, 200 | ||
|
||
def validate_goal(goal_id): | ||
try: | ||
goal_id = int(goal_id) | ||
except: | ||
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": "goal not found"} | ||
abort(make_response(response, 404)) | ||
return goal | ||
Comment on lines
+43
to
+56
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. Being that this is a helper function could we perhaps move it into a utilities functions file since this function is specifically for our goal routes? With that being said, what similarities to we see between this function and the |
||
|
||
@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() | ||
|
||
response_body = {"goal": goal.to_dict()} | ||
return response_body, 200 | ||
|
||
@goals_bp.delete("/<goal_id>") | ||
def delete_goal(goal_id): | ||
goal = validate_goal(goal_id) | ||
db.session.delete(goal) | ||
db.session.commit() | ||
|
||
response_body = {"details": f'Goal {goal_id} "{goal.title}" successfully deleted'} | ||
return response_body | ||
|
||
@goals_bp.post("/<goal_id>/tasks") | ||
def add_tasks_tp_goal(goal_id): | ||
goal = validate_goal(goal_id) | ||
request_body = request.get_json() | ||
task_ids = request_body["task_ids"] | ||
|
||
for task_id in task_ids: | ||
task = validate_task(task_id) | ||
goal.tasks.append(task) | ||
|
||
db.session.commit() | ||
|
||
response = { | ||
"id": goal.id, | ||
"task_ids": task_ids, | ||
} | ||
|
||
return response | ||
Comment on lines
+79
to
+96
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. 👍🏿 |
||
|
||
|
||
@goals_bp.get("/<goal_id>/tasks") | ||
def get_tasks_by_goal(goal_id): | ||
goal = validate_goal(goal_id) | ||
tasks = [task.to_dict() for task in goal.tasks] | ||
|
||
response = goal.to_dict() | ||
response["tasks"] = tasks | ||
|
||
return response |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,142 @@ | ||
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 | ||
import requests | ||
import os | ||
from dotenv import load_dotenv | ||
load_dotenv() | ||
|
||
tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") | ||
|
||
# slack URL | ||
SLACK_API_URL = os.environ.get('SLACK_API_URL') | ||
SLACK_API_TOKEN = os.environ.get('SLACK_API_TOCKEN') | ||
|
||
@tasks_bp.post("") | ||
def create_task(): | ||
request_body = request.get_json() | ||
|
||
try: | ||
new_task = Task.from_dict(request_body) | ||
|
||
except KeyError as e: | ||
response = {"details": "Invalid data"} | ||
abort(make_response(response, 400)) | ||
|
||
db.session.add(new_task) | ||
db.session.commit() | ||
Comment on lines
+19
to
+28
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 that is a sign for us to refactor this logic like in our previous create route. |
||
|
||
return {"task": new_task.to_dict()}, 201 | ||
|
||
@tasks_bp.get("") | ||
def get_all_tasks(): | ||
query = db.select(Task) | ||
|
||
title_param = request.args.get("title") | ||
if title_param: | ||
query = query.where(Task.title.ilike(f"%{title_param}%")) | ||
|
||
description_param = request.args.get("description") | ||
if description_param: | ||
query = query.where(Task.description.ilike(f"%{description_param}%")) | ||
|
||
sort_param = request.args.get("sort") | ||
if sort_param == "desc": | ||
query = query.order_by(Task.title.desc()) | ||
else: | ||
query = query.order_by(Task.title.asc()) | ||
|
||
|
||
tasks = db.session.scalars(query.order_by(Task.title)) | ||
|
||
tasks_response = [task.to_dict() for task in tasks] | ||
return tasks_response | ||
|
||
|
||
@tasks_bp.get("/<task_id>") | ||
def get_one_task(task_id): | ||
task = validate_task(task_id) | ||
|
||
return {"task": task.to_dict()}, 200 | ||
|
||
@tasks_bp.put("/<task_id>") | ||
def update_task(task_id): | ||
task = validate_task(task_id) | ||
request_body = request.get_json() | ||
|
||
task.title = request_body["title"] | ||
task.description = request_body["description"] | ||
|
||
db.session.commit() | ||
|
||
response_body = {"task":task.to_dict()} | ||
return response_body, 200 | ||
|
||
@tasks_bp.delete("/<task_id>") | ||
def delete_task(task_id): | ||
task = validate_task(task_id) | ||
db.session.delete(task) | ||
db.session.commit() | ||
|
||
response_body = {"details": f'Task {task_id} "{task.title}" successfully deleted'} | ||
return response_body | ||
|
||
@tasks_bp.patch("/<task_id>/mark_complete") | ||
def mark_complete(task_id): | ||
task = validate_task(task_id) | ||
|
||
task.mark_complete() | ||
db.session.commit() | ||
|
||
post_to_slack(task) | ||
|
||
return {"task": task.to_dict()}, 200 | ||
Comment on lines
+87
to
+94
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. ✨ |
||
|
||
|
||
@tasks_bp.patch("/<task_id>/mark_incomplete") | ||
def mark_incomplete(task_id): | ||
task = validate_task(task_id) | ||
|
||
task.mark_incomplete() | ||
db.session.commit() | ||
|
||
return {"task" : task.to_dict()}, 200 | ||
|
||
|
||
|
||
def validate_task(task_id): | ||
try: | ||
task_id = int(task_id) | ||
except: | ||
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": "task not found"} | ||
abort(make_response(response, 404)) | ||
return task | ||
|
||
def post_to_slack(task): | ||
headers = { | ||
"Authorization": f"Bearer {SLACK_API_TOKEN}", | ||
"Content-Type": "application/json", | ||
|
||
} | ||
if task.completed_at: | ||
data = { | ||
"text": f"Task '{task.title}' has been marked complete", | ||
"channel": "C080MLHBX5W", | ||
} | ||
else: | ||
data = { | ||
"text": f"Task '{task.title}' has been marked incomplete", | ||
"channel": "C080MLHBX5W", | ||
} | ||
|
||
r = requests.post(SLACK_API_URL, headers=headers, json=data) | ||
|
||
return r | ||
Comment on lines
+108
to
+142
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. Both of these functions could live in our utilities file to better separate the concerns of our files. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Single-database configuration for Flask. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# A generic, single database configuration. | ||
|
||
[alembic] | ||
# template used to generate migration files | ||
# file_template = %%(rev)s_%%(slug)s | ||
|
||
# set to 'true' to run the environment during | ||
# the 'revision' command, regardless of autogenerate | ||
# revision_environment = false | ||
|
||
|
||
# Logging configuration | ||
[loggers] | ||
keys = root,sqlalchemy,alembic,flask_migrate | ||
|
||
[handlers] | ||
keys = console | ||
|
||
[formatters] | ||
keys = generic | ||
|
||
[logger_root] | ||
level = WARN | ||
handlers = console | ||
qualname = | ||
|
||
[logger_sqlalchemy] | ||
level = WARN | ||
handlers = | ||
qualname = sqlalchemy.engine | ||
|
||
[logger_alembic] | ||
level = INFO | ||
handlers = | ||
qualname = alembic | ||
|
||
[logger_flask_migrate] | ||
level = INFO | ||
handlers = | ||
qualname = flask_migrate | ||
|
||
[handler_console] | ||
class = StreamHandler | ||
args = (sys.stderr,) | ||
level = NOTSET | ||
formatter = generic | ||
|
||
[formatter_generic] | ||
format = %(levelname)-5.5s [%(name)s] %(message)s | ||
datefmt = %H:%M:%S |
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: