Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
afa229d
added post, and get functionality to task. Works on postman but not o…
MBsea21 Nov 1, 2024
a75034a
test_get_task, test_get_task_not_found passed.
MBsea21 Nov 1, 2024
9b81c64
test_create_task, test_update_task, test_update_task_not_found, test_…
MBsea21 Nov 1, 2024
f20ddf9
wave 1 tests all passed. Added 404, 400 error code functionality.
MBsea21 Nov 1, 2024
3fe589c
wave two tests passed
MBsea21 Nov 6, 2024
37ee6e6
wave 3 all tests passed
MBsea21 Nov 6, 2024
5dae0b9
added utilities folder which holds create_model, validate_model, delt…
MBsea21 Nov 8, 2024
0aca135
have gotten all tests up until the final two tests in wave 6 complete…
MBsea21 Nov 8, 2024
f80a1f2
wave six, all tests have succesfully passed, everything works so far!…
MBsea21 Nov 8, 2024
7c65e6b
Merge pull request #1 from MBsea21/madeline/backup-branch
MBsea21 Nov 8, 2024
92c6133
update with gunicorn
MBsea21 Nov 8, 2024
7e22320
updated slack-routes to be able to correctly call an env value
MBsea21 Nov 8, 2024
5de99b7
updated slack-token calling functionality
MBsea21 Nov 8, 2024
305c76e
another one-- working on fixing slack post functionality
MBsea21 Nov 8, 2024
34f623b
continued troubleshooting
MBsea21 Nov 8, 2024
9be98f8
fixed typo in os.getenv command
MBsea21 Nov 8, 2024
c327580
forgot to save last file, updated task_routes and saved this time
MBsea21 Nov 8, 2024
d2ad3aa
reworked utilities sorting function and updated the task get function…
MBsea21 Nov 8, 2024
8a8a0f5
updated slack api call to use requests
MBsea21 Nov 8, 2024
bba559b
trying to remove slack post blueprint
MBsea21 Nov 8, 2024
5dfb50c
removed excess http request, post to slack api is functional locally,…
MBsea21 Nov 9, 2024
08b6e37
removed erraneous imports
MBsea21 Nov 9, 2024
3affb57
made edits requested by Ansel in pull request #35
MBsea21 Feb 18, 2025
01179ea
Fixed additional typos
MBsea21 Feb 18, 2025
daf5dd9
fixed bugs, redid installfest so everything could run properly. all t…
MBsea21 Feb 19, 2025
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
8 changes: 7 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from flask import Flask
from flask import Flask, request, jsonify
from .db import db, migrate
from .models import task, goal

Choose a reason for hiding this comment

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

Once we have other import paths to our models (here, through the blueprints, which import the models), we technically don't need the imports here any more. It's fine to leave them for clarity (and a reminder that any other models we add would need to be included until other routes are setup), but if the VS Code warning is bothersome, feel free to remove these.

from .routes.task_routes import bp as tasks_bp
from .routes.goal_routes import bp as goals_bp
import os

def create_app(config=None):
Expand All @@ -18,5 +20,9 @@ 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

26 changes: 25 additions & 1 deletion app/models/goal.py
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 datetime import datetime
from app.routes.utilities_routes import create_model, validate_model
from typing import Optional
from sqlalchemy import ForeignKey
# from app.models.task import Task
from ..db import db

class Goal(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str]
tasks: Mapped[list["Task"]] = relationship("Task",back_populates="goal")



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

return goal_as_dict


@classmethod
def from_dict(cls, goal_data):
new_goal = cls(
title=goal_data["title"]
)
return new_goal
42 changes: 41 additions & 1 deletion app/models/task.py
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 ..db import db
from datetime import datetime
from typing import Optional
from sqlalchemy import ForeignKey

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]]
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped["Goal"] = relationship("Goal", back_populates="tasks")



def to_dict(self):
task_as_dict = {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": check_for_completion(self)

Choose a reason for hiding this comment

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

If check_for_completion is an instance method, call it as

            "is_complete": self.check_for_completion()

Python will shuffle things around internally.

}
if self.goal_id:
task_as_dict["goal_id"] = self.goal_id
Comment on lines +24 to +25

Choose a reason for hiding this comment

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

Nice detection of goal membership to detemine whether to include the goal information.


return task_as_dict


@classmethod
def from_dict(cls, task_data):
goal_id = task_data.get("goal_id")

Choose a reason for hiding this comment

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

Nice way to optionally allow a taks to be created as already belonging to a goal.


new_task = cls(
title=task_data["title"],
description=task_data["description"],
goal_id = goal_id
)
return new_task

def check_for_completion(self):
if self.completed_at is not None:
return True
else :
return False
Comment on lines +41 to +45

Choose a reason for hiding this comment

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

If we're passing self, prefer this to be an actual instnce method (indent it into the class).

Nit: no space between else and :

    def check_for_completion(self):
        if self.completed_at is not None:
            return True
        else:
            return False

Empty file added app/routes/__init__.py
Empty file.
108 changes: 107 additions & 1 deletion app/routes/goal_routes.py

Choose a reason for hiding this comment

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

Remember to always create a __init__.py in any new folders you create. It may appear that the imports are working correctly, but there are corner cases where this can become a problem. So the recommendation is to always make the __init__.py.

Original file line number Diff line number Diff line change
@@ -1 +1,107 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, request, Response

from app.models.goal import Goal
from app.models.task import Task
from ..db import db
from datetime import datetime
from app.routes.utilities_routes import *

Choose a reason for hiding this comment

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

Prefer to explicitly list the symbols being imported rather than using import *


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


####################################################################
######################### Create FUNCTIONS #########################
####################################################################
@bp.post("")
def create_goal():
request_body = request.get_json()

if not request_body or not request_body.get("title"):
return make_response({"details": "Invalid data: missing title"}, 400)

goal = create_model(Goal, request_body)
return make_response(goal, 201)
Comment on lines +22 to +23

Choose a reason for hiding this comment

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

With the create_model refactor, create_model is already creating an appropriate response, so we can return it as

    return create_model(Goal, request_body)



@bp.post("/<goal_id>/tasks")
def post_task_ids_to_goal(goal_id):
request_body = request.get_json()
goal = validate_model(Goal, goal_id)

task_ids = request_body.get("task_ids", [])
for task_id in task_ids:
task = validate_model(Task, task_id)
goal.tasks.append(task)

db.session.commit()

response_body = {
"id": goal.id,
"task_ids": [task.id for task in goal.tasks]
}


return response_body, 200


####################################################################
######################### READ FUNCTIONS #########################
####################################################################

@bp.get("")
def get_goals():
request_arguements = request.args
goals = get_models_with_filters(Goal, request_arguements)
if not goals:
return make_response([], 200)
Comment on lines +55 to +56

Choose a reason for hiding this comment

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

This check is redundant. goals will alreayd be an empty list is nothing was found, so the return on line 58 willwork identically.


return make_response(goals, 200)

@bp.get("/<goal_id>")
def get_one_goal(goal_id):
try:
goal = validate_model(Goal, goal_id)
return make_response({"goal": goal.to_dict()}, 200)
except:
return make_response({"details": f"Goal {goal_id} not found"}, 404)
Comment on lines +65 to +66

Choose a reason for hiding this comment

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

The try/except his unnecessary. validate_model will already take care of generating the 404 response and halting further processing of the request.


@bp.get("/<goal_id>/tasks")
def get_tasks_for_specific_goal(goal_id):
goal = validate_model(Goal, goal_id)
response_body = goal.to_dict()
response_body["tasks"] = [task.to_dict() for task in goal.tasks]
return response_body, 200

####################################################################
######################### UPDATE FUNCTIONS #########################
####################################################################
@bp.put("/<goal_id>")
def update_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()

goal_title = request_body.get("title")
if not goal_title:
return make_response({"details": "Invalid request: missing title"}, 400)
goal.title = goal_title
db.session.commit()

return make_response({"goal": goal.to_dict()}, 200)


####################################################################
######################### DELETE FUNCTIONS #########################
####################################################################
@bp.delete("/<goal_id>")
def delete_goal(goal_id):
goal = validate_model(Goal, goal_id)
goal_id = goal.id
goal_title = goal.title

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

response_body = {
"details": f'Goal {goal_id} "{goal_title}" successfully deleted'
}
return make_response(response_body, 200)
27 changes: 27 additions & 0 deletions app/routes/slack_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from flask import request, jsonify, abort, make_response
import requests
import os


def send_message(message, slack_channel, slack_url, slack_api_token):
headers = {
'Authorization': f'Bearer {slack_api_token}',
'Content-Type': 'application/json'
}
payload = {
"channel": slack_channel,
"text": message
}

response = requests.post(slack_url, json=payload, headers= headers)
if response.status_code != 200 or response.json().get("ok") is not True:
print("Failed to send Slack message: error unknown")

def send_message_with_config(message):
slack_channel = os.environ.get("SLACK_CHANNEL")
slack_url = os.environ.get("SLACK_URL")
slack_api_token = os.getenv("SLACK_BOT_TOKEN")

return send_message(message, slack_channel, slack_url, slack_api_token)


92 changes: 91 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,91 @@
from flask import Blueprint
from flask import Blueprint, abort, make_response, request, Response
from app.models.task import Task
from app.routes.utilities_routes import create_model, validate_model, get_models_with_filters, delete_model
from app.routes.slack_functions import send_message_with_config
from datetime import datetime, timezone
from ..db import db
import os


bp = Blueprint("task_bp", __name__, url_prefix="/tasks")
invalid_data_response = ({"details": "Invalid data"}, 400)

#create a new task in database
@bp.post("")
def create_task():
request_body = request.get_json()
return create_model(Task, request_body)

@bp.get("")
def get_tasks():
request_arguments = request.args
return get_models_with_filters(Task, request_arguments), 200


#get task by task id:
@bp.get("/<task_id>")
def get_one_task(task_id):
task = validate_model(Task, task_id)
task_dict = task.to_dict()
response = {"task":task_dict}

return response,200

#update task
@bp.put("/<task_id>")
def update_task(task_id):
task = validate_model(Task, task_id)

request_body = request.get_json()
title = request_body.get("title")
description = request_body.get("description")

if not title or not description:
return {"error": "Missing required fields: 'title' or 'description'"}, 400
task.title = title
task.description = description

completed_at = request_body.get("completed_at", task.completed_at)
task.completed_at = completed_at

db.session.commit()

response = {"task":task.to_dict()}

Choose a reason for hiding this comment

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

Nit: there should be a space after the colon

return response, 200


#Delete task
@bp.delete("/<task_id>")
def delete_task(task_id):
task = validate_model(Task, task_id)
return delete_model(Task, task)


#route 2
@bp.patch("/<task_id>/mark_complete")
def mark_complete(task_id):
task = validate_model(Task, task_id)

utc_now = datetime.now(timezone.utc)
task.completed_at = utc_now
message = f"Task {task.title} has been marked as complete!"
slack_response= send_message_with_config(message)
db.session.commit()
response = {"task": task.to_dict()}
if slack_response == None:
# if slack_response.status_code != 200 or not slack_response.json().get("ok"):
# error_msg = slack_response.json().get("errror", "Unknown error")
print("slack_error : Failed to send slack Notification")

return response, 200


@bp.patch("/<task_id>/mark_incomplete")
def mark_incomplete(task_id):
task = validate_model(Task, task_id)
task.completed_at = None
db.session.commit()
response = {"task": task.to_dict()}
return make_response(response,200)


64 changes: 64 additions & 0 deletions app/routes/utilities_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from flask import abort, make_response
from app import db

def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
response = {"error": 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 = {"details": 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 as error:
response = {"details": "Invalid data"}
abort(make_response(response, 400))

db.session.add(new_model)
db.session.commit()

dict=cls.to_dict(new_model)

response = {((cls.__name__).lower()):dict}
return(make_response(response,201))

def get_models_with_filters(cls, filters=None):
query = db.select(cls)
if filters:
for attribute, value in filters.items():
if attribute != "sort":
if hasattr(cls,attribute):
query = query.where(getattr(cls, attribute).ilike(f"%{value}%"))
else:
if value == "asc":
query = query.order_by(cls.title.asc())
elif value == "desc":
query = query.order_by(cls.title.desc())
Comment on lines +43 to +47

Choose a reason for hiding this comment

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

Nice extension to handle the sort keyword along with attribute filtering.


models = db.session.scalars(query.order_by(cls.id))
models_response = [model.to_dict() for model in models]
return models_response


def delete_model(cls,model):
model_id = model.id
model_title = model.title

db.session.delete(model)
db.session.commit()

details = f"{cls.__name__} {model_id} \"{model_title}\" successfully deleted"
response_body = {"details": details}
return response_body

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