Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 14 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
from flask import Flask
from .db import db, migrate
from .models import task, goal
from app.routes.task_routes import bp as tasks_bp
from app.routes.goal_routes import bp as goals_bp
from dotenv import load_dotenv
from flask_cors import CORS
import os

def create_app(config=None):
load_dotenv()

def create_app(test_config=None):
app = Flask(__name__)
CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')

if config:
if test_config:
# Merge `config` into the app's configuration
# to override the app's default settings for testing
app.config.update(config)
app.config.update(test_config)

# Initialize app with SQLAlchemy and Migrate
db.init_app(app)
migrate.init_app(app, db)

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

return app
35 changes: 34 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship # type: ignore
from ..db import db
from ..routes.task_routes import validate_task
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 check_goal_tasks(self):
tasks_assigned = []
if self.tasks:
for task in self.tasks:
task = validate_task(task.id)

Choose a reason for hiding this comment

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

If a task is found on self.tasks then we already know that it is a valid task and therefore we don't need to validate it.

tasks_assigned.append(task.to_nested_dict())

return tasks_assigned

def to_dict(self):
goal_to_dict = {}
goal_to_dict["id"] = self.id
goal_to_dict["title"] = self.title
return goal_to_dict

def to_nested_dict(self):
goal_to_dict = {}
goal_to_dict["id"] = self.id
goal_to_dict["title"] = self.title
if not self.check_goal_tasks():
goal_to_dict["tasks"] = []
else:
goal_to_dict["tasks"] = self.check_goal_tasks()
Comment on lines +29 to +36

Choose a reason for hiding this comment

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

We have an endpoint that will provide us with the same data that goal_to_dict["tasks"] will give us so we don't need to put it on the dictionary representation of the object. If I client has the information to retrieve a Goal (i.e. goal.id) then they have the information to get the same information from our endpoint.


return goal_to_dict
Comment on lines +29 to +38

Choose a reason for hiding this comment

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

Notice that this method and the Task method of the same name are virtually the same. We could possibly take this logic and place it in a helper or placed it on the Base class to be inherited by the these children models.

52 changes: 51 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship # type: ignore
from ..db import db
from datetime import datetime
from sqlalchemy import ForeignKey # type: ignore
from typing import Optional

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]] # = completed_at if completed_at is not None

goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")
Comment on lines +18 to +19

Choose a reason for hiding this comment

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

Nice work establishing this relationship and attribute of the Goal this tasks belongs to!



def to_dict(self):
if not self.completed_at:
is_complete = False
else:
is_complete = True

return dict(
id=self.id,
title=self.title,
description=self.description,
is_complete=is_complete
)

def to_nested_dict(self):
if not self.completed_at:
is_complete = False
else:
is_complete = True

task_dictionary = dict(
id=self.id,
title=self.title,
description=self.description,
is_complete=is_complete
)

if self.goal_id:
task_dictionary["goal_id"] = self.goal_id

return task_dictionary




121 changes: 120 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,120 @@
from flask import Blueprint
from flask import Blueprint, abort, jsonify, make_response, request
from app.db import db
from ..models.goal import Goal
import requests
# from .route_utilities import validate_model, create_model, validate_task, validate_goal
from app.models.task import Task
from ..routes.task_routes import validate_task


bp = Blueprint("goals_bp", __name__, url_prefix="/goals")

@bp.post("")
def create_goal():

try:
request_body = request.get_json()
title = request_body["title"]

new_goal = Goal(title=title)

db.session.add(new_goal)
db.session.commit()

return {"goal": new_goal.to_dict()}, 201

except KeyError as error:
response = {"details": f"Invalid data"}
abort(make_response(response, 400))
Comment on lines +15 to +28

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


@bp.get("")
def get_all_goals():

query = db.select(Goal).order_by(Goal.id)
goals = db.session.scalars(query)

goals_response = [goal.to_dict() for goal in goals]

return goals_response
Comment on lines +33 to +38

Choose a reason for hiding this comment

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

The refactor I mention below about getting all of our tasks would allow us condense this to one line of code.


@bp.get("/<goal_id>")
def get_one_goal(goal_id):
goal = validate_goal(goal_id)
response = {"goal": goal.to_dict()}

Choose a reason for hiding this comment

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

Another place where our nested dictionary logic could be implemented.

return response


@bp.get("/<goal_id>/tasks")
def get_tasks_by_goal(goal_id):
goal = validate_goal(goal_id)
response = goal.to_nested_dict()
return response

# @bp.get("/<goal_id>/tasks")
# def get_tasks_for_one_goal(goal_id):
# goal = validate_goal(goal_id)
# return goal.to_nested_dict()



@bp.post("/<goal_id>/tasks")
def create_new_tasks_for_goal(goal_id):

Choose a reason for hiding this comment

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

I would name this something different, mainly because it gives the impression that you will create a new Task. In actuality, we are just assigning pre-existing tasks to a selected Goal.


current_goal = validate_goal(goal_id)
request_body = request.get_json()


current_goal_tasks = request_body["task_ids"]

for task in current_goal_tasks:
current_task = validate_task(task)
# query = db.select(Task).where(current_task.goal_id == goal_id)
current_task.goal_id = goal_id

db.session.commit()

return {"id": current_goal.id,
"task_ids": current_goal_tasks
}


@bp.delete("/<goal_id>")
def delete_goal(goal_id):

goal = validate_goal(goal_id)

db.session.delete(goal)
db.session.commit()

response = {"details": f'Goal {goal_id} "{goal.title}" successfully deleted'}
return jsonify(response)
Comment on lines +81 to +90

Choose a reason for hiding this comment

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

⭐️



@bp.put("/<goal_id>")
def update_goal(goal_id):
goal = validate_goal(goal_id)
request_body = request.get_json()

goal.title = request_body["title"]

db.session.commit()

return {"goal": goal.to_dict()}
Comment on lines +93 to +102

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?



def validate_goal(goal_id):
try:
goal_id = int(goal_id)

except:
response = {"details": f"Invalid data"}
abort(make_response(response, 400))

query = db.select(Goal).where(Goal.id == goal_id)
goal = db.session.scalar(query)

if not goal:
response = {"details": f"Goal {goal_id} not found"}
abort(make_response(response, 404))

return goal
Comment on lines +105 to +120

Choose a reason for hiding this comment

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

We could move this into it's own helper file now so that we could clear up this file since it is not a route itself. We could even DRY this up a bit to where it could written to check against any model instead of just the Goal model.

Loading