-
Notifications
You must be signed in to change notification settings - Fork 43
Dragonflies - Vicky F. #42
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
25d4588
4aecdb5
b745747
51b2d7b
b0f8673
407bf3c
656933d
7b209af
3b12d65
3f8a774
f3fe5f3
20828d9
8c6f7d8
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,8 +1,13 @@ | ||
| from flask import Flask | ||
| from .db import db, migrate | ||
| from .models import task, goal | ||
| from .routes.task_routes import bp as tasks_bp | ||
| from .routes.goal_routes import bp as goals_bp | ||
| from dotenv import load_dotenv | ||
| import os | ||
|
|
||
| load_dotenv() | ||
|
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 |
||
|
|
||
| def create_app(config=None): | ||
| app = Flask(__name__) | ||
|
|
||
|
|
@@ -18,5 +23,7 @@ def create_app(config=None): | |
| migrate.init_app(app, db) | ||
|
|
||
| # Register Blueprints here | ||
| app.register_blueprint(tasks_bp) | ||
| app.register_blueprint(goals_bp) | ||
|
Comment on lines
+26
to
+27
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 app | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,29 @@ | ||
| 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] | ||
|
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 work using the declarative mapping here! Since we are doing any specific declarations like in |
||
| 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. Perfect! You are making a relationship attribute on the |
||
|
|
||
| def to_dict(self, with_tasks=False): | ||
| goal_dict = { | ||
| "goal": { | ||
| "id": self.id, | ||
| "title": self.title | ||
| } | ||
| } | ||
|
|
||
| if with_tasks: | ||
| goal_dict["tasks"] = [task.to_dict()["task"] for task in self.tasks] | ||
|
|
||
| return goal_dict | ||
|
Comment on lines
+12
to
+23
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. 👍🏿 |
||
|
|
||
| @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,45 @@ | ||
| from sqlalchemy.orm import Mapped, mapped_column | ||
| from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
| from sqlalchemy import ForeignKey | ||
| from datetime import datetime | ||
| from typing import Optional | ||
| from ..db import db | ||
| 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[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. Instead of using completed_at: Mapped[Optional[datetime]]Since you have "Nullability derives from whether or not the |
||
| goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id")) | ||
| goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks") | ||
|
Comment on lines
+15
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. Nice work on the |
||
|
|
||
|
|
||
| def to_dict(self): | ||
| task_dict = { | ||
| "task": { | ||
| "id": self.id, | ||
| "title": self.title, | ||
| "description": self.description, | ||
| "is_complete": self.is_complete() | ||
| } | ||
| } | ||
|
|
||
| if self.goal_id: | ||
| task_dict["task"]["goal_id"]= self.goal_id | ||
|
|
||
| return task_dict | ||
|
Comment on lines
+19
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. 👍🏿 |
||
|
|
||
| def is_complete(self): | ||
| return bool(self.completed_at) | ||
|
|
||
|
|
||
| @classmethod | ||
| def from_dict(cls, task_data): | ||
| return cls( | ||
| goal_id=task_data.get("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. Great use of |
||
| title=task_data["title"], | ||
| description=task_data["description"], | ||
| completed_at=task_data["completed_at"] | ||
| ) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1 +1,80 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from flask import Blueprint | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from flask import Blueprint, Response, request, jsonify | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import requests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.models.goal import Goal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .route_utilities import validate_model, create_model, get_models_with_filters, get_validated_tasks_from_request_body | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from ..db import db | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bp = Blueprint("goals_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. ⭐️ |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @bp.post("") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def create_goal(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request_body = request.get_json() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return create_model(Goal, request_body) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @bp.get("") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_all_goals(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return get_models_with_filters(Goal,request.args) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @bp.get("/<id>") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_one_goal(id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| goal = validate_model(Goal, id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return goal.to_dict() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+22
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. Very concise route functions! Don't forget we can use whitespace to distinguish different logic blocks in our functions.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @bp.put("/<id>") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def update_goal(id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| goal = validate_model(Goal, id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request_body = request.get_json() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for attribute, value in request_body.items(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(Goal, attribute): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setattr(goal, attribute, value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response(status=204, mimetype="application/json") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
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. Love this logic! Notice how you have essentially the same lines in your update task route as well. This logic has higher level usability and is used across multiple files. You could move this logic into a helper function, resulting in a code base that is more D.R.Y.! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @bp.delete("/<id>") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def delete_goal(id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| goal = validate_model(Goal, id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.delete(goal) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Response(status=204, mimetype="application/json") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+44
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. 👍🏿 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @bp.post("/<id>/tasks") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def add_tasks_to_goal(id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| goal = validate_model(Goal, id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request_body = request.get_json() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tasks_to_add = get_validated_tasks_from_request_body(request_body) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| goal.tasks = tasks_to_add | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db.session.commit() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": goal.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "task_ids": [task.id for task in tasks_to_add] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify(response), 200 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @bp.get("/<id>/tasks") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_tasks_of_one_goal(id): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| goal = validate_model(Goal, id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tasks_response_list = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for task in goal.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. Nice work on leveraging the relationship between your two models. You are setting your relationship attribute, |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tasks_response_list.append(task.to_dict()["task"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response_body = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": goal.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "title": goal.title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "tasks": tasks_response_list | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify(response_body), 200 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| from flask import abort, make_response, jsonify | ||
| from sqlalchemy import asc, desc | ||
| from app.models.task import Task | ||
| import requests | ||
| from ..db import db | ||
|
|
||
| def validate_model(cls, model_id): | ||
| try: | ||
| model_id = int(model_id) | ||
| except ValueError: | ||
| invalid = {"details": f"{cls.__name__} id {model_id} is invalid."} | ||
| abort(make_response(invalid,400)) | ||
|
|
||
| query = db.select(cls).where(cls.id == model_id) | ||
| model = db.session.scalar(query) | ||
|
|
||
| if not model: | ||
| not_found = {"details": f"{cls.__name__} with id {model_id} was not found."} | ||
| abort(make_response(not_found, 404)) | ||
|
|
||
| return model | ||
|
|
||
|
|
||
|
|
||
| def create_model(cls, model_data): | ||
| try: | ||
| new_model = cls.from_dict(model_data) | ||
| except KeyError as e: | ||
| response = {"details": "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, to_dict_options=None): | ||
| query = db.select(cls) | ||
| order_by_clause = cls.id | ||
|
|
||
| if filters: | ||
| for parameter, value in filters.items(): | ||
| if parameter == "sort": | ||
| if value == "asc": | ||
| order_by_clause = asc("title") | ||
| elif value == "desc": | ||
| order_by_clause = desc("title") | ||
| elif hasattr(cls, parameter): | ||
| query = query.where(getattr(cls,parameter).ilike(f"%{value}%")) | ||
|
|
||
| models = db.session.scalars(query.order_by(order_by_clause)) | ||
|
|
||
|
|
||
| models_response = [] | ||
| to_dict_options = to_dict_options if to_dict_options is not None else {} | ||
| for model in models: | ||
| model_dict = model.to_dict(**to_dict_options) | ||
| inner_key = cls.__name__.lower() | ||
|
|
||
| if inner_key in model_dict: | ||
| models_response.append(model_dict[inner_key]) | ||
| else: | ||
| models_response.append(model_dict) | ||
|
|
||
| return models_response | ||
|
|
||
| def call_slackbot(task_title): | ||
| url = "https://hooks.slack.com/services/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" | ||
| text = f"Someone just completed the task: {task_title}" | ||
|
|
||
| try: | ||
| response = requests.post(url=url,json={"text": text},timeout=5.0) | ||
| response.raise_for_status() | ||
| except Exception: | ||
| response = f"An unexpected error occured." | ||
| abort(make_response(response,400)) | ||
|
Comment on lines
+67
to
+76
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! |
||
|
|
||
|
|
||
|
|
||
| def get_validated_tasks_from_request_body(request_body): | ||
| try: | ||
| if not request_body: | ||
| response = {"details": "Request body is empty."} | ||
| abort(make_response(jsonify(response), 400)) | ||
|
|
||
| task_ids_from_request = request_body["task_ids"] | ||
|
|
||
| if not isinstance(task_ids_from_request, list): | ||
| response = {"details": "'task_ids' must be a list."} | ||
| abort(make_response(jsonify(response), 400)) | ||
|
|
||
| validated_task_objects = [] | ||
| for task_id in task_ids_from_request: | ||
| if not isinstance(task_id, int): | ||
| response = {"details": f"Each task ID must be an integer, but received {task_id}."} | ||
| abort(make_response(jsonify(response), 400)) | ||
|
|
||
| task_instance = validate_model(Task, task_id) | ||
| validated_task_objects.append(task_instance) | ||
|
|
||
| return validated_task_objects | ||
|
|
||
| except KeyError: | ||
| response = {"details": "Request body must contain 'task_ids'."} | ||
| abort(make_response(jsonify(response), 400)) | ||
| except Exception as e: | ||
| response = {"details": f"An unexpected error occured: {e}."} | ||
| abort(make_response(jsonify(response), 400)) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,64 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint, Response, abort, make_response, request | ||
| import requests | ||
| from datetime import datetime | ||
| from app.models.task import Task | ||
| from .route_utilities import validate_model, create_model, get_models_with_filters, call_slackbot | ||
| from ..db import db | ||
|
|
||
| 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("/<id>") | ||
| def get_one_task(id): | ||
| task = validate_model(Task, id) | ||
|
|
||
| return task.to_dict() | ||
|
|
||
|
|
||
| @bp.put("/<id>") | ||
| def update_task(id): | ||
| task = validate_model(Task, id) | ||
| request_body = request.get_json() | ||
|
|
||
| for attribute, value in request_body.items(): | ||
| if hasattr(Task, attribute): | ||
| setattr(task, attribute, value) | ||
|
|
||
| db.session.commit() | ||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @bp.delete("/<id>") | ||
| def delete_task(id): | ||
| task = validate_model(Task, id) | ||
|
|
||
| db.session.delete(task) | ||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
|
||
| @bp.patch("<id>/<completion_status>") | ||
|
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. Love that you are using route parameters in this way! |
||
| def modify_task_completion_status(id, completion_status): | ||
| task = validate_model(Task, id) | ||
|
|
||
| if completion_status == 'mark_incomplete': | ||
| task.completed_at = None | ||
|
|
||
| elif completion_status == 'mark_complete': | ||
| task.completed_at = datetime.now() | ||
| call_slackbot(task.title) | ||
| else: | ||
| response = {"details": f"Route {completion_status} not recognized"} | ||
| abort(make_response(response,400)) | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return Response(status=204, mimetype="application/json") | ||
|
Comment on lines
+52
to
+64
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 here! ⭐️ |
||
| 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.
Nice! You are following Flask convention to by naming your Blueprints
bpand then usingasto import them under an alias.