-
Notifications
You must be signed in to change notification settings - Fork 124
zoisite c19 izzy_task_list #117
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: main
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -15,8 +15,13 @@ def create_app(test_config=None): | |
| app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False | ||
|
|
||
| if test_config is None: | ||
| app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( | ||
| "SQLALCHEMY_DATABASE_URI") | ||
| # app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( | ||
| #"SQLALCHEMY_DATABASE_URI") | ||
|
|
||
| app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get("RENDER_DATABASE_URI") | ||
|
|
||
|
|
||
|
|
||
| else: | ||
| app.config["TESTING"] = True | ||
| app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( | ||
|
|
@@ -28,7 +33,12 @@ def create_app(test_config=None): | |
|
|
||
| db.init_app(app) | ||
| migrate.init_app(app, db) | ||
| from app.models.task import Task | ||
| from app.models.goal import Goal | ||
|
|
||
| # Register Blueprints here | ||
| from .routes import task_list_bp,goals_bp | ||
| app.register_blueprint(task_list_bp) | ||
| app.register_blueprint(goals_bp) | ||
|
|
||
| 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. Remember to make an empty init.py file in any package folder/subfolder. app has one, but we should have one here in the models folder as well. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,16 @@ | |
|
|
||
| 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") | ||
|
|
||
| def to_dict(self): | ||
| return { | ||
| "id" : self.goal_id, | ||
| "title" : self.title | ||
| } | ||
|
|
||
| @classmethod | ||
| def from_dict(cls,request_data): | ||
| return cls( | ||
| title=request_data["title"]) | ||
|
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 goal model looks great! |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,31 @@ | ||
| from app import db | ||
|
|
||
| from flask import make_response | ||
|
|
||
| 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, nullable=True) | ||
|
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 default for a nullable constraint is True, so you can absolutely leave that out here if you would like! |
||
| goal_id = db.Column(db.Integer,db.ForeignKey("goal.goal_id")) | ||
| goal = db.relationship("Goal", back_populates="tasks") | ||
|
|
||
| #returning a dictionary from the database | ||
| def task_dict(self): | ||
| task_dict = { | ||
| "id": self.task_id, | ||
| "title": self.title, | ||
| "description": self.description, | ||
| "is_complete": self.completed_at != 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. Great job using the inclusion of the .completed_at to define the truthiness of is_complete! Just a slight nitpick, make sure to move the brace at the end to the next line! |
||
|
|
||
| if self.goal_id: | ||
| task_dict["goal_id"] =self.goal_id | ||
| return task_dict | ||
|
|
||
|
|
||
| #take data from user to make new task | ||
| @classmethod | ||
| def from_dict(cls,request_data): | ||
| return cls( | ||
| title=request_data["title"], | ||
| description=request_data["description"]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,254 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint,jsonify, abort,make_response,request | ||
| from app import db | ||
| import requests | ||
| from app.models.task import Task | ||
| from app.models.goal import Goal | ||
| from datetime import datetime | ||
| import os | ||
|
|
||
| task_list_bp = Blueprint("tasks", __name__, url_prefix ="/tasks") | ||
| goals_bp = Blueprint("goals", __name__, url_prefix = "/goals") | ||
|
|
||
| @task_list_bp.route("", methods = ["POST"]) | ||
| def create_tasks(): | ||
| request_body = request.get_json() | ||
| print(request_body) | ||
| print("************") | ||
|
|
||
| if (not "title" in request_body) or (not "description" in request_body): | ||
| return{ | ||
| "details":"Invalid data" | ||
| }, 400 | ||
| try: | ||
| new_task = Task.from_dict(request_body) | ||
| db.session.add(new_task) | ||
| db.session.commit() | ||
|
|
||
| #message = f"Task {new_task.title} successfully created" | ||
| return make_response(jsonify({"task":new_task.task_dict()}), 201) | ||
|
|
||
| except KeyError as e: | ||
| abort(make_response("Invalid request. Missing required value: {e}"), 400) | ||
|
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 error handling when processing a user-created request body! We should assume that a user's request body may contain errors! |
||
|
|
||
| @task_list_bp.route("/<id>", methods = ["GET"]) | ||
| def get_one_saved_task(id): | ||
| task = validate_task(id) | ||
| return jsonify({"task":task.task_dict()}), 200 | ||
|
|
||
| @task_list_bp.route("", methods = ["GET"]) | ||
| def get_all_saved_tasks(): | ||
| sort_query=request.args.get("sort") | ||
| tasks_query=Task.query | ||
|
|
||
| if sort_query =="asc": | ||
| tasks_query = Task.query.order_by(Task.title.asc()) | ||
| elif sort_query =="desc": | ||
| tasks_query = Task.query.order_by(Task.title.desc()) | ||
|
|
||
| tasks = tasks_query.all() | ||
|
|
||
| tasks_response =[task.task_dict() 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. Great use of list comprehension here! |
||
|
|
||
| return (jsonify(tasks_response)),200 | ||
|
|
||
|
|
||
|
|
||
| def validate_task(task_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. Not too big of an issue since you are only ever using validate_task to validate your tasks, but it's never a bad idea to make it a generic function in case you have other models that need to be validated later! Also, since it is a helper function, best practice would either have it placed before your routes or in its own helper file! |
||
| try: | ||
| task_id = int(task_id) | ||
| except: | ||
| abort(make_response({"message":f"task {task_id} invalid"}, 400)) | ||
|
|
||
|
|
||
| task = Task.query.get(task_id) | ||
|
|
||
| if not task: | ||
| abort(make_response({"message": f"task {task_id} not found"}, 404)) | ||
|
|
||
| return task | ||
|
|
||
|
|
||
| @task_list_bp.route("/<id>", methods = ["PUT"]) | ||
| def update_task(id): | ||
| task = validate_task(id) | ||
| request_body = request.get_json() | ||
| task.title = request_body["title"] | ||
| task.description = request_body["description"] | ||
|
|
||
| db.session.commit() | ||
| response_body = {"task":task.task_dict()} | ||
|
|
||
|
|
||
| return make_response(jsonify(response_body), 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. We need to have error handling for a request that uses the request body to create a response (like you have in the POST route). If request_body doesn't have a key "title" then line 75 would throw an unhandled exception. |
||
|
|
||
| @task_list_bp.route("/<id>", methods=["DELETE"]) | ||
| def delete_one_task(id): | ||
| task= validate_task(id) | ||
|
|
||
| db.session.delete(task) | ||
| db.session.commit() | ||
|
|
||
| message = {"details":f'Task {task.task_id} "{task.title}" successfully deleted'} | ||
| return make_response(jsonify(message), 200) | ||
|
|
||
|
|
||
| @task_list_bp.route("/<id>/mark_complete", methods=["PATCH"]) | ||
| def mark_task_complete(id): | ||
| task=validate_task(id) | ||
| task.completed_at=datetime.now() | ||
| db.session.commit() | ||
|
|
||
| return jsonify({"task":task.task_dict()}), 200 | ||
|
|
||
|
|
||
|
|
||
| @task_list_bp.route("/<id>/mark_incomplete", methods=["PATCH"]) | ||
| def mark_task_incomplete(id): | ||
| task=validate_task(id) | ||
| task.completed_at=None | ||
| db.session.commit() | ||
|
|
||
| return jsonify({"task":task.task_dict()}), 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. This looks great! 😊 |
||
|
|
||
| #wave4 | ||
| def slack_notification(): | ||
| url = "https://slack.com/api/chat.postMessage" | ||
|
|
||
| payload = { | ||
| "channel": "api-test-channel", | ||
| "text": "Task completed" | ||
| } | ||
| headers = { | ||
| 'Authorization':os.environ.get("SLACK_API_TOKEN")} | ||
|
|
||
| response = requests.post(url, headers=headers, data=payload) | ||
|
|
||
| return response.text | ||
|
|
||
| #wave 5 | ||
| @goals_bp.route("",methods=["POST","GET"]) | ||
| def handle_goal(): | ||
| if request.method == "POST": | ||
| request_body = request.get_json() | ||
| if "title" not in request_body: | ||
| return{ | ||
| "details": "Invalid data" | ||
| },400 | ||
|
|
||
| new_goal = Goal ( | ||
| title=request_body["title"] | ||
| ) | ||
|
|
||
| db.session.add(new_goal) | ||
| db.session.commit() | ||
|
|
||
| return { | ||
| "goal": { | ||
| "id":new_goal.goal_id, | ||
| "title":new_goal.title | ||
| } | ||
| }, 201 | ||
|
|
||
| elif request.method == "GET": | ||
| sorting_goals= request.args.get('sort') | ||
| list = None | ||
| if sorting_goals== "desc": | ||
| list = Goal.query.order_by(Goal.title.desc()) # descending method | ||
| elif sorting_goals == "asc": | ||
| list = Goal.query.order_by(Goal.title.asc()) # ascending method | ||
| else: | ||
| list = Goal.query.all() | ||
| goals_response = [] | ||
| for goal in list: | ||
| goals_response.append({ | ||
| "id": goal.goal_id, | ||
| "title": goal.title, | ||
| }) | ||
|
|
||
| return jsonify(goals_response), 200 | ||
|
|
||
| @goals_bp.route("/<goal_id>", methods=["GET","PUT","DELETE"]) | ||
| def handle_goal_get(goal_id): | ||
| goal = Goal.query.get(goal_id) | ||
| if goal == None: | ||
| return ("", 404) | ||
|
|
||
| if request.method == "GET": | ||
| return { | ||
| "goal": { | ||
| "id":goal.goal_id, | ||
| "title":goal.title, | ||
| } | ||
| } | ||
| if request.method == "PUT": | ||
| form_data = request.get_json() | ||
|
|
||
| goal.title = form_data["title"] | ||
|
|
||
| db.session.commit() | ||
|
|
||
| return jsonify({ | ||
| "goal":{ | ||
| "id":goal.goal_id, | ||
| "title":goal.title, | ||
| } | ||
| }),200 | ||
|
|
||
| elif request.method == "DELETE": | ||
| db.session.delete(goal) | ||
| db.session.commit() | ||
|
|
||
| if not goal in Goal.query: | ||
| abort(make_response({"message": f"Goal {goal_id} not found"}, 404)) | ||
| return jsonify({ | ||
| "details": f'Goal {goal.goal_id} "{goal.title}" successfully deleted' | ||
| }),200 | ||
|
|
||
|
|
||
| @goals_bp.route("/<goal_id>/tasks", methods=["POST","GET"]) | ||
| def post_tasked_goal(goal_id): | ||
|
|
||
| goal = Goal.query.get(goal_id) | ||
|
|
||
| if goal == None: | ||
| return (""), 404 | ||
|
|
||
| if request.method == "POST": | ||
| request_body = request.get_json() | ||
|
|
||
| tasks_instances= [] | ||
| for task_id in request_body["task_ids"]: | ||
| tasks_instances.append(Task.query.get(task_id)) | ||
|
|
||
| goal.tasks = tasks_instances | ||
|
|
||
| db.session.commit() | ||
|
|
||
| task_ids = [] | ||
| for task in goal.tasks: | ||
| task_ids.append(task.task_id) | ||
|
|
||
| response_body = { | ||
| "id": goal.goal_id, | ||
| "task_ids": task_ids | ||
| } | ||
|
|
||
| return jsonify(response_body), 200 | ||
|
|
||
| if request.method == "GET": | ||
| tasks_response =[] | ||
| for task in goal.tasks: | ||
| tasks_response.append({ | ||
| "id": task.task_id, | ||
| "goal_id": task.goal_id, | ||
| "title": task.title, | ||
| "description": task.description, | ||
| "is_complete": bool(task.completed_at) | ||
| }) | ||
|
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 looks nearly identical to what you have in the instance method to_dict in the Task class. We should use the to_dict method on each task from goal.tasks instead of repeating code. You already have the logic to handle adding goal_id to each task dict too in to_dict! |
||
| response_body = { | ||
| "id": goal.goal_id, | ||
| "title": goal.title, | ||
| "tasks" : tasks_response | ||
| } | ||
| return jsonify(response_body), 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. While it is possible to attach multiple methods to a single route like you've done here, it starts to get a bit cluttered. It will absolutely depend on how your team wants to handle things, but overall, it's a good idea to separate each method out for readability! |
||
|
|
||
| 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.
Don't be afraid to separate your routes into task_routes and goal_routes!