-
Notifications
You must be signed in to change notification settings - Fork 128
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
Say Zoisite #109
base: main
Are you sure you want to change the base?
Say Zoisite #109
Changes from all commits
14e96d2
dc0e33b
7dfc11d
7b68c7f
f3cf73c
f826270
c7b2ad4
08cd26d
8188612
3c27830
11e419d
f627a27
9674caa
2cd5805
58d343d
53280d1
71e9fc9
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 |
---|---|---|
|
@@ -4,31 +4,43 @@ | |
import os | ||
from dotenv import load_dotenv | ||
|
||
token = os.environ.get('SLACK_TOKEN') | ||
|
||
db = SQLAlchemy() | ||
migrate = Migrate() | ||
load_dotenv() | ||
|
||
|
||
# client = slack.WebClient(token=os.environ['SLACK_TOKEN']) | ||
def create_app(test_config=None): | ||
app = Flask(__name__) | ||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False | ||
|
||
if test_config is None: | ||
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( | ||
"SQLALCHEMY_DATABASE_URI") | ||
"RENDER_DATABASE_URI") | ||
# 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") | ||
|
||
db.init_app(app) | ||
migrate.init_app(app, db) | ||
|
||
|
||
|
||
# Import models here for Alembic setup | ||
from app.models.task import Task | ||
from app.models.goal import Goal | ||
|
||
db.init_app(app) | ||
migrate.init_app(app, db) | ||
|
||
|
||
from app.routes.task_routes import tasks_bp | ||
from app.routes.goal_routes import goals_bp | ||
# Register Blueprints here | ||
app.register_blueprint(tasks_bp) | ||
app.register_blueprint(goals_bp) | ||
|
||
# client = slack.WebClient(token=os.environ['SLACK_TOKEN']) | ||
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 see this commented line duplicated on line 13, we should remove commented code like this and rely on our repo's commit history if we need to look up past code. |
||
return app | ||
|
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 recommend placing this file in the routes directory to keep it close to the code that uses it. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
from app.models.task import Task | ||
from flask import jsonify, abort, make_response | ||
from app import token | ||
import requests | ||
|
||
def filter_by_params(cls, query_params): | ||
sort_by = query_params.get("sort") | ||
|
||
if sort_by: | ||
return get_sorted_items_by_params(cls, query_params) | ||
Comment on lines
+7
to
+10
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 would consider separating out the sort into its own function or renaming |
||
|
||
if query_params: | ||
query_params = {k.lower(): v.title() for k, v in query_params.items()} | ||
items = cls.query.filter_by(**query_params).all() | ||
else: | ||
items = cls.query.all() | ||
Comment on lines
+12
to
+16
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. There's some similar code repeated depending on if we want to sort and filter or only filter. If we want to D.R.Y. up the code, we could get a reference to the class's query object that we keep updating. One possibility might look like: model_query = cls.query
if query_params:
query_params = {k.lower(): v.title() for k, v in query_params.items()}
model_query = model_query.filter_by(**query_params).all()
if sort_by and sort_by == "asc":
model_query = model_query.order_by(cls.title.asc())
elif sort_by and sort_by == "desc":
model_query = model_query.order_by(cls.title.desc())
items = model_query.all() |
||
|
||
return items | ||
|
||
|
||
def get_sorted_items_by_params(cls, query_params): | ||
sort_param = query_params.pop('sort', None) | ||
|
||
if sort_param == 'asc': | ||
return cls.query.filter_by(**query_params).order_by(cls.title.asc()).all() | ||
elif sort_param == 'desc': | ||
return cls.query.filter_by(**query_params).order_by(cls.title.desc()).all() | ||
else: | ||
return cls.query.filter_by(**query_params).order_by(cls.id.asc()).all() | ||
|
||
|
||
def validate_model(cls, id): | ||
try: | ||
id = int(id) | ||
except: | ||
message = f"{cls.__name__} {id} is invalid" | ||
abort(make_response({"message": message}, 400)) | ||
|
||
obj = cls.query.get(id) | ||
if not obj: | ||
abort(make_response(jsonify(message=f"{cls.__name__} not found"), 404)) | ||
|
||
return obj | ||
|
||
def slack_post_message(task): | ||
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 use of a helper function for the slack message! |
||
api_url = 'http://slack.com/api/chat.postMessage' | ||
|
||
payload = { | ||
"channel": "task-notifications", | ||
"text":f"Someone just completed the task {task.title}" | ||
} | ||
headers = { | ||
"Authorization": f"Bearer {token}" | ||
} | ||
response = requests.post(api_url, headers=headers, data=payload) | ||
|
||
print(response.text) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,35 @@ | |
|
||
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 goal_to_dict(self): | ||
return { | ||
"id": self.goal_id, | ||
"title": self.title | ||
} | ||
@classmethod | ||
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. Style best practice note: We should leave a blank line between functions to make it visually clear where one ends and another begins. |
||
def create_new_goal(cls, request_data): | ||
if "title" not in request_data: | ||
raise KeyError | ||
|
||
return cls( | ||
title=request_data["title"].title() | ||
) | ||
|
||
def __str__(self): | ||
return { | ||
self.__class__.__name__.lower(): { | ||
"id": self.goal_id, | ||
"title": self.title | ||
} | ||
} | ||
Comment on lines
+23
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. To reduce confusion for users of our code, I suggest following expectations of a function when implementing python's built in methods for a class. |
||
|
||
def update(self, goal): | ||
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. If the parameter |
||
for key, value in goal.items(): | ||
if key == "title": | ||
self.title = value | ||
Comment on lines
+32
to
+34
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. The dictionary could contain many extraneous keys that we need to loop through, another option could be to check if the dictionary contains the |
||
|
||
return self.goal_to_dict() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,54 @@ | |
|
||
class Task(db.Model): | ||
task_id = db.Column(db.Integer, primary_key=True) | ||
title = db.Column(db.String(length=255)) | ||
description=db.Column(db.String) | ||
completed_at=db.Column(db.DateTime, nullable=True) | ||
is_complete= db.Column(db.Boolean, default=False) | ||
Comment on lines
+8
to
+9
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.
|
||
goal_id = db.Column(db.Integer, db.ForeignKey('goal.goal_id')) | ||
goal = db.relationship("Goal", back_populates="tasks") | ||
|
||
def task_to_dict(self): | ||
return { | ||
"id": self.task_id, | ||
"title": self.title, | ||
"description": self.description, | ||
"is_complete": False if self.completed_at is None else True | ||
if self.completed_at is not None else None | ||
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. Is line 19 intended to be there? |
||
} | ||
|
||
@classmethod | ||
def create_new_task(cls, request_data): | ||
if "title" not in request_data: | ||
raise KeyError({"details": "Invalid data"}) | ||
if "description" not in request_data: | ||
raise KeyError("description") | ||
|
||
request_data["completed_at"] = None | ||
is_complete = False if request_data["completed_at"] is None else True | ||
Comment on lines
+29
to
+30
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. In this implementation, we hardcode a value in the input dictionary to |
||
|
||
return cls( | ||
title=request_data["title"].title(), | ||
description=request_data["description"], | ||
completed_at=request_data.get("completed_at"), | ||
is_complete=is_complete | ||
) | ||
|
||
def update(self, task): | ||
for key, value in task.items(): | ||
if key == "title": | ||
self.title = value | ||
if key == "description": | ||
self.description = value | ||
|
||
return self.task_to_dict() | ||
|
||
def __str__(self): | ||
return { | ||
self.__class__.__name__.lower(): { | ||
"id": self.task_id, | ||
"title": self.title, | ||
"description": self.description, | ||
"is_complete": self.is_complete | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
from flask import Blueprint, jsonify, make_response, request | ||
from app import db | ||
from app.helper import validate_model | ||
from app.models.goal import Goal | ||
from app.models.task import Task | ||
|
||
|
||
goals_bp = Blueprint("goals", __name__, url_prefix="/goals") | ||
|
||
@goals_bp.route("", methods = ["POST"]) | ||
def create_goals(): | ||
request_body = request.get_json() | ||
try: | ||
new_goal = Goal.create_new_goal(request_body) | ||
db.session.add(new_goal) | ||
db.session.commit() | ||
|
||
message = new_goal.__str__() | ||
|
||
return make_response(jsonify(message), 201) | ||
except KeyError as e: | ||
message = "Invalid data" | ||
return make_response({"details": message}, 400) | ||
Comment on lines
+13
to
+23
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. To make it easier to debug what line caused an exception, we typically want to limit what we put in try blocks to just the code we think might cause an exception. What part of this code could result in a KeyError? I suggest keeping just that code in the try, and moving the rest to after the try/except block. |
||
|
||
@goals_bp.route("", methods = ["GET"]) | ||
def read_all_goals(): | ||
goals = Goal.query.all() | ||
goal_response = [goal.goal_to_dict() for goal in goals] | ||
return jsonify(goal_response) | ||
|
||
@goals_bp.route("/<goal_id>", methods=["GET"]) | ||
def read_one_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
message = goal.__str__() | ||
|
||
return make_response(jsonify(message), 200) | ||
|
||
@goals_bp.route("/<goal_id>", methods=["PUT"]) | ||
def update_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
request_body = request.get_json() | ||
goal.update(request_body) | ||
|
||
db.session.commit() | ||
message = goal.__str__() | ||
return make_response(jsonify(message)) | ||
|
||
@goals_bp.route("/<goal_id>", methods=["DELETE"]) | ||
def delete_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
db.session.delete(goal) | ||
db.session.commit() | ||
|
||
message = { | ||
"details": f'Goal {goal_id} "{goal.title}" successfully deleted' | ||
} | ||
return make_response(jsonify(message), 200) | ||
|
||
@goals_bp.route("/<goal_id>/tasks", methods=["POST"]) | ||
def assign_tasks_to_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
request_body = request.get_json() | ||
if not request_body: | ||
return jsonify({'error': 'Invalid request body'}), 400 | ||
|
||
task_ids = request_body.get('task_ids', []) | ||
for task_id in task_ids: | ||
task = validate_model(Task, task_id) | ||
task.goal_id = goal.goal_id | ||
db.session.commit() | ||
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 could move this line outside of the for-loop to save the changes to all the tasks at once. |
||
|
||
return jsonify({'id': goal.goal_id, 'task_ids': task_ids}), 200 | ||
|
||
@goals_bp.route("/<goal_id>/tasks", methods=["GET"]) | ||
def get_task_of_goal(goal_id): | ||
goal = validate_model(Goal, goal_id) | ||
goal = Goal.query.get(goal_id) | ||
task_data = [] | ||
for task in goal.tasks: | ||
task_data.append({ | ||
"id": task.task_id, | ||
"goal_id": goal.goal_id, | ||
"title": task.title, | ||
"description": task.description, | ||
"is_complete": task.is_complete | ||
}) | ||
Comment on lines
+80
to
+86
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. Could we use the task's |
||
task_data | ||
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 should remove this line since we aren't performing an operation. |
||
response = { | ||
"id": goal.goal_id, | ||
"title": goal.title, | ||
"tasks": task_data | ||
} | ||
return jsonify(response), 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. Great looking code across the route files as far as line length, clear variable names, spacing, and overall readability 😊 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from flask import Blueprint, jsonify, abort, make_response, request | ||
from app import db | ||
from app.models.task import Task | ||
from app.helper import filter_by_params, validate_model, slack_post_message | ||
from datetime import datetime | ||
|
||
|
||
tasks_bp = Blueprint("tasks", __name__, url_prefix="/tasks") | ||
@tasks_bp.route("", methods = ["POST"]) | ||
def create_tasks(): | ||
request_body = request.get_json() | ||
try: | ||
new_task = Task.create_new_task(request_body) | ||
db.session.add(new_task) | ||
db.session.commit() | ||
|
||
message = new_task.__str__() | ||
|
||
return make_response(jsonify(message), 201) | ||
except KeyError as e: | ||
message = "Invalid data" | ||
return make_response({"details": message}, 400) | ||
|
||
@tasks_bp.route("", methods = ["GET"]) | ||
def read_all_tasks(): | ||
query_params = request.args.to_dict() | ||
tasks = filter_by_params(Task, query_params) | ||
tasks_response = [task.task_to_dict() for task in tasks] | ||
return jsonify(tasks_response) | ||
|
||
@tasks_bp.route("/<task_id>", methods=["GET"]) | ||
def read_one_task(task_id): | ||
task = validate_model(Task, task_id) | ||
task = Task.query.get(task_id) | ||
task = task.__str__() | ||
return jsonify(task), 200 | ||
|
||
@tasks_bp.route("/<task_id>", methods=["PUT"]) | ||
def update_task(task_id): | ||
task = validate_model(Task, task_id) | ||
request_body = request.get_json() | ||
task.update(request_body) | ||
|
||
db.session.commit() | ||
message = task.__str__() | ||
return make_response(jsonify(message)) | ||
|
||
@tasks_bp.route("/<task_id>", methods=["DELETE"]) | ||
def delete_task(task_id): | ||
task = validate_model(Task, task_id) | ||
db.session.delete(task) | ||
db.session.commit() | ||
|
||
message = { | ||
"details": f'Task {task_id} "{task.title}" successfully deleted' | ||
} | ||
return make_response(jsonify(message), 200) | ||
|
||
@tasks_bp.route("/<task_id>/mark_complete", methods=["PATCH"]) | ||
def mark_task_complete(task_id): | ||
task = validate_model(Task, task_id) | ||
task.is_complete = True | ||
task.completed_at = datetime.now().isoformat() | ||
db.session.commit() | ||
|
||
slack_post_message(task) | ||
message = task.__str__() | ||
return make_response(jsonify(message), 200) | ||
|
||
@tasks_bp.route("/<task_id>/mark_incomplete", methods=["PATCH"]) | ||
def mark_task_incomplete(task_id): | ||
task = validate_model(Task, task_id) | ||
task.is_complete = False | ||
task.completed_at = None | ||
db.session.commit() | ||
message = task.__str__() | ||
return make_response(jsonify(message), 200) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# import os | ||
# from pathlib import Path | ||
# from dotenv import load_dotenv | ||
|
||
# env_path = Path('.') / '.env' | ||
# load_dotenv(dotenv_path=env_path) | ||
|
||
# token = os.environ.get('SLACK_TOKEN') | ||
Comment on lines
+1
to
+8
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. Was this file intended to be committed? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Generic single-database configuration. |
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.
Nice choice to split up the routes into files that are more specific to the resources they work with.