-
Notifications
You must be signed in to change notification settings - Fork 97
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
base: master
Are you sure you want to change the base?
C16-Pine Alma #79
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
web: gunicorn 'app:create_app()' |
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' | ||
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 |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this isn't ever used and can be removed. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need this part. |
||
|
||
return jsonify(goal_response), 200 | ||
|
||
|
||
@goals_bp.route("/<goal_id>", methods=["GET", "PUT", "DELETE"]) | ||
def handle_goal(goal_id): | ||
goal_id=int(goal_id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very nice! |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Generic single-database configuration. |
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 |
There was a problem hiding this comment.
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.