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

Lion - Miranda #123

Open
wants to merge 14 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()'
8 changes: 7 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv
import os


db = SQLAlchemy()
Expand Down Expand Up @@ -30,5 +30,11 @@ def create_app(test_config=None):
migrate.init_app(app, db)

# Register Blueprints here
from app.tasks import task_bp
from app.goal import goal_bp

app.register_blueprint(task_bp)
app.register_blueprint(goal_bp)


return app
97 changes: 97 additions & 0 deletions app/goal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@

from flask import Blueprint, request, make_response, jsonify
from app import db
from app.models.task import Task
from app.models.goal import Goal
from app.routes_helper import get_one_obj_or_abort

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


# create goal
@goal_bp.route("", methods=["POST"])
def create_goal():
response_body = request.get_json()

if "title" not in response_body:
return jsonify({"details": "Invalid data"}), 400
Comment on lines +16 to +17

Choose a reason for hiding this comment

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

It's good that you're doing data validation, but you should probably indicate what data is invalid.


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

db.session.add(new_goal)
db.session.commit()
return jsonify({"goal":new_goal.return_body()}), 201


# Get Goals
@goal_bp.route("", methods=["GET"])
def read_goal():
goals = Goal.query.all()
response = [goal.return_body() for goal in goals]

Choose a reason for hiding this comment

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

Nice use of a list comprehension.

return jsonify(response), 200


# Get One Goal
@goal_bp.route("/<goal_id>", methods=["GET"])
def get_one_goal_by_id(goal_id):
chosen_goal= get_one_obj_or_abort(Goal, goal_id)
return jsonify({"goal":chosen_goal.return_body()}), 200


# Update Goal
@goal_bp.route("/<goal_id>", methods=["PUT"])
def update_goal(goal_id):
chosen_goal = get_one_obj_or_abort(Goal, goal_id)
request_body = request.get_json()

chosen_goal.title = request_body["title"]

db.session.commit()
return jsonify({"goal":chosen_goal.return_body()}), 200


# Delete Goal
@goal_bp.route("/<goal_id>", methods=["DELETE"])
def delete_goal_by_id(goal_id):
chosen_goal = get_one_obj_or_abort(Goal, goal_id)

db.session.delete(chosen_goal)
db.session.commit()

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




# ==================================================
# One-to-Many Relationship bewteen goals and tasks
# ==================================================

@goal_bp.route("/<goal_id>/tasks", methods=["POST"])
def post_task_belonging_to_a_goal(goal_id):
parent_goal = get_one_obj_or_abort(Goal, goal_id)
request_body = request.get_json()

for task in request_body["task_ids"]:
chosen_task = get_one_obj_or_abort(Task, task)
chosen_task.goal = parent_goal

db.session.add(chosen_task)
db.session.commit()

return jsonify({"id": int(goal_id), "task_ids": request_body["task_ids"]}), 200



@goal_bp.route("/<goal_id>/tasks", methods=["GET"])
def get_task_belonging_to_a_goal(goal_id):
parent_goal = get_one_obj_or_abort(Goal, goal_id)

task_list = []
for task in parent_goal.tasks:
task_list.append(task.return_body())

response_dict = parent_goal.return_body()
response_dict["tasks"] = task_list
return jsonify(response_dict), 200

18 changes: 18 additions & 0 deletions app/models/goal.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
from app import db



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


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

@classmethod
def from_dict(cls, data_dict):
if "title" in data_dict:
new_obj = cls(title=data_dict["title"])
return new_obj

Comment on lines +17 to +22

Choose a reason for hiding this comment

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

Good use of a class-method as a helper.


43 changes: 43 additions & 0 deletions app/models/task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
from app import db


# create the table with attributes
# after created, flask db migrate, flask db upgrade
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)
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'),nullable=True)
goal = db.relationship("Goal", back_populates="tasks")


def return_body(self):
task_dict = {
"id": self.task_id,
"title": self.title,
"description": self.description,
"is_complete": self.check_complete_or_not()
}
if self.goal_id:
task_dict["goal_id"] = self.goal_id

return task_dict


@classmethod
def from_dict(cls, data_dict):
if "title" in data_dict and\
"description" in data_dict and\
"completed_at" in data_dict:
new_obj = cls(
title=data_dict["title"],
description=data_dict["description"],
completed_at=data_dict["completed_at"]
)
return new_obj


def check_complete_or_not(self):
if self.completed_at:
is_complete = True
else:
is_complete = False
return is_complete


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

This file was deleted.

16 changes: 16 additions & 0 deletions app/routes_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from flask import jsonify, abort, make_response

def get_one_obj_or_abort(cls, obj_id):
try:
obj_id = int(obj_id)
except ValueError:
response_str = f"Invalid ID: '{obj_id}' must be an integer"
abort(make_response(jsonify({"message":response_str}), 400))

matching_obj = cls.query.get(obj_id)

if not matching_obj:
response_str = f"ID: '{cls.__name__}' was not found in the database"
abort(make_response(jsonify({"message":response_str}), 404))

return matching_obj
131 changes: 131 additions & 0 deletions app/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import requests, os
from datetime import datetime
from flask import Blueprint, request, make_response, jsonify
from app import db
from app.models.task import Task
from app.routes_helper import get_one_obj_or_abort


task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks")
goal_bp = Blueprint("goal_bp", __name__, url_prefix="/goals")


# Create a Task: Valid Task With null completed_at
@task_bp.route("", methods=["POST"])
def create_task():
response_body = request.get_json()

if "title" not in response_body or\
"description" not in response_body or\
"completed_at" not in response_body:
return jsonify({"details": "Invalid data"}), 400

new_task = Task.from_dict(response_body)

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

# using the class method in task.py
return jsonify({"task":new_task.return_body()}), 201


# Get Tasks: Getting Saved Tasks, sorting by ascending/descending
@task_bp.route("", methods=["GET"])
def read_task():
sort_query = request.args.get("sort")

if sort_query == "asc":
tasks = Task.query.order_by(Task.title.asc())
elif sort_query == "desc":
tasks = Task.query.order_by(Task.title.desc())
else:
tasks = Task.query.all()

response = [task.return_body() for task in tasks]
return jsonify(response), 200



# Get One Task: One Saved Task
@task_bp.route("/<task_id>", methods=["GET"])
def get_one_task_by_id(task_id):
chosen_task = get_one_obj_or_abort(Task, task_id)

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


# Update Task
@task_bp.route("/<task_id>", methods=["PUT"])
def update_task(task_id):
chosen_task = get_one_obj_or_abort(Task, task_id)
request_body = request.get_json()
chosen_task.title = request_body["title"]
chosen_task.description = request_body["description"]

db.session.commit()
return jsonify({"task":chosen_task.return_body()}), 200



# Deleting a Task
@task_bp.route("/<task_id>", methods=["DELETE"])
def delete_one_task(task_id):
task_to_delete = get_one_obj_or_abort(Task, task_id)

db.session.delete(task_to_delete)
db.session.commit()

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


@task_bp.route("/<task_id>/mark_complete", methods=["PATCH"])
def mark_complete_update(task_id):
chosen_task = get_one_obj_or_abort(Task, task_id)
task = Task.query.get(task_id)
if task is None:
return make_response("The task was not found", 404)
task.completed_at = datetime.now()
db.session.commit()

PATH = "https://slack.com/api/chat.postMessage"

SLACKBOT_TOKEN = os.environ.get("SLACKBOT_TOKEN")

# the query parameters come from the
query_params = {
"token": SLACKBOT_TOKEN,
"channel": "task-notifications",
"text": f"Someone just completed the task {task.title}"
}

requests.post(url=PATH, data=query_params, headers={"Authorization": SLACKBOT_TOKEN})
Comment on lines +90 to +101

Choose a reason for hiding this comment

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

The slack portion of this method might do well as a helper function.

# POST: to submit data to be processed to the server.

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


@task_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"])
def mark_incomplete_update(task_id):
chosen_task = get_one_obj_or_abort(Task, task_id)
task = Task.query.get(task_id)
if task is None:
return make_response("The task was not found", 404)
task.completed_at = None
db.session.commit()
return jsonify({"task":chosen_task.return_body()}), 200


# helper function to check the value of completed_at
# def check_task_status(goal_id, result):
# chosen_task = validate_task(goal_id)
# task = Task.query.get(goal_id)
# if task is None:
# return make_response("The task was not found", 404)
# task.complete_at = result
# db.session.commit()
# return jsonify({"task":chosen_task.return_body()}), 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