Skip to content

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

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7386432
Initial project set up
rooted-dev Nov 5, 2024
be9c751
Mapped attributes for Task class.
rooted-dev Nov 5, 2024
690c40b
Added blueprint instance for /tasks
rooted-dev Nov 5, 2024
039c24d
Registered blueprint
rooted-dev Nov 5, 2024
1d93b7b
Added POST request for /tasks
rooted-dev Nov 5, 2024
858df5a
Updated attribute is_complete
rooted-dev Nov 5, 2024
2cc6f12
Added get all tasks function to /tasks
rooted-dev Nov 5, 2024
6e1dab6
Added get one task to /task
rooted-dev Nov 5, 2024
53a933a
Added validate task to check if id is an int, created response body f…
rooted-dev Nov 5, 2024
8c16efe
Added put request to update a task in /task
rooted-dev Nov 5, 2024
2e862e9
Added delete request in /tasks
rooted-dev Nov 5, 2024
50194e1
added to_dict to refactor the response body for create task, get all …
rooted-dev Nov 5, 2024
a114dd6
added query params to get all tasks
rooted-dev Nov 5, 2024
41797ae
added a KeyError response to create_task and adjusted the response to…
rooted-dev Nov 5, 2024
51008a2
Added asserts to incomplete test for wave 1
rooted-dev Nov 5, 2024
7894b20
added a sort param to order titles in asc and desc
rooted-dev Nov 5, 2024
0b1c748
wave 2 test file
rooted-dev Nov 5, 2024
37daef1
Added code for incomplete and complete function
rooted-dev Nov 7, 2024
dc81387
refactored response body for update function
rooted-dev Nov 7, 2024
d2efd56
Added create task function in /goals
rooted-dev Nov 8, 2024
4abd413
added get all method for /goals
rooted-dev Nov 8, 2024
5156434
added get one goal to /goals and added validate id function
rooted-dev Nov 8, 2024
9b0f168
added post update task on /goals and completed the update goal wave 5…
rooted-dev Nov 8, 2024
26e2e2a
added delete function to /goals and updated incomplete tests for wave 5
rooted-dev Nov 8, 2024
1bfc8f7
passed a few of wave 6 tests.
rooted-dev Nov 8, 2024
51b422b
reworked the get function /goals/goal id/tasks
rooted-dev Nov 8, 2024
fea87a2
added code to pass wave 6
rooted-dev Nov 8, 2024
61b99cf
restarted migration folder and updated to_dict
rooted-dev Nov 8, 2024
3a19fe0
completed tests and deployed on render
rooted-dev Nov 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
from .db import db, migrate
from .models import task, goal
import os
from .routes.task_routes import tasks_bp
from .routes.goal_routes import goals_bp
Comment on lines +5 to +6

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:

from .routes.task_routes import bp as tasks_bp




def create_app(config=None):
app = Flask(__name__)
Expand All @@ -18,5 +22,7 @@ def create_app(config=None):
migrate.init_app(app, db)

# Register Blueprints here
app.register_blueprint(tasks_bp)
app.register_blueprint(goals_bp)

return app
18 changes: 17 additions & 1 deletion app/models/goal.py
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")

Choose a reason for hiding this comment

The 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
40 changes: 39 additions & 1 deletion app/models/task.py
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

Choose a reason for hiding this comment

The 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,

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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
108 changes: 107 additions & 1 deletion app/routes/goal_routes.py
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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We essentially just want an outer dictionary structure with a key of the class name, perhaps we could move this into a helper function or add it to a class in order to DRY up some of our code given that we have this same logic throughout the project?


@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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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 validate_task function? Are there any opportunities to refactor?


@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"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this code similar to our other .put method? How could we use the hasattr and setattr functions that Python provides in order to make DRY up our routes?


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

Choose a reason for hiding this comment

The 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
143 changes: 142 additions & 1 deletion app/routes/task_routes.py
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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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.

1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Single-database configuration for Flask.
50 changes: 50 additions & 0 deletions migrations/alembic.ini
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
Loading