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

C16-Pine Alma #79

Open
wants to merge 5 commits into
base: master
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
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()'
20 changes: 16 additions & 4 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
from flask import Flask
# Step 1:
# Import and initialize SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv


# Initialize SQLAlchemy
db = SQLAlchemy()
migrate = Migrate()
DATABASE_CONNECTION_STRING='postgresql+psycopg2://postgres:postgres@localhost:5432/task_list_api_development'

Choose a reason for hiding this comment

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

Because you now get this string from environment variables you no longer need this line.

load_dotenv()


def create_app(test_config=None):
app = Flask(__name__)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = DATABASE_CONNECTION_STRING

if test_config is None:
# Step 2:
#Configure SQLAlchemy
if not test_config:
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI")
else:
app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_TEST_DATABASE_URI")

# Import models here for Alembic setup
# Import models
from app.models.task import Task
from app.models.goal import Goal

# Step 3:
# Hook up Flask & SQLALchemy
db.init_app(app)
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
21 changes: 21 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
from flask import current_app
from app import db
from sqlalchemy.orm import backref

from app.models.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)
#Task.query.get(goal_id) = a list of tasks with that matching goal_id
def to_dict(self):
return{
"id": self.goal_id,
"title": self.title
}
Comment on lines +13 to +17

Choose a reason for hiding this comment

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

Nice and clean!


def to_dict_with_tasks(self, goal_id):
tasks = Task.query.filter_by(fk_goal_id=f"{goal_id}")
task_list = []
for task in tasks:
task_list.append(task.to_dict())
return{
"id":self.goal_id,
"title":self.title,
}
Comment on lines +19 to +27

Choose a reason for hiding this comment

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

It looks like this isn't ever used and can be removed.

27 changes: 26 additions & 1 deletion app/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,29 @@


class Task(db.Model):
task_id = db.Column(db.Integer, primary_key=True)
__tablename__= "tasks"
task_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title=db.Column(db.String(200))
description=db.Column(db.String(200))
completed_at=db.Column(db.DateTime, nullable=True)
goal_id=db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True)

def to_dict(self):
if self.goal_id:
return {
"id":self.task_id,
"title": self.title,
"description": self.description,
"is_complete": True if self.completed_at else False,

Choose a reason for hiding this comment

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

Nice use of ternary operatory!

"goal_id": self.goal_id
}

else:
return{
"id":self.task_id,
"title": self.title,
"description": self.description,
"is_complete": True if self.completed_at else False
}
Comment on lines +14 to +29

Choose a reason for hiding this comment

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

This definitely works! Can you think of a way to restructure it so we set up most of the dictionary outside of the if/else and only add the "goal_id" key when needed? This could help reduce repetition in your code.



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


tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks")
goals_bp = Blueprint("goals", __name__, url_prefix="/goals")
env_path = Path('.')/ '.env'
load_dotenv()


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

if request.method == "POST":
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 make_response({"details":"Invalid data"}, 400)
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.

👍🏻


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

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

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

elif request.method == "GET":
task_response= []
if request.args.get('sort') == 'asc':
tasks = Task.query.order_by(Task.title.asc()).all()
elif request.args.get('sort') == 'desc':
tasks = Task.query.order_by(Task.title.desc()).all()
else:
tasks = Task.query.all()

for task in tasks:
task_response.append(task.to_dict())
Comment on lines +40 to +48

Choose a reason for hiding this comment

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

Very nice!

return jsonify(task_response), 200

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


if task is None:
return make_response(f"Task {task_id} not found"), 404
Comment on lines +53 to +57

Choose a reason for hiding this comment

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

I like how your code is structured so this initial logic can be re-used regardless of whether it's a GET, PUT, or DELETE.


if request.method == "GET":
return jsonify({"task": task.to_dict()}), 200

elif request.method == "PUT":
request_body = request.get_json()
if "title" not in request_body or "description" not in request_body:
return make_response("invalid request"), 400

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


db.session.commit()

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


elif request.method == "DELETE":
db.session.delete(task)
db.session.commit()

return ({'details': f'Task {task_id} "{task.title}" successfully deleted'}), 200

Choose a reason for hiding this comment

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

Nice use of f-strings and double vs single quotes!



@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def mark_complete(task_id):
task = Task.query.get(task_id)
today = date.today()
if task is None:
return make_response("", 404)
else:
task.completed_at = today
db.session.commit()
PATH = 'https://slack.com/api/chat.postMessage'
params = {"token": os.environ.get("SLACK_TOKEN"),
"channel":"task-notifications",
"text":f"Someone just completed the task {task.title}"
}
Comment on lines +93 to +96

Choose a reason for hiding this comment

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

Nice job getting the token from the env vars! One extra way to make your code more flexible would be to make the channel to post to an environment variable as well.

requests.post(PATH,data=params)
return jsonify({"task":task.to_dict()}), 200

@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def mark_incomplete(task_id):
task = Task.query.get(task_id)
today = date.today()
if task is None:
return make_response("", 404)
else:
task.completed_at = None
db.session.commit()

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

@goals_bp.route("", methods=["POST"])
def create_goal():
request_body=request.get_json()
if "title" not in request_body:
return jsonify({"details": "Invalid data"}), 400
new_goal=Goal(
title = request_body["title"]
)
db.session.add(new_goal)
db.session.commit()


return jsonify({"goal": {"id": new_goal.goal_id, "title": new_goal.title}}), 201


@goals_bp.route("", methods=["GET"])
def get_goals():
request_body=request.get_json()

goals = Goal.query.all()
goal_response=[]
for goal in goals:
goal_response.append(goal.to_dict())
if not goal:
return make_response("", 404)
Comment on lines +135 to +136

Choose a reason for hiding this comment

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

We don't need this part. Goal.query.all() should only give us valid goals back.


return jsonify(goal_response), 200


@goals_bp.route("/<goal_id>", methods=["GET", "PUT", "DELETE"])
def handle_goal(goal_id):
goal_id=int(goal_id)

Choose a reason for hiding this comment

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

Make sure to do input validation here - if the user passed in an invalid goal_id this code would crash.

goal = Goal.query.get(goal_id)
if not goal:
return make_response("", 404)
if request.method == "GET":
return jsonify({"goal": goal.to_dict()}), 200

elif request.method == "PUT":
input_data = request.get_json()

goal.title=input_data["title"]
db.session.commit()

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

elif request.method == "DELETE":
db.session.delete(goal)
db.session.commit()

return ({'details': f'Goal {goal.goal_id} "{goal.title}" successfully deleted'}), 200

@goals_bp.route("/<goal_id>/tasks", methods=["GET", "POST"])
def handles_both(goal_id):
goal = Goal.query.get(goal_id)
goals_response=[]
if goal is None:
abort(404)
if request.method == "GET":
answer = {
"id": goal.goal_id,
"title": goal.title,
"tasks": [task.to_dict() for task in goal.tasks]
}
Comment on lines +171 to +175

Choose a reason for hiding this comment

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

Gorgeous!

return jsonify(answer), 200
elif request.method == "POST":
request_body=request.get_json()

task_ids = request_body["task_ids"]
for task_id in task_ids:

task = Task.query.get(task_id)
goal.tasks.append(task)

db.session.commit()
return jsonify({"id":goal.goal_id, "task_ids": [task.task_id for task in goal.tasks]}), 200

Choose a reason for hiding this comment

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

Very nice!


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