-
Notifications
You must be signed in to change notification settings - Fork 36
Possum - Amanda Thompson #30
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
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 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] = mapped_column(nullable=False) | ||
|
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.
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") |
||
| 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, goal_data): | ||
| title = goal_data["title"] | ||
| return cls(title=title) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,34 @@ | ||
| from datetime import datetime | ||
| from typing import Optional | ||
| from sqlalchemy import ForeignKey | ||
| from sqlalchemy.orm import Mapped, mapped_column | ||
| from ..db import db | ||
|
|
||
| class Task(db.Model): | ||
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||
| title: Mapped[str] = mapped_column(nullable=False) | ||
| description: Mapped[str] = mapped_column(nullable=False) | ||
| completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) | ||
|
Comment on lines
8
to
+11
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. Take a look at the the feedback around
|
||
| goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id")) | ||
| goal: Mapped[Optional["Goal"]] = db.relationship("Goal", back_populates="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. We should be consistent with the syntax we use across a project to avoid confusion. Since in What would the updated attribute look like with that change? |
||
|
|
||
| def to_dict(self): | ||
| task_dict = { | ||
| "id": self.id, | ||
| "title": self.title, | ||
| "description": self.description, | ||
| "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): | ||
| title = task_data["title"] | ||
| description = task_data["description"] | ||
| completed_at = task_data.get("completed_at", None) | ||
| goal_id = task_data.get("goal_id", None) | ||
|
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. Great use of |
||
| return cls(title=title, description=description, completed_at=completed_at, 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. How can we break up this line to keep within PEP8 best practices of 79 characters or less? |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,68 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint, request, Response | ||
| from .route_utilities import validate_model, create_model, send_slack_message | ||
| from app.models.goal import Goal | ||
| from app.models.task import Task | ||
| from ..db import db | ||
|
|
||
| goals_bp = Blueprint("goals", __name__, url_prefix="/goals") | ||
|
|
||
| @goals_bp.get("") | ||
| def get_all_goals(): | ||
| db_query = db.select(Goal) | ||
| all_goals = db.session.scalars(db_query) | ||
| goals_response = [] | ||
| for goal in all_goals: | ||
| goals_response.append(goal.to_dict()) | ||
| return goals_response | ||
|
|
||
| @goals_bp.get("/<goal_id>") | ||
| def get_goal(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
| return goal.to_dict() | ||
|
|
||
| @goals_bp.post("") | ||
| def create_goal(): | ||
| goal_data = request.get_json() | ||
| new_goal = create_model(Goal, goal_data) | ||
| return new_goal | ||
|
|
||
| @goals_bp.delete("/<goal_id>") | ||
| def delete_goal(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
| db.session.delete(goal) | ||
| db.session.commit() | ||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @goals_bp.put("/<goal_id>") | ||
| def update_goal(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
| goal_data = request.get_json() | ||
| goal.title = goal_data["title"] | ||
| db.session.commit() | ||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @goals_bp.get("/<goal_id>/tasks") | ||
| def get_tasks_for_goal(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
| response = goal.to_dict() | ||
| response["tasks"] = [task.to_dict() for task in goal.tasks] | ||
| return response | ||
|
|
||
| @goals_bp.post("/<goal_id>/tasks") | ||
| def associate_task_ids_to_goal(goal_id): | ||
| goal = validate_model(Goal, goal_id) | ||
| request_body = request.get_json() | ||
|
|
||
| goal.tasks = [] | ||
|
|
||
| for id in request_body["task_ids"]: | ||
| task = validate_model(Task, id) | ||
| task.goal_id = goal.id | ||
|
Comment on lines
+56
to
+60
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 works, but we can also associate the tasks with a goal by assigning them to the task_ids = request_body["task_ids"]
new_tasks = [validate_model(Task, id) for id in task_ids]
goal.tasks = new_tasks |
||
|
|
||
| db.session.commit() | ||
|
|
||
| response = { | ||
| "id": goal.id, | ||
| "task_ids": [task.id for task in goal.tasks] | ||
| } | ||
| return response | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| from flask import make_response, abort | ||
| import requests | ||
| from ..db import db | ||
| import os | ||
|
|
||
| def validate_model(cls, model_id): | ||
| try: | ||
| model_id = int(model_id) | ||
|
|
||
| except: | ||
| response = {'message': f'{cls.__name__} {model_id} 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: | ||
| response = {'details': f'Invalid data'} | ||
| abort(make_response(response, 400)) | ||
|
|
||
| db.session.add(new_model) | ||
| db.session.commit() | ||
|
|
||
| return new_model.to_dict(), 201 | ||
|
Comment on lines
+23
to
+34
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. Since this both creates a model and creates an http response, I'd consider renaming this to
|
||
|
|
||
| def send_slack_message(task): | ||
| headers = {"Authorization": os.environ.get("SLACK_OAUTH_TOKEN")} | ||
| task_messgae = {"channel":"new_channel", "text": f'Someone just completed the task {task.title}'} | ||
| requests.post("https://slack.com/api/chat.postMessage", headers=headers, json=task_messgae) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,63 @@ | ||
| from flask import Blueprint | ||
| from datetime import datetime | ||
| from flask import Blueprint, request, Response | ||
| from .route_utilities import validate_model, create_model, send_slack_message | ||
| from app.models.task import Task | ||
| from ..db import db | ||
|
|
||
| tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") | ||
|
|
||
| @tasks_bp.get("/<task_id>") | ||
| def get_task(task_id): | ||
| task = validate_model(Task, task_id) | ||
| return task.to_dict() | ||
|
|
||
| @tasks_bp.post("") | ||
| def create_task(): | ||
| task_data = request.get_json() | ||
| new_task = create_model(Task, task_data) | ||
| return new_task | ||
|
|
||
| @tasks_bp.put("/<task_id>") | ||
| def update_task(task_id): | ||
| task = validate_model(Task, task_id) | ||
| task_data = request.get_json() | ||
| task.title = task_data["title"] | ||
| task.description = task_data["description"] | ||
| db.session.commit() | ||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @tasks_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/json") | ||
|
|
||
| @tasks_bp.get("") | ||
| def get_all_tasks(): | ||
| sort_order = request.args.get("sort") | ||
| db_query = db.select(Task) | ||
| if sort_order == "asc": | ||
| db_query = db_query.order_by(Task.title) | ||
| elif sort_order == "desc": | ||
| db_query = db_query.order_by(Task.title.desc()) | ||
| all_tasks = db.session.scalars(db_query) | ||
| tasks_response = [] | ||
| for task in all_tasks: | ||
| tasks_response.append(task.to_dict()) | ||
| return tasks_response | ||
|
|
||
| @tasks_bp.patch("/<task_id>/mark_complete") | ||
| def mark_task_completed(task_id): | ||
| task = validate_model(Task, task_id) | ||
| task.completed_at = datetime.now() | ||
| db.session.commit() | ||
| return task.to_dict(), 204 | ||
|
|
||
| @tasks_bp.patch("/<task_id>/mark_incomplete") | ||
| def mark_task_incomplete(task_id): | ||
| task = validate_model(Task, task_id) | ||
| task.completed_at = None | ||
| db.session.commit() | ||
| send_slack_message(task) | ||
| return task.to_dict(), 204 |
| 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.
To be more consistent with Flask and API patterns, we should name our blueprints
bp, and in other files like__init__.pyhere we should import them with more specific names if necessary.