-
Notifications
You must be signed in to change notification settings - Fork 69
Paper - Whit #49
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
base: master
Are you sure you want to change the base?
Paper - Whit #49
Changes from all commits
ef1dd60
b724489
b7a3f5a
0b7d59e
07fd376
91931bd
e6a3816
a7b311e
fbc87ef
1ef0ca6
b0449e4
69ce6de
479ba43
f0cdea2
d45f4c3
1c515b5
a8d04e1
e0570bc
18cc1ea
0d5945a
30126f1
42e7774
e02c18b
db980dc
fc3efd6
84bc061
784773f
8e5404d
be930e8
3ef4d7d
47c647d
e34eb52
d5ccb01
7685c3a
96632bb
dd3ad40
bca49fc
1921b7a
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,6 +1,16 @@ | ||
| from flask import current_app | ||
| from flask import Flask, current_app | ||
| from flask_sqlalchemy import SQLAlchemy | ||
| from app import db | ||
| from app.models.task import Task # is this necessary? | ||
|
|
||
|
|
||
| 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) | ||
|
|
||
| def to_json(self): | ||
| return { | ||
| "id": self.goal_id, | ||
| "title": self.title | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,32 @@ | ||
| from flask import current_app | ||
| from flask import Flask, current_app | ||
| from flask_sqlalchemy import SQLAlchemy | ||
| from app import db | ||
|
|
||
|
|
||
| class Task(db.Model): | ||
| task_id = db.Column(db.Integer, primary_key=True) | ||
| task_id = db.Column(db.Integer, primary_key=True, autoincrement=True) | ||
| title = db.Column(db.String) | ||
| description = db.Column(db.String) | ||
| completed_at = db.Column(db.DateTime) | ||
| goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id'), nullable=True) | ||
|
|
||
| def is_task_complete(self): | ||
| if not self.completed_at: | ||
| return False | ||
| return True | ||
|
|
||
| def get_task_info(self): | ||
|
Comment on lines
+12
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. Great helper methods |
||
| task_info = { | ||
| "id": self.task_id, | ||
| "title": self.title, | ||
| "description": self.description, | ||
| "is_complete": self.is_task_complete(), | ||
| } | ||
| if self.goal_id: | ||
| task_info['goal_id'] = self.goal_id | ||
| return task_info | ||
|
|
||
| def from_json(self, request_body): | ||
|
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! |
||
| self.title = request_body['title'] | ||
| self.description = request_body['description'] | ||
| self.completed_at = request_body['completed_at'] | ||
| return self | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,281 @@ | ||
| from flask import Blueprint | ||
| from app import db | ||
| from app.models.task import Task | ||
| from app.models.goal import Goal | ||
| from flask import Blueprint, make_response, request, jsonify | ||
| from sqlalchemy import desc | ||
| from datetime import datetime | ||
| import requests | ||
| import os | ||
|
|
||
| tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") | ||
| goals_bp = Blueprint("goals", __name__, url_prefix="/goals") | ||
|
|
||
| ########################## | ||
| #### /tasks Endpoints #### | ||
| ########################## | ||
|
|
||
| @tasks_bp.route("", methods=["POST"]) | ||
| def post_new_task(): | ||
| request_body = request.get_json() | ||
|
|
||
| try: | ||
| new_task = Task(title=request_body['title'], | ||
| description=request_body['description'], | ||
| completed_at=request_body['completed_at']) | ||
| except KeyError: | ||
| return make_response({ | ||
| "details": "Invalid data" | ||
| }, 400) | ||
|
|
||
| db.session.add(new_task) | ||
| db.session.commit() | ||
|
|
||
| response = { | ||
| "task": new_task.get_task_info() | ||
| } | ||
|
|
||
| return make_response(jsonify(response), 201) | ||
|
|
||
|
|
||
| @tasks_bp.route("", methods=["GET"]) | ||
| def get_tasks(): | ||
| sort_query = request.args.get("sort") | ||
| sort_by_id_query = request.args.get("sort_by_id") | ||
| filter_by_query = request.args.get("filter_by_title") | ||
|
Comment on lines
+42
to
+44
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. Doing the optionals! 😀 |
||
|
|
||
| if sort_query == "asc": | ||
| tasks = Task.query.order_by("title") | ||
| elif sort_query == "desc": | ||
| tasks = Task.query.order_by(desc("title")) | ||
| elif sort_by_id_query == "asc": | ||
| tasks = Task.query.order_by("task_id") | ||
| elif sort_by_id_query == "desc": | ||
| tasks = Task.query.order_by(desc("task_id")) | ||
| elif filter_by_query: | ||
| tasks = Task.query.filter_by(title=filter_by_query) | ||
| else: | ||
| tasks = Task.query.all() | ||
|
|
||
| return jsonify([task.get_task_info() for task in tasks]) | ||
|
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 list comprehension |
||
|
|
||
|
|
||
| @tasks_bp.route("/<task_id>", methods=["GET"]) | ||
| def get_single_task(task_id): | ||
| task = Task.query.get(task_id) | ||
| if task is None: | ||
| return make_response("", 404) | ||
|
|
||
| return { | ||
| "task": task.get_task_info() | ||
| } | ||
|
|
||
|
|
||
| @tasks_bp.route("/<task_id>", methods=["PUT"]) | ||
| def edit_task(task_id): | ||
| task = Task.query.get(task_id) | ||
| if task is None: | ||
| return make_response("", 404) | ||
|
|
||
| request_body = request.get_json() | ||
|
|
||
| task = task.from_json(request_body) | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return { | ||
| "task": task.get_task_info() | ||
| } | ||
|
|
||
|
|
||
| @tasks_bp.route("/<task_id>", methods=["DELETE"]) | ||
| def delete_task(task_id): | ||
| task = Task.query.get(task_id) | ||
| if task is None: | ||
| return make_response("", 404) | ||
|
|
||
| db.session.delete(task) | ||
| db.session.commit() | ||
|
|
||
| return { | ||
| "details": f"Task {task.task_id} \"{task.title}\" successfully deleted" | ||
| } | ||
|
|
||
|
|
||
| @tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"]) | ||
| def mark_task_incomplete(task_id): | ||
| task = Task.query.get(task_id) | ||
|
|
||
| if task is None: | ||
| return make_response("", 404) | ||
|
|
||
| task.completed_at = None | ||
|
|
||
| return { | ||
| "task": task.get_task_info() | ||
| } | ||
|
|
||
|
|
||
| @tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"]) | ||
| def mark_task_complete(task_id): | ||
| task = Task.query.get(task_id) | ||
|
|
||
| if task is None: | ||
| return make_response("", 404) | ||
|
|
||
| task.completed_at = datetime.now() | ||
|
|
||
| post_to_slack(f"Someone just completed the task {task.title}") | ||
|
|
||
| return { | ||
| "task": task.get_task_info() | ||
| } | ||
|
|
||
|
|
||
|
|
||
| ########################## | ||
| #### /goals Endpoints #### | ||
| ########################## | ||
|
|
||
| @goals_bp.route("", methods=['POST']) | ||
| def post_new_goal(): | ||
| request_body = request.get_json() | ||
|
|
||
| try: | ||
| new_goal = Goal(title=request_body['title']) | ||
| except KeyError: | ||
| return make_response({ | ||
| "details": "Invalid data" | ||
| }, 400) | ||
|
Comment on lines
+143
to
+148
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. Handling exceptions 👍 |
||
|
|
||
| db.session.add(new_goal) | ||
| db.session.commit() | ||
|
|
||
| response = { | ||
| "goal": new_goal.to_json() | ||
| } | ||
|
|
||
| return make_response(jsonify(response), 201) | ||
|
|
||
|
|
||
| @goals_bp.route("", methods=['GET']) | ||
| def get_goals(): | ||
| sort_query = request.args.get("sort") | ||
| sort_by_id_query = request.args.get("sort_by_id") | ||
| filter_by_title_query = request.args.get("filter_by_title") | ||
|
|
||
| if sort_query == "asc": | ||
| goals = Goal.query.order_by("title") | ||
| elif sort_query == "desc": | ||
| goals = Goal.query.order_by(desc("title")) | ||
| elif sort_by_id_query == "asc": | ||
| goals = Goal.query.order_by("goal_id") | ||
| elif sort_by_id_query == "desc": | ||
| goals = Goal.query.order_by(desc("goal_id")) | ||
| elif filter_by_title_query: | ||
| goals = Goal.query.filter_by(title=filter_by_title_query) | ||
| else: | ||
| goals = Goal.query.all() | ||
|
|
||
| return jsonify([goal.to_json() for goal in goals]) | ||
|
|
||
|
|
||
| @goals_bp.route("/<goal_id>", methods=['GET']) | ||
| def get_single_goal(goal_id): | ||
| goal = Goal.query.get(goal_id) | ||
| if goal is None: | ||
| return make_response("", 404) | ||
|
|
||
| return { | ||
| "goal": goal.to_json() | ||
| } | ||
|
|
||
|
|
||
| @goals_bp.route("/<goal_id>", methods=['PUT']) | ||
| def edit_goal(goal_id): | ||
| goal = Goal.query.get(goal_id) | ||
| if goal is None: | ||
| return make_response("", 404) | ||
|
|
||
| request_body = request.get_json() | ||
|
|
||
| goal.title = request_body["title"] | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return { | ||
| "goal": goal.to_json() | ||
| } | ||
|
|
||
|
|
||
| @goals_bp.route("/<goal_id>", methods=["DELETE"]) | ||
| def delete_goal(goal_id): | ||
| goal = Goal.query.get(goal_id) | ||
| if goal is None: | ||
| return make_response("", 404) | ||
|
|
||
| db.session.delete(goal) | ||
| db.session.commit() | ||
|
|
||
| return { | ||
| "details": f"Goal {goal.goal_id} \"{goal.title}\" successfully deleted" | ||
| } | ||
|
|
||
|
|
||
| @goals_bp.route("/<goal_id>/tasks", methods=["POST"]) | ||
| def post_tasks_for_goal(goal_id): | ||
| goal = Goal.query.get(goal_id) | ||
| if goal is None: | ||
| return make_response("", 404) | ||
|
|
||
| request_body = request.get_json() | ||
|
|
||
| for task_id in request_body['task_ids']: | ||
| task = Task.query.get(task_id) | ||
| task.goal_id = int(goal_id) | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return { | ||
| "id": int(goal_id), | ||
| "task_ids": request_body['task_ids'] | ||
| } | ||
|
|
||
|
|
||
| @goals_bp.route("/<goal_id>/tasks", methods=["GET"]) | ||
| def get_tasks_for_goal(goal_id): | ||
| goal = Goal.query.get(goal_id) | ||
| if goal is None: | ||
| return make_response("", 404) | ||
|
|
||
| associated_tasks = Task.query.filter_by(goal_id=int(goal_id)) | ||
|
|
||
| response = goal.to_json() | ||
| response['tasks'] = [task.get_task_info() for task in associated_tasks] | ||
|
|
||
| return response | ||
|
|
||
|
|
||
|
|
||
| ########################## | ||
| #### Helper Functions #### | ||
| ########################## | ||
|
|
||
| def post_to_slack(message): | ||
|
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. Great helper function |
||
| """ | ||
| Posts a given message to the task-notifications channel in my Task Manager Slack workspace. | ||
| """ | ||
| path = "https://slack.com/api/chat.postMessage" | ||
|
|
||
| SLACK_API_KEY = os.environ.get("SLACK_API_KEY") | ||
|
|
||
| auth_header = { | ||
| "Authorization": f"Bearer {SLACK_API_KEY}" | ||
| } | ||
|
|
||
| query_params = { | ||
| "channel": "task-notifications", | ||
| "text": message | ||
| } | ||
|
|
||
| requests.post(path, params=query_params, headers=auth_header) | ||
|
|
||
| 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.
Great helper method!