Skip to content
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

task-list-project-paigeh #131

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,6 @@ dmypy.json
.pytype/

# Cython debug symbols
cython_debug/
cython_debug/

.env
6 changes: 5 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def create_app(test_config=None):

if test_config is None:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
"RENDER_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
Expand All @@ -30,5 +30,9 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from app.routes.routes import tasks_bp
app.register_blueprint(tasks_bp)
from app.routes.goal_routes import goals_bp
Comment on lines +33 to +35

Choose a reason for hiding this comment

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

Nice choice to split up the routes into files that are more specific to the resources they work with!

app.register_blueprint(goals_bp)

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


class Goal(db.Model):
goal_id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
tasks = db.relationship("Task", back_populates="goal")

def to_dict(self, tasks=False):
goal_dict = {
"id": self.id,
"title": self.title
}

if tasks:
goal_dict["tasks"] = [task.to_dict() for task in self.tasks]

return goal_dict

24 changes: 23 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
from app import db
from datetime import datetime


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
description = db.Column(db.String)
completed_at = db.Column(db.DateTime, nullable=True)
goal = db.relationship("Goal", back_populates="tasks")
goal_id = db.Column(db.Integer, db.ForeignKey('goal.id'), nullable=True)

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

Choose a reason for hiding this comment

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

Great choice to derive the value for is_complete from completed_at!

I want to share a gentle reminder that we should use is or is not when comparing objects to None in python. Another option could be to use the python bool function, what could that look like?

}
if self.goal_id:
task_as_dict["goal_id"] = self.goal_id

return task_as_dict

# @classmethod
# def from_dict(cls, task_data):
# return cls(title=task_data["title"], description=task_data["description"])
Comment on lines +25 to +27

Choose a reason for hiding this comment

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

Gentle reminder that we want to clean up commented code before opening PRs - we can look up our past commits on GitHub if we need to take a look at a previous state =]

1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

76 changes: 76 additions & 0 deletions 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.

Really nicely divided up routes overall, there a a few long lines, but the use of spacing and naming makes things easy to quickly read and understand =]

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from flask import Blueprint, jsonify, abort, make_response, request
from app.models.goal import Goal
from app.models.task import Task
from app.routes.routes import validate_model
from datetime import datetime
from app import db

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

@goals_bp.route("", methods=["POST"])
def create_goal():
request_body = request.get_json()
if "title" not in request_body:
abort(make_response({"details": "Invalid data"}, 400))


new_goal = Goal(title=request_body["title"])
db.session.add(new_goal)
db.session.commit()
return make_response(jsonify({"goal": new_goal.to_dict()}), 201)

@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_tasks_for_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()
# if "tasks_ids" not in request_body:
# abort(make_response({"details": "Invalid data"}, 400))

task_ids = request_body["task_ids"]
for task_id in task_ids:
task = validate_model(Task, task_id)
task.goal_id = goal.id

db.session.commit()
return make_response({"id": goal.id, "task_ids": task_ids}, 200)

@goals_bp.route("", methods=["GET"])
def read_all_goals():
goals = Goal.query.all()
goal_response = []

for goal in goals:
goal_response.append(goal.to_dict())
return jsonify(goal_response)

@goals_bp.route("/<goal_id>", methods=["GET"])
def read_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
return make_response(jsonify({"goal":goal.to_dict()}), 200)


@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def read_tasks_for_one_goal(goal_id):
goal = validate_model(Goal, goal_id)
return make_response(goal.to_dict(tasks=True), 200)



@goals_bp.route("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):
goal = validate_model(Goal, goal_id)
request_body = request.get_json()

goal.title = request_body["title"]

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

@goals_bp.route("/<goal_id>", methods=["DELETE"])
def delete_goal(goal_id):
goal = validate_model(Goal, goal_id)

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

return make_response({"details": 'Goal 1 "Build a habit of going outside daily" successfully deleted'}, 200)

Choose a reason for hiding this comment

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

How might we split this line up to keep it under 79 characters? What other lines across the project would be nice to split up for readability?

117 changes: 117 additions & 0 deletions app/routes/routes.py

Choose a reason for hiding this comment

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

Since this file mostly holds task routes, I suggest updating the name to reflect that.

Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from flask import Blueprint, jsonify, abort, make_response, request
from app.models.task import Task
from datetime import datetime
import os
import requests
from dotenv import load_dotenv
from app import db

load_dotenv()
SLACK_TOKEN = os.environ.get("SLACK_TOKEN")
tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks")


def validate_model(cls, model_id):
try:
model_id = int(model_id)
except:
abort(make_response({"message":f"{cls.__name__.lower()} {model_id} invalid data"}, 400))

task = cls.query.get(model_id)

if not task:
abort(make_response({"message":f"{cls.__name__.lower()} {model_id} not found"}, 404))

return task

def send__slack_msg(task_title):
slack_url = "https://slack.com/api/chat.postMessage"
headers = {
"Authorization": f"Bearer {SLACK_TOKEN}"
}
data = {
"channel": "task-notifications",
"text": f"Someone just completed the task {task_title}"
}

return requests.post(slack_url, headers=headers, data=data)



@tasks_bp.route("", methods=["POST"])
def create_task():
request_body = request.get_json()
if "title" not in request_body or "description" not in request_body:
return make_response({"details": "Invalid data"}, 400)

new_task = Task(title=request_body["title"],
description=request_body["description"],
completed_at=None)

db.session.add(new_task)
db.session.commit()

return make_response(jsonify({"task": new_task.to_dict()}), 201)


@tasks_bp.route("", methods=["GET"])
def read_all_tasks():
# tasks = Task.query.get.all()
sort = request.args.get("sort")
if sort == "asc":
tasks = Task.query.order_by(Task.title.asc()).all()
else:
tasks = Task.query.order_by(Task.title.desc()).all()
tasks_response = []

for task in tasks:
tasks_response.append(task.to_dict())
return jsonify(tasks_response)

@tasks_bp.route("/<task_id>", methods=["GET"])
def read_one_task(task_id):
task = validate_model(Task, task_id)
return make_response(jsonify({"task":task.to_dict()}), 200)

@tasks_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):
task = validate_model(Task, task_id)
request_body = request.get_json()

task.title = request_body["title"]
task.description = request_body["description"]
task.completed_at = None

db.session.commit()
return make_response({"task":task.to_dict()}, 200)


Comment on lines +87 to +88

Choose a reason for hiding this comment

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

One of the things to look at when we are in our code clean up phase is whitespace across a file. The general guideline is one newline between functions inside of a class, 2 new lines for top level functions in a file - but what's most important is to be consistent across a codebase. A little extra whitespace is often used as a visual separation between groups of related functions, or to make it clear where imports end and implementation code begins. We lose the ability to have a visual separation have meaning if we are not consistent with how they are applied. The PEP 8 style guide has a little more info on their recommendations: https://peps.python.org/pep-0008/#blank-lines

@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def complete_task(task_id):
task = validate_model(Task, task_id)

if task.completed_at == None:
task.completed_at = datetime.now()
db.session.commit()

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

@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def incomplete_task(task_id):
task = validate_model(Task, task_id)

if task.completed_at != None:
task.completed_at = None
db.session.commit()

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


@tasks_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task = validate_model(Task, task_id)

db.session.delete(task)
db.session.commit()

return make_response({"details": 'Task 1 "Go on my daily walk 🏞" successfully deleted'}, 200)
1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
45 changes: 45 additions & 0 deletions migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 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

[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

[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