Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
a5d2d10
Adds Task model
Nerpassevera Nov 5, 2024
8ad364a
Adds Task blueprint
Nerpassevera Nov 5, 2024
11048e4
Updates migration files
Nerpassevera Nov 5, 2024
20dabc8
Adds create_task to task_routes.py
Nerpassevera Nov 5, 2024
af10725
Adds get_one_task function
Nerpassevera Nov 5, 2024
1a41d60
Adds get_all_tasks function
Nerpassevera Nov 5, 2024
4d17fc6
refactor: Adds Task class method from_dict
Nerpassevera Nov 5, 2024
a6094b6
Adds edit_task function
Nerpassevera Nov 6, 2024
7d877be
Adds delete_task function
Nerpassevera Nov 6, 2024
991f860
Fixes tests for wave 1
Nerpassevera Nov 6, 2024
055a22f
Adds sorting order to get_all_tasks
Nerpassevera Nov 6, 2024
2858943
Adds complete and incomplete tasks finctionality
Nerpassevera Nov 6, 2024
3c6bb1f
Adds message sending functionality to mark_task_completed
Nerpassevera Nov 7, 2024
ededb67
Creates Goal model
Nerpassevera Nov 7, 2024
4bdfaec
refactor: Creates validate_model and set_new_attributes route utilities
Nerpassevera Nov 7, 2024
0589dab
feat: Creates goal routes
Nerpassevera Nov 7, 2024
360a353
Adds tests for goal_routes functions
Nerpassevera Nov 7, 2024
23f70c1
Adds /tasks GET and POST routes to Blueprint Goal
Nerpassevera Nov 8, 2024
b4031cc
refactor: removes abondend nullable
Nerpassevera Nov 8, 2024
d96de8b
refactor: minor changes
Nerpassevera Nov 9, 2024
81eb790
refactor: Creates utility func create_class_instance
Nerpassevera Nov 10, 2024
e328024
refactor: Creates utility func get_all_instances
Nerpassevera Nov 10, 2024
d2fbaf4
refactor: Creates utility func get_one_instance
Nerpassevera Nov 10, 2024
db350a8
refactor: Creates utility func update_instance
Nerpassevera Nov 10, 2024
b4c37a8
refactor: Creates utility func delete_instance
Nerpassevera Nov 10, 2024
a8c75bb
chore: remove print statements
Nerpassevera Nov 10, 2024
d910bdf
fix: a typo in Content_type
Nerpassevera Nov 10, 2024
490d343
refactor: Adds descriptive error message to create_class_instance
Nerpassevera Nov 10, 2024
94df535
Adds Travis CI config file
Nerpassevera Nov 20, 2024
cb0af09
Changes python v in travis config
Nerpassevera Nov 20, 2024
e391da3
Changes python v in travis config
Nerpassevera Nov 20, 2024
e93fc24
Changes python v in travis config3
Nerpassevera Nov 20, 2024
edc1841
Changes python v in travis config 4
Nerpassevera Nov 20, 2024
0039a99
Changes python v in travis config 5
Nerpassevera Nov 20, 2024
f9521b9
Changes python v in travis config 5
Nerpassevera Nov 20, 2024
3302239
Changes python v in travis config 6
Nerpassevera Nov 20, 2024
a6dcf4a
Changes python v in travis config 7
Nerpassevera Nov 20, 2024
b49f6a0
Changes python v in travis config 8
Nerpassevera Nov 20, 2024
2b73458
Changes python v in travis config 9
Nerpassevera Nov 20, 2024
4c54403
Changes python v in travis config 10
Nerpassevera Nov 20, 2024
d32dfb3
Changes python v in travis config 11
Nerpassevera Nov 20, 2024
1ca3859
Changes python v in travis config 12
Nerpassevera Nov 20, 2024
4d92b7d
Changes python v in travis config 13
Nerpassevera Nov 20, 2024
3fee895
Changes python v in travis config 13 fixed
Nerpassevera Nov 20, 2024
c440a7b
Changes python v in travis config 14
Nerpassevera Nov 20, 2024
069d7ec
Changes python v in travis config 15
Nerpassevera Nov 20, 2024
583722c
Changes python v in travis config 16
Nerpassevera Nov 20, 2024
3457fe4
Changes python v in travis config 17
Nerpassevera Nov 21, 2024
ed09c67
Changes python v in travis config 18
Nerpassevera Nov 21, 2024
6716d2a
Changes python v in travis config
Nerpassevera Nov 20, 2024
a4943da
Merge branch 'main' of https://github.com/Nerpassevera/task-list-api
Nerpassevera Nov 21, 2024
82fc58d
Adds instruction for deploy on Render
Nerpassevera Nov 21, 2024
4eb37c0
Adds instruction for deploy on Render 2
Nerpassevera Nov 21, 2024
5755c17
Adds instruction for deploy on Render 3
Nerpassevera Nov 21, 2024
0db806a
Adds instruction for deploy on Render 4
Nerpassevera Nov 21, 2024
321b6bd
Adds instruction for deploy on Render 5
Nerpassevera Nov 21, 2024
485dac5
Adds instruction for deploy on Render 6
Nerpassevera Nov 21, 2024
a92db83
Adds instruction for deploy on Render 7
Nerpassevera Nov 21, 2024
2e4dc64
Adds instruction for deploy on Render 8
Nerpassevera Nov 21, 2024
10c2c39
Breaking the code
Nerpassevera Dec 2, 2024
721fe94
Fixing the code
Nerpassevera Dec 2, 2024
52da2dc
Breaking the code
Nerpassevera Dec 2, 2024
2ffdc3f
Fixing the code
Nerpassevera Dec 2, 2024
a2bf5ed
Fixed to work with local frontend
Nerpassevera Dec 18, 2024
1b51387
Add render.yaml
Nerpassevera Dec 18, 2024
09f89e9
log for debugging the server version of backend
Nerpassevera Jan 4, 2025
363fb62
configurates CORS
Nerpassevera Jan 4, 2025
5ceb059
configurates CORS
Nerpassevera Jan 4, 2025
f22a456
fixing notofocations in deployed BE
Nerpassevera Jan 4, 2025
acca02a
fixing notofocations in deployed BE
Nerpassevera Jan 4, 2025
c2e586d
fixing notofocations in deployed BE
Nerpassevera Jan 4, 2025
5c178dd
fixing notofocations in deployed BE
Nerpassevera Jan 4, 2025
53f779c
fixing notofocations in deployed BE
Nerpassevera Jan 4, 2025
6052392
fixing notofocations in deployed BE
Nerpassevera Jan 4, 2025
b15e2d0
Merge branch 'main' of https://github.com/Nerpassevera/task-list-api
Nerpassevera Jan 5, 2025
011e936
adds gh-pages address to CORS configs
Nerpassevera Jan 5, 2025
c904e75
Updates README
Nerpassevera Jan 5, 2025
051049f
Create LICENSE
Nerpassevera Jan 5, 2025
6507919
Updates README
Nerpassevera Jan 5, 2025
4697786
Merge branch 'main' of https://github.com/Nerpassevera/task-list-api
Nerpassevera Jan 5, 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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,7 @@ dmypy.json
.pytype/

# Cython debug symbols
cython_debug/
cython_debug/

# Slack bot logo
slack_bot_logo.png
4 changes: 4 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from flask import Flask
from .routes.task_routes import bp as task_bp
from .routes.goal_routes import bp as goal_bp
Comment on lines +4 to +5

Choose a reason for hiding this comment

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

👍

from .db import db, migrate
from .models import task, goal
import os
Expand All @@ -18,5 +20,7 @@ def create_app(config=None):
migrate.init_app(app, db)

# Register Blueprints here
app.register_blueprint(task_bp)
app.register_blueprint(goal_bp)

return app
16 changes: 15 additions & 1 deletion app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import String
from ..db import db

class Goal(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(50))

Choose a reason for hiding this comment

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

Consider whether the this column for Goal should be nullable. It feels odd to allow someone to create a goal that gets saved to the DB without a title.

tasks: Mapped[list["Task"]] = relationship(back_populates="goal")

def to_dict(self):
return {
"id": self.id,
"title": self.title
}

@classmethod
def from_dict(cls, data):
return Goal(title=data["title"])

Choose a reason for hiding this comment

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

While we can explicitly use Goal here, we can also use the cls keyword, like:

Suggested change
return Goal(title=data["title"])
return cls(title=data["title"])


35 changes: 34 additions & 1 deletion app/models/task.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
from sqlalchemy import String, DateTime, ForeignKey
from datetime import datetime
from typing import Optional
from ..db import db

class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(50))

Choose a reason for hiding this comment

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

As with Goal, consider which of the columns for Task should be nullable and which should not be.

description: Mapped[str] = mapped_column(String(255))
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id"))
goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks")

def to_dict(self):
task_dict = {
"id": self.id,
"title": self.title,
"description": self.description,
"is_complete": bool(self.completed_at)
}

if self.goal_id is not None:
task_dict["goal_id"] = self.goal_id

return task_dict

@classmethod
def from_dict(cls, data):
data["completed_at"] = data.get("completed_at", None)

task = Task(
title=data["title"],
description=data["description"],
completed_at=data["completed_at"],
)

return task
Comment on lines +32 to +38

Choose a reason for hiding this comment

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

Since you don't reference the variable task after you initialize it and before you return it, you can just directly return the instance of task without making a variable:

Suggested change
task = Task(
title=data["title"],
description=data["description"],
completed_at=data["completed_at"],
)
return task
return cls(
title=data["title"],
description=data["description"],
completed_at=data["completed_at"],
)

58 changes: 57 additions & 1 deletion app/routes/goal_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,57 @@
from flask import Blueprint
from flask import Blueprint, request, abort, make_response

Choose a reason for hiding this comment

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

abort and make_response are imported but not accessed so they should be removed.

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

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


@bp.post("/", strict_slashes=False)
def create_goal():
return create_class_instance(Goal, request, ["title"])

Choose a reason for hiding this comment

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

The logic in create_class_instance doesn't access the list of required fields so I think ["title"] should be removed since we don't need to pass it to the helper function


@bp.get("/", strict_slashes=False)
def get_all_goals():
return get_all_instances(Goal, request.args)

@bp.get("/<goal_id>", strict_slashes=False)
def get_one_goal(goal_id):
return get_one_instance(Goal, goal_id)

@bp.put("/<goal_id>", strict_slashes=False)
def update_goal(goal_id):
return update_instance(Goal, goal_id, request)

@bp.delete("/<goal_id>", strict_slashes=False)
def delete_goal(goal_id):
return delete_instance(Goal, goal_id)

@bp.post("/<goal_id>/tasks")
def assign_task_to_goal(goal_id):
goal = validate_model(Goal, goal_id)
task_ids = request.get_json().get("task_ids", [])
list_of_tasks = []

for task_id in task_ids:
task = validate_model(Task, task_id)
if task:
list_of_tasks.append(task)

goal.tasks.extend(list_of_tasks)
db.session.commit()

return {
"id": goal.id,
"task_ids": [task.id for task in goal.tasks]

Choose a reason for hiding this comment

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

I like that you get task ids from goal.tasks 👍 It's best to return a client a response that has data that we retrieve from our source of truth, the database.

Since goal is a record from the DB (line 32), then we know we're providing correct information.

You didn't do this, but if you had done "task_ids": task_ids where task_ids is data that we get from the request body on line 33, then we would just be echoing back to the client data that they sent to us in the request body (which isn't ideal).

}

@bp.get("/<goal_id>/tasks")
def get_task_of_goal(goal_id):
goal = validate_model(Goal, goal_id)

return {
"id": goal.id,
"title": goal.title,
"tasks": [task.to_dict() for task in goal.tasks]
}
73 changes: 73 additions & 0 deletions app/routes/route_utilities.py

Choose a reason for hiding this comment

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

Your helper functions look great. By encapsulating logic in generic helper functions that can be used across multiple routes, you keep your codebase DRY and your routes concise!

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from flask import abort, make_response
from app.db import db

def apply_filters(cls, arguments, query):
for attribute, value in arguments:
if hasattr(cls, attribute):
query = query.where(getattr(cls, attribute).ilike(f"%{value}%"))

def validate_model(cls, cls_id):
try:
cls_id = int(cls_id)
except ValueError:
message = { "message": f"{cls.__name__} ID {cls_id} is invalid"}
abort(make_response(message, 400))

query = db.select(cls).where(cls.id == cls_id)
result = db.session.scalar(query)

if not result:
message = {"message": f"{cls.__name__} with ID {cls_id} was not found"}
abort(make_response(message, 404))

return result

def set_new_attributes(instance, req_body):
for attr, value in req_body.items():
if hasattr(instance, attr):
setattr(instance, attr, value)

def create_class_instance(cls, request, required_fields):

Choose a reason for hiding this comment

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

Notice that your function definition has the param required_fields but logic never accesses it -- therefore the last param should be removed.

Choose a reason for hiding this comment

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

While this function does at some point create an instance of some class (line 33), it ultimately creates a record and saves it to the DB. Maybe a name like create_model_from_request would be more descriptive and self documenting.

req_body = request.get_json()
try:
new_instance = cls.from_dict(req_body)
except KeyError as error:
message = {"details": f"Invalid request: missing {error.args[0]}"}
abort(make_response(message, 400))

db.session.add(new_instance)
db.session.commit()

return {cls.__name__.lower(): new_instance.to_dict()}, 201

def get_all_instances(cls, args):
sort = args.get("sort")
query = db.select(cls).order_by(cls.title.desc() if sort=="desc" else cls.title)

apply_filters(cls, args.items(), query)

instances = db.session.scalars(query)
return [instance.to_dict() for instance in instances], 200

def get_one_instance(cls, instance_id):
instance = validate_model(cls, instance_id)

return { cls.__name__.lower(): instance.to_dict() if instance else instance}, 200

def update_instance(cls, instance_id, request):
instance = validate_model(cls, instance_id)
req_body = request.get_json()
if cls.__name__ == "Task":
req_body["completed_at"] = req_body.get("completed_at", None)

set_new_attributes(instance, req_body)

db.session.commit()
return { cls.__name__.lower(): instance.to_dict() }, 200

def delete_instance(cls, instance_id):
instance = validate_model(cls, instance_id)
db.session.delete(instance)
db.session.commit()

return {"details": f'{cls.__name__} {instance.id} "{instance.title}" successfully deleted'}, 200
72 changes: 71 additions & 1 deletion app/routes/task_routes.py
Original file line number Diff line number Diff line change
@@ -1 +1,71 @@
from flask import Blueprint
from flask import Blueprint, request, make_response, abort

Choose a reason for hiding this comment

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

abort and make_response are imported but not accessed so they should be removed.

from datetime import datetime
from os import environ
import requests

from app.models.task import Task
from app.db import db
from app.routes.route_utilities import *

bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks")

@bp.post("/", strict_slashes=False)
def create_task():
return create_class_instance(Task, request, ["title", "description"])

Choose a reason for hiding this comment

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

Same comment as in goal_routes.py: looks like create_class_instance never accesses the last param required_fields so it should be removed

Suggested change
return create_class_instance(Task, request, ["title", "description"])
return create_class_instance(Task, request)


@bp.get("/", strict_slashes=False)
def get_all_tasks():
return get_all_instances(Task, request.args)

@bp.get("/<task_id>", strict_slashes=False)
def get_one_task(task_id):
return get_one_instance(Task, task_id)

@bp.put("/<task_id>", strict_slashes=False)
def update_task(task_id):
return update_instance(Task, task_id, request)

@bp.delete("/<task_id>", strict_slashes=False)
def delete_task(task_id):
return delete_instance(Task, task_id)

@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()

message_is_sent = send_task_complete_message(task.title)

if not message_is_sent:
raise Exception(
"An error occured during notification sending!\
Please connect to the Task List developer!")
return { "task": task.to_dict() }, 200

@bp.patch("/<task_id>/mark_incomplete")
def mark_task_incompleted(task_id):
task = validate_model(Task, task_id)
task.completed_at = None
db.session.commit()

return { "task": task.to_dict() }, 200

def send_task_complete_message(task_title):
request_data = {
"channel": "#api-test-channel", # Slack channel for tests
# "channel": "U07GC9C8Y4X", # My Slack account ID
"username": "Task List app",
Comment on lines +57 to +59

Choose a reason for hiding this comment

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

Prefer the string literals on lines 56 and 58 be referenced with constant variables like CHANNEL_NAME and USERNAME

"text": f"Someone just completed the task \"{task_title}\""
}
message_status = requests.post(
url="https://slack.com/api/chat.postMessage",

Choose a reason for hiding this comment

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

Prefer the URL to be referenced by a constant variable instead of having a string literal here.

URL = "https://slack.com/api/chat.postMessage" 
        url=URL, 

json=request_data,
headers={
"Authorization": environ.get('SLACK_API_KEY'),
"Content-Type": "application/json"

Choose a reason for hiding this comment

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

When you pass your payload as json, like you do on line 63, Flask will auto set the Contnet-Type to JSON so you don't need to

},
timeout=5
)

return message_status.json()["ok"]
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