-
Notifications
You must be signed in to change notification settings - Fork 97
Pine - Kayla Huddleston #76
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?
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,6 +1,15 @@ | ||
| from flask import current_app | ||
| from app import db | ||
| from sqlalchemy.orm import backref | ||
|
|
||
|
|
||
| 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_dict(self): | ||
| return { | ||
| "id": self.goal_id, | ||
| "title": self.title | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,34 @@ | ||
| from flask import current_app | ||
| from app import db | ||
| from app.models.goal import Goal | ||
|
|
||
|
|
||
| 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) | ||
|
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 foreign key relationship! |
||
|
|
||
| def to_dict(self): | ||
| if self.goal_id == None: | ||
| if not self.completed_at: | ||
| self.completed_at = None | ||
| else: | ||
| self.completed_at = True | ||
| 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 truthiness and the ternary operator! |
||
| } | ||
| else: | ||
| task_dict = { | ||
| "id": self.task_id, | ||
| "goal_id": self.goal_id, | ||
| "title": self.title, | ||
| "description": self.description, | ||
| "is_complete": self.completed_at is not None | ||
| } | ||
| return task_dict | ||
|
Comment on lines
+14
to
+33
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! For an extra challenge, see if you can think of how to re-write this with less duplication of code. It might be helpful to start off with a dictionary with all the shared values, and then only add the additional values you need in the conditionals. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,184 @@ | ||
| from flask import Blueprint | ||
| from flask import Blueprint, jsonify, make_response, request | ||
| from app.models.task import Task | ||
| from app.models.goal import Goal | ||
| from app import db | ||
| from datetime import date | ||
| import requests | ||
| import os | ||
| from dotenv import load_dotenv | ||
|
|
||
| load_dotenv() | ||
|
|
||
| task_bp = Blueprint("task_bp", __name__, url_prefix="/tasks") | ||
| goal_bp = Blueprint("goal_bp", __name__, url_prefix="/goals") | ||
|
|
||
| @task_bp.route("", methods=["GET", "POST"]) | ||
| def handle_all_tasks(): | ||
| tasks_response = [] | ||
|
|
||
| #WV2: Get Tasks Asc, Get Tasks Desc | ||
| if request.method == "GET": | ||
| 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() | ||
|
|
||
| #WV1: No Saved Tasks, One Saved Tasks | ||
| else: | ||
| tasks = Task.query.all() | ||
| for task in tasks: | ||
| tasks_response.append(task.to_dict()) | ||
|
Comment on lines
+21
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. Awesome condensed code here! |
||
| return jsonify(tasks_response), 200 | ||
|
|
||
| #WV1: Create Task With None Completed_At, Create Task - Title, Create Task - Description, Create Task - Completed_At | ||
| #WV3: Create Task Wtih Valid Completed_At | ||
| if request.method == "POST": | ||
| 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 ({'details': 'Invalid data'}, 400) | ||
|
Comment on lines
+37
to
+42
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. Cool use of try and except! I especially like that you specify that |
||
| db.session.add(new_task) | ||
| db.session.commit() | ||
| return jsonify({"task":new_task.to_dict()}), 201 | ||
|
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 compact return statement. |
||
|
|
||
| @task_bp.route("/<task_id>", methods=["GET", "PUT", "DELETE"]) | ||
| def handle_single_task(task_id): | ||
| task_id = int(task_id) | ||
| task = Task.query.get(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. Consider using |
||
|
|
||
| #WV1: Get Task, Get Task Not Found | ||
| if request.method == "GET": | ||
| if task is None: | ||
| return make_response("", 404) | ||
| return jsonify({"task": task.to_dict()}), 200 | ||
|
|
||
| #WV1: Update Task, Update Task Not Found | ||
| #WV3: Update Task With Valid Completed At | ||
| elif request.method == "PUT": | ||
| input_data = request.get_json() | ||
| if task is None: | ||
| return make_response("", 404) | ||
| task.title = input_data["title"] | ||
| task.description = input_data["description"] | ||
| db.session.commit() | ||
| return ({"task":task.to_dict()}), 200 | ||
|
|
||
| #WV1: Delete Task, Delete Task Not Found | ||
| elif request.method == "DELETE": | ||
| if task is None: | ||
| return make_response("", 404) | ||
| else: | ||
| db.session.delete(task) | ||
| db.session.commit() | ||
| return ({'details': f'Task {task_id} "{task.title}" successfully deleted'}), 200 | ||
|
|
||
| #WV3: Mark Comp on Incom, Mark Incom on Comp, Mark Comp on Comp, Marck Comp on Missing, Mark Incom on Missing | ||
| @task_bp.route("/<task_id>/<completion_status>", methods=["PATCH"]) | ||
|
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 cool idea making the completion status a dynamic part of the route! I hadn't considered doing it that way! |
||
| def mark_complete(task_id, completion_status): | ||
|
|
||
| task = Task.query.get(task_id) | ||
| PATH="https://slack.com/api/chat.postMessage" | ||
| if task is None: | ||
| return make_response("", 404) | ||
|
|
||
| if completion_status == "mark_complete": | ||
| task.completed_at = date.today() | ||
|
|
||
| #WV4: Send Slack Message | ||
| token=os.environ.get("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. Nice job getting the token from the environment variables! You might consider fetching the variable from the OS once, and then keeping it saved so that your code does not need to do |
||
| query_params = { | ||
| "token": token, | ||
| "channel": "task-notifications", | ||
| "text": f"Somone just completed task {task.title}" | ||
| } | ||
| requests.post(PATH, data=query_params) | ||
|
|
||
| if completion_status == "mark_incomplete": | ||
| task.completed_at = None | ||
| 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. Consider adding logic here to send back an error code if the user attempts to pass in an invalid |
||
| return jsonify({"task": task.to_dict()}) | ||
|
|
||
| @goal_bp.route("", methods=["GET", "POST"]) | ||
| def handle_all_goals(): | ||
| goal_response = [] | ||
|
|
||
| #WV5: No Saved Goals, One Saved Goals | ||
| if request.method == "GET": | ||
| goals = Goal.query.all() | ||
| for goal in goals: | ||
| goal_response.append(goal.to_dict()) | ||
|
Comment on lines
+112
to
+113
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! As an additional challenge, can you think of how you would do this in a list comprehension? Either way is fine, it's just a matter of personal style. |
||
| return jsonify(goal_response), 200 | ||
|
|
||
| #WV5: Create Goal, Create Goal Missing Title | ||
| if request.method == "POST": | ||
| request_body = request.get_json() | ||
| try: | ||
| new_goal = Goal(title = request_body["title"]) | ||
| except KeyError: | ||
| return ({'details': 'Invalid data'}, 400) | ||
| db.session.add(new_goal) | ||
| db.session.commit() | ||
|
|
||
| return jsonify({"goal": new_goal.to_dict()}), 201 | ||
|
|
||
| @goal_bp.route("/<goal_id>", methods=["GET", "DELETE", "PUT", "PATCH"]) | ||
| def handle_single_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. Consider adding validation here so an error response is made if the user passes in a non-integer |
||
| goal = Goal.query.get(goal_id) | ||
|
|
||
| #WV5: Get Goal, Get Goal Not Found | ||
| if request.method == "GET": | ||
| if goal is None: | ||
| return make_response("", 404) | ||
| return jsonify({"goal": goal.to_dict()}), 200 | ||
|
|
||
| #WV5: Update Goal, Update Goal Not Found | ||
| elif request.method == "PUT": | ||
| input_data = request.get_json() | ||
| if goal is None: | ||
| return make_response("", 404) | ||
| goal.title = input_data["title"] | ||
| db.session.commit() | ||
| return ({"goal": goal.to_dict()}), 200 | ||
|
|
||
| #WV5: Delete Goal Not Found, Delete Goal | ||
| elif request.method == "DELETE": | ||
| if goal is None: | ||
| return make_response("", 404) | ||
| else: | ||
| db.session.delete(goal) | ||
| db.session.commit() | ||
| return ({'details': f'Goal {goal_id} "{goal.title}" successfully deleted'}), 200 | ||
|
|
||
| @goal_bp.route("/<goal_id>/tasks", methods=["GET", "POST"]) | ||
| def handle_goal_and_tasks(goal_id): | ||
| goal = Goal.query.get(goal_id) | ||
| request_body = request.get_json() | ||
|
|
||
| #W5: Get Tasks, Get Tasks No Goal, Get Tasks No Tasks, Task Include Goal ID | ||
| if request.method == "GET": | ||
| if goal is None: | ||
| return make_response("", 404) | ||
| task_list = [] | ||
| for task in goal.tasks: | ||
| task_list.append(task.to_dict()) | ||
| return { | ||
| "id": goal.goal_id, | ||
| "title": goal.title, | ||
| "tasks": task_list | ||
| }, 200 | ||
|
Comment on lines
+167
to
+173
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. 👍🏻 |
||
|
|
||
| #W5: Post Tasks IDs to Goal, Post Tasks to Goal with Goals | ||
| if request.method == "POST": | ||
| if goal is None: | ||
| return make_response("", 404) | ||
| task_ids = request_body["task_ids"] | ||
| for id in task_ids: | ||
| task = Task.query.get(id) | ||
| task.goal_id = goal_id | ||
| db.session.commit() | ||
| return make_response({"id": int(goal_id), "task_ids": task_ids}), 200 | ||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| from __future__ import with_statement | ||
|
|
||
| import logging | ||
| from logging.config import fileConfig | ||
|
|
||
| from sqlalchemy import engine_from_config | ||
| from sqlalchemy import pool | ||
| from flask import current_app | ||
|
|
||
| from alembic import context | ||
|
|
||
| # this is the Alembic Config object, which provides | ||
| # access to the values within the .ini file in use. | ||
| config = context.config | ||
|
|
||
| # Interpret the config file for Python logging. | ||
| # This line sets up loggers basically. | ||
| fileConfig(config.config_file_name) | ||
| logger = logging.getLogger('alembic.env') | ||
|
|
||
| # add your model's MetaData object here | ||
| # for 'autogenerate' support | ||
| # from myapp import mymodel | ||
| # target_metadata = mymodel.Base.metadata | ||
| config.set_main_option( | ||
| 'sqlalchemy.url', | ||
| str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) | ||
| target_metadata = current_app.extensions['migrate'].db.metadata | ||
|
|
||
| # other values from the config, defined by the needs of env.py, | ||
| # can be acquired: | ||
| # my_important_option = config.get_main_option("my_important_option") | ||
| # ... etc. | ||
|
|
||
|
|
||
| def run_migrations_offline(): | ||
| """Run migrations in 'offline' mode. | ||
|
|
||
| This configures the context with just a URL | ||
| and not an Engine, though an Engine is acceptable | ||
| here as well. By skipping the Engine creation | ||
| we don't even need a DBAPI to be available. | ||
|
|
||
| Calls to context.execute() here emit the given string to the | ||
| script output. | ||
|
|
||
| """ | ||
| url = config.get_main_option("sqlalchemy.url") | ||
| context.configure( | ||
| url=url, target_metadata=target_metadata, literal_binds=True | ||
| ) | ||
|
|
||
| with context.begin_transaction(): | ||
| context.run_migrations() | ||
|
|
||
|
|
||
| def run_migrations_online(): | ||
| """Run migrations in 'online' mode. | ||
|
|
||
| In this scenario we need to create an Engine | ||
| and associate a connection with the context. | ||
|
|
||
| """ | ||
|
|
||
| # this callback is used to prevent an auto-migration from being generated | ||
| # when there are no changes to the schema | ||
| # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html | ||
| def process_revision_directives(context, revision, directives): | ||
| if getattr(config.cmd_opts, 'autogenerate', False): | ||
| script = directives[0] | ||
| if script.upgrade_ops.is_empty(): | ||
| directives[:] = [] | ||
| logger.info('No changes in schema detected.') | ||
|
|
||
| connectable = engine_from_config( | ||
| config.get_section(config.config_ini_section), | ||
| prefix='sqlalchemy.', | ||
| poolclass=pool.NullPool, | ||
| ) | ||
|
|
||
| with connectable.connect() as connection: | ||
| context.configure( | ||
| connection=connection, | ||
| target_metadata=target_metadata, | ||
| process_revision_directives=process_revision_directives, | ||
| **current_app.extensions['migrate'].configure_args | ||
| ) | ||
|
|
||
| with context.begin_transaction(): | ||
| context.run_migrations() | ||
|
|
||
|
|
||
| if context.is_offline_mode(): | ||
| run_migrations_offline() | ||
| else: | ||
| run_migrations_online() |
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.
I think we don't need this import statement. The
backrefon line 9 is just a named parameter for the relationship function instead of an object we need to import.