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
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn 'app:create_app()'
5 changes: 4 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here

from .routes import tasks_bp
app.register_blueprint(tasks_bp)
from .routes import goals_bp
app.register_blueprint(goals_bp)
return app
18 changes: 18 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
from flask import current_app
from sqlalchemy.orm import backref
from app import db
from .task import Task


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

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

def task_list(self):

Choose a reason for hiding this comment

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

Nice work encapsulating this functionality in an instance method.

task_list = []

for task in self.tasks:
task_list.append(task.to_dict())

return task_list
23 changes: 21 additions & 2 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
from flask import current_app
from flask import current_app, jsonify
from app import db

import requests
import os
#from app.models.goal import Goal

class Task(db.Model):
task_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)
is_complete = db.Column(db.Boolean)

Choose a reason for hiding this comment

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

is_complete should not be a separate attribute. is_complete us False when completed_at is None and True when completed_at has a value as you calculated below on line 20 (nice work there!). The danger in adding is_complete as another attribute on the task model is that we could store data that is inconsistent

goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)

def to_dict(self):
task_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": self.completed_at is not None,
}
if self.goal_id:
task_dict["goal_id"] = self.goal_id

return task_dict
231 changes: 230 additions & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,231 @@
from flask import Blueprint
from app import db
from app.models.task import Task
from app.models.goal import Goal
from flask import Blueprint, jsonify, request, abort, make_response
from datetime import datetime, timezone
import os
from dotenv import load_dotenv
import requests

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

def valid_int(number, parameter_type):
try:
int(number)
except:
abort(make_response({"error": f"{parameter_type} must be an int"})), 400

def get_task_from_id(task_id):
valid_int(task_id, "task_id")
return Task.query.get_or_404(task_id, description="{task not found}")
Comment on lines +18 to +20

Choose a reason for hiding this comment

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

Consider implementing a similar method for goals


@tasks_bp.route("", methods=["POST"])
def post_new_task():

request_body = request.get_json()
if "title" not in request_body or "description" not in request_body\
or "completed_at" not in request_body:
return jsonify({"details": "Invalid data"}), 400

new_task = Task(title=request_body["title"],
Comment on lines +26 to +30

Choose a reason for hiding this comment

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

Consider encapsulating this functionality in a helper method that either aborts for invalid data or returns the new_task if the request body is valid

description=request_body["description"],
completed_at=request_body["completed_at"])

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

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

@tasks_bp.route("/<task_id>", methods=["GET"])
def getting_one_task(task_id):
task = Task.query.get(task_id)

if task is None:
return jsonify(None), 404
Comment on lines +44 to +47

Choose a reason for hiding this comment

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

To DRY up your code, you could use the get_task_from_id helper function here.


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

@tasks_bp.route("", methods=["GET"])
def getting_tasks():
query = Task.query # Base query

# Query params, adding to query where indicated
sort = request.args.get("sort")
if sort == "asc":
query = query.order_by(Task.title)
elif sort == "desc":
query = query.order_by(Task.title.desc())

query = query.all() # Final query

Choose a reason for hiding this comment

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

Consider naming query tasks to enhance readability.

Suggested change
query = query.all() # Final query
tasks = query.all() # Final query
Suggested change
query = query.all() # Final query
query = query.all() # Final query


# Returns jsonified list of task dicionaries
return jsonify([task.to_dict() for task in query])

@tasks_bp.route("/<task_id>", methods=["PUT"])
def put_task(task_id):
task = Task.query.get(task_id)
if task is None:
return jsonify(None), 404

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 jsonify(response_body), 200

@tasks_bp.route("/<task_id>", methods=["DELETE"])
def delete_task(task_id):
task = Task.query.get(task_id)
if task is None:
return jsonify(None), 404
Comment on lines +89 to +91

Choose a reason for hiding this comment

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

Here is another place you can use the helper function get_task_from_id


db.session.delete(task)
db.session.commit()
return jsonify({
'details': f'Task {task.task_id} "{task.title}" successfully deleted'
}), 200

# Wave 3/new endpoints updates task as complete
@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def update_task_completion(task_id):
task= get_task_from_id(task_id)
task.is_complete=True
task.completed_at = datetime.now()
db.session.commit()

slack_key = os.environ.get("SLACK_KEY")
slack_url = os.environ.get("SLACK_URL")
channel_id = os.environ.get("CHANNEL_ID")
Comment on lines +108 to +109

Choose a reason for hiding this comment

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

CHANNEL_ID and SLACK_URL do not need to be secret. Consider saving them as a constant rather than in the .env

requests.post(slack_url, headers= {'Authorization': f"Bearer {slack_key}"}, \
data= {'channel' : f"{channel_id}", 'text' : f"Someone just completed the task My Beautiful Task"})
Comment on lines +107 to +111

Choose a reason for hiding this comment

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

Great work implementing this functionality. Consider encapsulating in a helper function to enhance readability and flexibility (You may want to use this function when someone updates a task, for instance).

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

# route that will mark item as incomplete
@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def update_task_incomplete(task_id):
task= get_task_from_id(task_id)

task.is_complete=False
task.completed_at=None
# db.session.add(task)
db.session.commit()
return jsonify({"task": task.to_dict()}), 200

##########GOAL_ROUTES############
#################################

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

@goals_bp.route("", methods=["POST"]) #my decorator
def create_goal():
request_body = request.get_json()

if "title" not in request_body:
return {"details": "Invalid data"}, 400

new_goal = Goal(
title=request_body["title"]
)

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

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

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

goal_list = []

if not goals:
return jsonify(goal_list), 200

for goal in goals:
goal_list.append(goal.to_dict())

return jsonify(goal_list), 200

@goals_bp.route("/<goal_id>/tasks", methods=["GET"])
def read_tasks_for_goal(goal_id):
goal = Goal.query.get(goal_id)

if not goal:
return "", 404

return {
"id": goal.goal_id,
"title": goal.title,
"tasks": goal.task_list()

Choose a reason for hiding this comment

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

Great use of an instance method!

}, 200

@goals_bp.route("/<goal_id>", methods=["GET"])
def get_goal(goal_id):
goal = Goal.query.get(goal_id)

if goal is None:
return jsonify(None), 404

response_body = {
"goal": (goal.to_dict())
}

return jsonify(response_body), 200

@goals_bp.route("/<goal_id>/tasks", methods=["POST"])
def create_tasks_for_goal(goal_id):
request_body = request.get_json()

goal = Goal.query.get(goal_id)

if not goal:
return "", 404

for id in request_body['task_ids']:
goal.tasks.append(Task.query.get(id))

db.session.commit()

return {
"id": goal.goal_id,
"task_ids": request_body['task_ids']
}, 200

@goals_bp.route("/<goal_id>", methods=["PUT", "PATCH"])
def update_goal(goal_id):
goal = Goal.query.get(goal_id)

if not goal:
return "", 404

request_body = request.get_json()

goal.title = request_body["title"]

db.session.commit()

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

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

if not goal:
return "", 404

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

return {"details":
f'Goal {goal.goal_id} "{goal.title}" 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