-
Notifications
You must be signed in to change notification settings - Fork 44
Sphinx - Salma Anany #27
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 6 commits
9685e58
0209ebd
edb1aee
5a1986a
2501a25
7449eb8
41f12a9
71c015e
ee4e586
2c87abb
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 |
|---|---|---|
|
|
@@ -138,4 +138,6 @@ dmypy.json | |
| .pytype/ | ||
|
|
||
| # Cython debug symbols | ||
| cython_debug/ | ||
| cython_debug/ | ||
|
|
||
| .idea | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| from flask_sqlalchemy import SQLAlchemy | ||
| from flask_migrate import Migrate | ||
| from flask_sqlalchemy import SQLAlchemy | ||
|
|
||
| from .models.base import Base | ||
|
|
||
| db = SQLAlchemy(model_class=Base) | ||
| migrate = Migrate() | ||
| migrate = Migrate() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| from sqlalchemy.orm import DeclarativeBase | ||
|
|
||
|
|
||
| class Base(DeclarativeBase): | ||
| pass | ||
| pass |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,23 @@ | ||||||
| from sqlalchemy.orm import Mapped, mapped_column | ||||||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||||||
|
|
||||||
| from ..db import db | ||||||
|
|
||||||
|
|
||||||
| class Goal(db.Model): | ||||||
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||||||
| title: Mapped[str] | ||||||
|
||||||
| tasks = relationship('Task', back_populates='goal', cascade='all, delete-orphan') | ||||||
|
|
||||||
| def to_dict(self, noTasks=True): | ||||||
|
||||||
| def to_dict(self, noTasks=True): | |
| def to_dict(self, no_tasks=True): |
Outdated
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.
You could also put this logic in a helper function like is_complete
| "is_complete": task.completed_at is not None}) | |
| "is_complete": task.is_complete()}) |
|
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. Same comment as in |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,26 @@ | ||
| from sqlalchemy.orm import Mapped, mapped_column | ||
| from datetime import datetime | ||
|
|
||
| from sqlalchemy import Integer, ForeignKey | ||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
|
|
||
| from ..db import db | ||
|
|
||
|
|
||
| class Task(db.Model): | ||
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||
| title: Mapped[str] | ||
|
||
| description: Mapped[str] | ||
| completed_at: Mapped[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. This white space should be removed |
||
| goal_id = db.Column(Integer, ForeignKey('goal.id'), nullable=True) | ||
| goal = relationship('Goal', back_populates='tasks') | ||
|
|
||
| def to_dict(self): | ||
| result = dict( | ||
| id=self.id, | ||
| title=self.title, | ||
| description=self.description, | ||
| is_complete=self.completed_at != None) | ||
| if self.goal is not None: | ||
| result["goal_id"] = self.goal.id | ||
| return result | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,136 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint, make_response, abort, request | ||
|
|
||
| from app import db | ||
| from app.models.goal import Goal | ||
| from app.models.task import Task | ||
|
|
||
| goal_bp = Blueprint("goal_bp", __name__, url_prefix="/goals") | ||
|
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. In Learn and in the livecode for Flasky, we name blueprints for each model If we have multiple route files that each have a blueprint named from .routes.task_routes import bp as tasks_bp
from .routes.goal_routes import bp as goals_bp |
||
|
|
||
|
|
||
| @goal_bp.post("") | ||
| def create_goal(): | ||
| request_body = request.get_json() | ||
| if "title" not in request_body.keys(): | ||
| abort(make_response({"details": "Invalid data"}, 400)) | ||
| title = request_body["title"] | ||
| new_goal = Goal(title=title) | ||
| db.session.add(new_goal) | ||
| db.session.commit() | ||
| response = { | ||
| "goal": new_goal.to_dict() | ||
| } | ||
| return response, 201 | ||
|
||
|
|
||
|
|
||
| @goal_bp.post("/<goal_id>") | ||
| def create_goal_with_id(goal_id): | ||
| request_body = request.get_json() | ||
| if "title" not in request_body.keys(): | ||
| abort(make_response({"details": "Invalid data"}, 400)) | ||
| title = request_body["title"] | ||
| new_goal = Goal(id=goal_id, title=title) | ||
| db.session.add(new_goal) | ||
| db.session.commit() | ||
| response = { | ||
| "goal": new_goal.to_dict() | ||
| } | ||
| return response, 201 | ||
|
||
|
|
||
|
|
||
| @goal_bp.get("") | ||
| def get_all_tasks(): | ||
|
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. Would be nice if all the logic for a GET request to /goals was in a helper method that can be used across different routes. Using a helper here would make this route more concise. Here's how we did it in Flasky |
||
| title_param = request.args.get("title") | ||
|
|
||
| query = db.select(Goal) | ||
| if title_param: | ||
| query = query.where(Goal.title.like(f"%{title_param}")) | ||
|
|
||
| goals = db.session.scalars(query).all() | ||
| goals_response = [goal.to_dict() for goal in goals] | ||
| return goals_response | ||
SalmaAnany marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def goal_id_validation(id): | ||
| if not id.isnumeric(): | ||
| abort(make_response({"details": f" {id} Invalid data"}, 400)) | ||
|
|
||
|
|
||
| @goal_bp.get("/<goal_id>") | ||
| def get_one_goal(goal_id): | ||
| goal_id_validation(goal_id) | ||
| query = db.select(Goal).where(Goal.id == goal_id) | ||
| goal = db.session.scalar(query) | ||
|
|
||
| if not goal: | ||
| response = {"message": f"{goal_id} not found"} | ||
| abort(make_response(response, 404)) | ||
|
||
| return {"goal": goal.to_dict()} | ||
|
|
||
|
|
||
| @goal_bp.post("/<goal_id>/tasks") | ||
| def set_one_goal_tasks(goal_id): | ||
| request_body = request.get_json() | ||
| goal_id_validation(goal_id) | ||
|
|
||
| goal = db.session.get(Goal, int(goal_id)) | ||
| if not goal: | ||
| response = {"message": f"{goal_id} not found"} | ||
| abort(make_response(response, 404)) | ||
|
||
|
|
||
| task_ids = request_body.get("task_ids", []) | ||
| if not task_ids: | ||
| abort(make_response({"message": "No task IDs provided"}, 400)) | ||
|
|
||
| tasks = db.session.query(Task).filter(Task.id.in_(task_ids)).all() | ||
| if len(tasks) != len(task_ids): | ||
| abort(make_response({"message": "One or more tasks not found"}, 404)) | ||
|
|
||
| for task in tasks: | ||
| task.goal = goal | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return {"id": goal.id, "task_ids": [task.id for task in tasks]}, 200 | ||
|
|
||
|
|
||
| @goal_bp.put("/<goal_id>") | ||
| def update_goal(goal_id): | ||
| goal_id_validation(goal_id) | ||
| query = db.select(Goal).where(Goal.id == goal_id) | ||
| goal = db.session.scalar(query) | ||
|
|
||
| if goal is None: | ||
| abort(make_response({"details": f"Goal {goal_id} not found"}, 404)) | ||
|
|
||
| request_body = request.get_json() | ||
| goal.title = request_body["title"] | ||
| db.session.commit() | ||
| result = {"goal": goal.to_dict()} | ||
| return make_response(result, 200) | ||
|
|
||
|
|
||
| @goal_bp.delete("/<goal_id>") | ||
| def delete_task(goal_id): | ||
| goal_id_validation(goal_id) | ||
| query = db.select(Goal).where(Goal.id == goal_id) | ||
| goal = db.session.scalar(query) | ||
| if not goal: | ||
| response = {"details": f"{goal_id} not found."} | ||
| abort(make_response(response, 404)) | ||
|
|
||
| db.session.delete(goal) | ||
| db.session.commit() | ||
| response = {"details": f'Goal {goal_id} "{goal.title}" successfully deleted'} | ||
| return make_response(response, 200) | ||
|
|
||
|
|
||
| @goal_bp.get("/<goal_id>/tasks") | ||
| def get_one_goal_tasks(goal_id): | ||
| goal_id_validation(goal_id) | ||
| query = db.select(Goal).where(Goal.id == goal_id) | ||
| goal = db.session.scalar(query) | ||
|
|
||
| if not goal: | ||
| response = {"message": f"{goal_id} not found"} | ||
| abort(make_response(response, 404)) | ||
| return goal.to_dict(False) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,144 @@ | ||
| from flask import Blueprint | ||
| from datetime import datetime | ||
|
|
||
| from flask import request, Blueprint, make_response, abort | ||
| from sqlalchemy import asc, desc | ||
|
|
||
| from app import db | ||
| from app.models.task import Task | ||
| from app.slack.slack_client import SlackClient | ||
| from app.slack.slackmessage import SlackMessage | ||
|
|
||
| tasks_bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") | ||
| slack_client = SlackClient() | ||
|
|
||
|
|
||
| @tasks_bp.post("") | ||
| def create_task(): | ||
| request_body = request.get_json() | ||
| if "title" not in request_body.keys(): | ||
| abort(make_response({"details": "Invalid data"}, 400)) | ||
| title = request_body["title"] | ||
| if "description" not in request_body.keys(): | ||
| abort(make_response({"details": "Invalid data"}, 400)) | ||
| description = request_body["description"] | ||
| new_task = Task(title=title, description=description, completed_at=None) | ||
| db.session.add(new_task) | ||
| db.session.commit() | ||
|
Comment on lines
+17
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. Another great candidate for refactoring to use a When writing route logic, we might ask ourselves questions like, "Does this route need to know how to process a request, handle potential errors in the request body, create an instance of the object, and add/commit it to the DB?" We can make our routes concise and readable by using helper methods |
||
| response = { | ||
| "task": new_task.to_dict() | ||
| } | ||
| return response, 201 | ||
|
|
||
|
|
||
| @tasks_bp.get("") | ||
| def get_all_tasks(): | ||
| description_param = request.args.get("description") | ||
| title_param = request.args.get("title") | ||
| sort_param = request.args.get("sort") | ||
|
|
||
| query = db.select(Task) | ||
| if description_param: | ||
| query = query.where(Task.description.like(f"%{description_param}%")) | ||
|
|
||
| if title_param: | ||
| query = query.where(Task.title.like(f"%{title_param}")) | ||
| if sort_param == "asc": | ||
| query = query.order_by(asc(Task.title)) | ||
| elif sort_param == "desc": | ||
| query = query.order_by(desc(Task.title)) | ||
|
|
||
| tasks = db.session.scalars(query).all() | ||
| tasks_response = [task.to_dict() for task in tasks] | ||
| return tasks_response | ||
|
|
||
|
|
||
| def validate_id(id): | ||
| if not id.isnumeric(): | ||
| abort(make_response({"message": f"Tasks id {id} invalid"}, 400)) | ||
|
|
||
|
Comment on lines
+55
to
+58
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'd expect the logic for a method like |
||
|
|
||
| @tasks_bp.get("/<task_id>") | ||
| def get_one_task(task_id): | ||
| validate_id(task_id) | ||
| query = db.select(Task).where(Task.id == task_id) | ||
| task = db.session.scalar(query) | ||
|
|
||
| if not task: | ||
| response = {"message": f"{task_id} not found"} | ||
| abort(make_response(response, 404)) | ||
|
|
||
| return {"task": task.to_dict()} | ||
|
|
||
|
|
||
| @tasks_bp.put("/<task_id>") | ||
| def update_task(task_id): | ||
| validate_id(task_id) | ||
| query = db.select(Task).where(Task.id == task_id) | ||
| task = db.session.scalar(query) | ||
|
|
||
| if task is None: | ||
| abort(make_response({"message": f"Task {task_id} not found"}, 404)) | ||
|
|
||
| request_body = request.get_json() | ||
| task.title = request_body["title"] | ||
| task.description = request_body["description"] | ||
| if "is_complete" in request_body.keys() and request_body["is_complete"]: | ||
| task.completed_at = datetime.now() | ||
| else: | ||
| task.completed_at = None | ||
| db.session.commit() | ||
| return { | ||
| "task": task.to_dict() | ||
| } | ||
|
|
||
|
|
||
| @tasks_bp.delete("/<task_id>") | ||
| def delete_task(task_id): | ||
| validate_id(task_id) | ||
| query = db.select(Task).where(Task.id == task_id) | ||
| task = db.session.scalar(query) | ||
| if not task: | ||
| response = {"message": f"{task_id} not found"} | ||
| abort(make_response(response, 404)) | ||
|
|
||
| db.session.delete(task) | ||
| db.session.commit() | ||
|
|
||
| return { | ||
| "details": f'Task {task_id} "{task.title}" successfully deleted' | ||
| } | ||
|
|
||
|
|
||
| def change_task_status(task_id, is_completed): | ||
| task = Task.query.get(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. The query = db.select(Task).where(Task.id == task_id)
model = db.session.scalar(query)See how we made a generic helper method for validating and fetching records from the DB in Flasky here on lines 10-11 |
||
| if task is None: | ||
| response = {"message": f"Task {task_id} not found"} | ||
| abort(make_response(response, 404)) | ||
|
|
||
| if is_completed: | ||
| completed_date = datetime.now() | ||
| task_text = f"Someone just completed the task '{task.title}'" | ||
| else: | ||
| completed_date = None | ||
| task_text = f"Someone just marked the task '{task.title}' incomplete" | ||
|
|
||
| notification_messages = SlackMessage("task-notifications", task_text) | ||
SalmaAnany marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
SalmaAnany marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| notification_was_sent = slack_client.post_message(notification_messages) | ||
|
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. ~~Recall that the project requirement was to use the Python package We want folks to get practice using @SalmaAnany Please let me know, as a repy to this comment, how you would re-write the code for calling Slack using
Author
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 used requests, please check the client, that's my own class using requests. 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. Ah yes, I meant to remove this comment after I saw your SlackClient class! Thanks for pointing me there again. |
||
| if notification_was_sent: | ||
| Task.query.filter_by(id=task_id).update({Task.completed_at: completed_date}) | ||
| db.session.commit() | ||
| return task | ||
|
|
||
|
|
||
| @tasks_bp.patch("/<task_id>/mark_complete") | ||
| def mark_completed_task(task_id): | ||
| validate_id(task_id) | ||
| task = change_task_status(task_id, True) | ||
| return make_response({"task": task.to_dict()}, 200) | ||
|
|
||
|
|
||
| @tasks_bp.patch("/<task_id>/mark_incomplete") | ||
| def mark_in_complete_task(task_id): | ||
| validate_id(task_id) | ||
| task = change_task_status(task_id, False) | ||
| return make_response({"task": task.to_dict()}, 200) | ||
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.
Consider adding the class method
from_dictto this model so you don't need to create an instance of Goal in your routes (example from Flasky)