diff --git a/README.md b/README.md index e326c7b6d..85e1c0f69 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Skills Assessed -- Following directions and reading comprehension +- Gathering technical requirements from written documentation - Reading, writing, and using tests - Demonstrating understanding of the client-server model, request-response cycle and conventional RESTful routes - Driving development with independent research, experimentation, and collaboration diff --git a/ada-project-docs/assets/render-show-language-drop-down.png b/ada-project-docs/assets/render-show-language-drop-down.png new file mode 100644 index 000000000..0a41efb62 Binary files /dev/null and b/ada-project-docs/assets/render-show-language-drop-down.png differ diff --git a/ada-project-docs/assets/render-show-language-field.png b/ada-project-docs/assets/render-show-language-field.png new file mode 100644 index 000000000..acb20ac27 Binary files /dev/null and b/ada-project-docs/assets/render-show-language-field.png differ diff --git a/ada-project-docs/optional-enhancements.md b/ada-project-docs/optional-enhancements.md index b306a1150..526ea5e42 100644 --- a/ada-project-docs/optional-enhancements.md +++ b/ada-project-docs/optional-enhancements.md @@ -28,14 +28,6 @@ How would you write tests for it? How would you implement it? Your decisions should not break the other tests. -### Re-organize Routes - -Consider refactoring how endpoints are written in the `routes.py` file. - -Here are some ideas to start: - -- Instead of having `if/elif` blocks to handle many HTTP methods in one route method, separate them into different route methods - ### Model Instance Methods We can define instance methods in our model classes. @@ -44,7 +36,7 @@ Consider places in your code that deal with one model at a time. Is there any re Here are some ideas to start: -- Create an instance method in `Task` named `to_json()` +- Create an instance method in `Task` named `to_dict()` - Converts a `Task` instance into JSON - Returns a Python dictionary in the shape of the JSON our API returns in the `GET` `/tasks` route - Create a class method in `Task` named `from_json()` @@ -54,13 +46,13 @@ Here are some ideas to start: ### Use List Comprehensions -Use list comprehensions in your `routes.py` logic. +Use list comprehensions in your route functions where applicable. ### Route Helper Methods -If you have not already refactored your `routes.py` to use helper methods, do so now! +If you have not already refactored your route files to use helper methods, do so now! -Consider code with complex or repetitive logic, and refactor it into helper methods. Watch your `routes.py` file become cleaner and more readable! +Consider code with complex or repetitive logic, and refactor it into helper methods. Watch your route files become cleaner and more readable! ### More Query Params diff --git a/ada-project-docs/setup.md b/ada-project-docs/setup.md index 125900ca9..b3ee1840d 100644 --- a/ada-project-docs/setup.md +++ b/ada-project-docs/setup.md @@ -9,7 +9,7 @@ The goal for setup is to cover all of the set up needed at the beginning of this 1. Setting up development and test databases 1. Setting up a `.env` file 1. Running `$ flask db init` -1. Running `$ flask run` and `$ FLASK_ENV=development flask run` +1. Running `$ flask run` and `$ flask run --debug` # Requirements @@ -63,14 +63,14 @@ Run `$ flask db init`. **_After you make your first model in Wave 1_**, run the other commands `migrate` and `upgrade`. -## Run `$ flask run` or `$ FLASK_ENV=development flask run` +## Run `$ flask run` or `$ flask run --debug` Check that your Flask server can run with `$ flask run`. We can run the Flask server specifying that we're working in the development environment. This enables hot-reloading, which is a feature that refreshes the Flask server every time there is a detected change. ```bash -$ FLASK_ENV=development flask run +$ flask run --debug ``` **It is highly recommended to run the Flask servers with this command**. diff --git a/ada-project-docs/wave_01.md b/ada-project-docs/wave_01.md index 159e8917d..30be86ad8 100644 --- a/ada-project-docs/wave_01.md +++ b/ada-project-docs/wave_01.md @@ -10,7 +10,7 @@ Tasks are entities that describe a task a user wants to complete. They contain a - description to hold details about the task - an optional datetime that the task is completed on -Our goal for this wave is to be able to create, read, update, and delete different tasks. We will create RESTful routes for this different operations. +Our goal for this wave is to be able to create, read, update, and delete different tasks. We will create RESTful routes for these different operations. # Requirements @@ -18,17 +18,17 @@ Our goal for this wave is to be able to create, read, update, and delete differe There should be a `Task` model that lives in `app/models/task.py`. -Tasks should contain these attributes. Feel free to change the name of the `task_id` column if you would like. **The tests require the remaining columns to be named exactly** as `title`, `description`, and `completed_at`. +Tasks should contain these attributes. **The tests require the following columns to be named exactly** as `title`, `description`, and `completed_at`. -- `task_id`: a primary key for each task +- `id`: a primary key for each task - `title`: text to name the task - `description`: text to describe the task -- `completed_at`: a datetime that has the date that a task is completed on. **Can be _nullable_,** and contain a null value. A task with a `null` value for `completed_at` has not been completed. When we create a new task, `completed_at` should be `null` AKA `None` in Python. +- `completed_at`: a datetime that represents the date that a task is completed on. **Can be _nullable_,** and contain a null value. A task with a `null` value for `completed_at` has not been completed. When we create a new task, `completed_at` should be `null` AKA `None` in Python. ### Tips -- SQLAlchemy's column type for text is `db.String`. The column type for datetime is `db.DateTime`. -- SQLAlchemy supports _nullable_ columns with specific syntax. +- To work with date information, we can import the `datetime` data type with the import line `from datetime import datetime`. +- SQLAlchemy supports optional, or _nullable_, columns with specific syntax. - Don't forget to run: - `flask db init` once during setup - `flask db migrate` every time there's a change in models, in order to generate migrations @@ -38,8 +38,6 @@ Tasks should contain these attributes. Feel free to change the name of the `task ## CRUD for Tasks -The following are required routes for wave 1. Feel free to implement the routes in any order within this wave. - ### Tips - Pay attention to the exact shape of the expected JSON. Double-check nested data structures and the names of the keys for any mispellings. @@ -52,9 +50,15 @@ The following are required routes for wave 1. Feel free to implement the routes ### CLI -In addition to testing your code with pytest and postman, you can play test your code with the CLI (Command Line Interface) by running `python3 cli/main.py`. The flask server needs to be running to run the CLI. +In addition to testing your code with pytest and postman, you can play test your code with the CLI (Command Line Interface) by running `python3 cli/main.py`. + +The flask server needs to be running first before running the CLI. + +### CRUD Routes -### Create a Task: Valid Task With `null` `completed_at` +The following are required routes for wave 1. Feel free to implement the routes in any order within this wave. + +#### Create a Task: Valid Task With `null` `completed_at` As a client, I want to be able to make a `POST` request to `/tasks` with the following HTTP request body @@ -83,7 +87,7 @@ and get this response: so that I know I successfully created a Task that is saved in the database. -### Get Tasks: Getting Saved Tasks +#### Get Tasks: Getting Saved Tasks As a client, I want to be able to make a `GET` request to `/tasks` when there is at least one saved task and get this response: @@ -106,7 +110,7 @@ As a client, I want to be able to make a `GET` request to `/tasks` when there is ] ``` -### Get Tasks: No Saved Tasks +#### Get Tasks: No Saved Tasks As a client, I want to be able to make a `GET` request to `/tasks` when there are zero saved tasks and get this response: @@ -116,7 +120,7 @@ As a client, I want to be able to make a `GET` request to `/tasks` when there ar [] ``` -### Get One Task: One Saved Task +#### Get One Task: One Saved Task As a client, I want to be able to make a `GET` request to `/tasks/1` when there is at least one saved task and get this response: @@ -133,7 +137,7 @@ As a client, I want to be able to make a `GET` request to `/tasks/1` when there } ``` -### Update Task +#### Update Task As a client, I want to be able to make a `PUT` request to `/tasks/1` when there is at least one saved task with this request body: @@ -159,9 +163,9 @@ and get this response: } ``` -Note that the update endpoint does update the `completed_at` attribute. This will be updated with custom endpoints implemented in Wave 03. +Note that the update endpoint does update the `completed_at` attribute. This will be updated with custom endpoints implemented in Wave 3. -### Delete Task: Deleting a Task +#### Delete Task: Deleting a Task As a client, I want to be able to make a `DELETE` request to `/tasks/1` when there is at least one saved task and get this response: @@ -173,7 +177,7 @@ As a client, I want to be able to make a `DELETE` request to `/tasks/1` when the } ``` -### No matching Task: Get, Update, and Delete +#### No Matching Task: Get, Update, and Delete As a client, if I make any of the following requests: @@ -181,7 +185,7 @@ As a client, if I make any of the following requests: * `UPDATE` `/tasks/` * `DELETE` `/tasks/` -and there is no existing task with `task_id` +and there is no existing task with an `id` of `task_id` The response code should be `404`. @@ -190,9 +194,9 @@ You may choose the response body. Make sure to complete the tests for non-existing tasks to check that the correct response body is returned. -### Create a Task: Invalid Task With Missing Data +#### Create a Task: Invalid Task With Missing Data -#### Missing `title` +##### Missing `title` As a client, I want to be able to make a `POST` request to `/tasks` with the following HTTP request body @@ -215,7 +219,7 @@ and get this response: so that I know I did not create a Task that is saved in the database. -#### Missing `description` +##### Missing `description` If the HTTP request is missing `description`, we should also get this response: @@ -226,15 +230,3 @@ If the HTTP request is missing `description`, we should also get this response: "details": "Invalid data" } ``` - -#### Missing `completed_at` - -If the HTTP request is missing `completed_at`, we should also get this response: - -`400 Bad Request` - -```json -{ - "details": "Invalid data" -} -``` diff --git a/ada-project-docs/wave_05.md b/ada-project-docs/wave_05.md index cdefda1be..bb601b0b5 100644 --- a/ada-project-docs/wave_05.md +++ b/ada-project-docs/wave_05.md @@ -18,7 +18,7 @@ This wave requires more test writing. - These tests are currently skipped with `@pytest.mark.skip(reason="test to be completed by student")` and the function body has `pass` in it. Once you implement these tests you should remove the `skip` decorator and the `pass`. - For the tests you write, use the requirements in this document to guide your test writing. - Pay attention to the exact shape of the expected JSON. Double-check nested data structures and the names of the keys for any mispellings. -- You can model your tests off of the Wave 01 tests for Tasks. +- You can model your tests off of the Wave 1 tests for Tasks. - Some tests use a [fixture](https://docs.pytest.org/en/6.2.x/fixture.html) named `one_goal` that is defined in `tests/conftest.py`. This fixture saves a specific goal to the test database. @@ -28,9 +28,9 @@ This wave requires more test writing. There should be a `Goal` model that lives in `app/models/goal.py`. -Goals should contain these attributes. Feel free to change the name of the `goal_id` column if you would like. **The tests require the title column to be named exactly** as `title`. +Goals should contain these attributes. **The tests require the title column to be named exactly** as `title`. -- `goal_id`: a primary key for each goal +- `id`: a primary key for each goal - `title`: text to name the goal ### Tips diff --git a/ada-project-docs/wave_06.md b/ada-project-docs/wave_06.md index b781458ce..699738ba8 100644 --- a/ada-project-docs/wave_06.md +++ b/ada-project-docs/wave_06.md @@ -16,7 +16,7 @@ Secondly, we should create our new route, `/goals//tasks`, so that our ### Tips -- Use independent research to discover how to set up a one-to-many relationship in Flask. +- Use lesson materials and independent research to review how to set up a one-to-many relationship in Flask. - Remember to run `flask db migrate` and `flask db upgrade` whenever there is a change to the model. - Pay attention to the exact shape of the expected JSON. Double-check nested data structures and the names of the keys for any mispellings. - Use the tests in `tests/test_wave_06.py` to guide your implementation. @@ -24,24 +24,18 @@ Secondly, we should create our new route, `/goals//tasks`, so that our ### Updates to the Goal Model -Use independent research to discover how to set up a one-to-many relationship in Flask. - The Goal model should have a _relationship_ with the model Task. -After learning the strategy for creating a one-to-many relationship, in the Goal model, we recommend: - -- Setting the `lazy` value to `True` +After reviewing the strategy for creating a one-to-many relationship, it is up to you if you would like to add convenience attributes for accessing the `Goal` model from it's related `Task`s and vice versa, accessing the list of associated `Task`s from a `Goal` model. ### Updates to the Task Model -Use independent research to discover how to set up a one-to-many relationship in Flask. - The Task model should belong to one `Goal`. -After learning the strategy for creating a one-to-many relationship, in the Task model, we recommend: +After reviewing the strategy for creating a one-to-many relationship, in the Task model, we recommend: - Setting the foreign key to `goal`'s primary key column -- Setting the `nullable` to `True` +- Using `Optional` syntax to make the attribute nullable Remember to run `flask db migrate` and `flask db upgrade` whenever there is a change to the model. diff --git a/ada-project-docs/wave_07.md b/ada-project-docs/wave_07.md index 9518501f8..d3a5a5b8d 100644 --- a/ada-project-docs/wave_07.md +++ b/ada-project-docs/wave_07.md @@ -8,7 +8,15 @@ Our goal is to make our project accessible online! Deploy this project to Render. -Then, add some Task records and Goal records to the production database. +When deploying a web service to Render, it will try to be helpful and set the `Language` field for you, but it doesn't always select the correct option. + +![A screen shot of Render's UI for deploying a web service with the Language field circled in red showing Docker selected](assets/render-show-language-field.png) + +Our language for this project should be `Python 3`, which you can select from a drop down if needed by clicking on the current value of the `Language` field. + +![A screen shot of Render's UI for deploying a web service showing the drop down for selecting a runtime value](assets/render-show-language-drop-down.png) + +Once deployed, add some Task records and Goal records to the production database. Be sure to grab the URL of your deployed app. It will be submitted at the time of project submission. diff --git a/app/__init__.py b/app/__init__.py index 2764c4cc8..3c581ceeb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,30 +1,18 @@ from flask import Flask -from flask_sqlalchemy import SQLAlchemy -from flask_migrate import Migrate +from .db import db, migrate +from .models import task, goal import os -from dotenv import load_dotenv - -db = SQLAlchemy() -migrate = Migrate() -load_dotenv() - - -def create_app(test_config=None): +def create_app(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") - else: - app.config["TESTING"] = True - app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( - "SQLALCHEMY_TEST_DATABASE_URI") + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI') - # Import models here for Alembic setup - from app.models.task import Task - from app.models.goal import Goal + if config: + # Merge `config` into the app's configuration + # to override the app's default settings for testing + app.config.update(config) db.init_app(app) migrate.init_app(app, db) diff --git a/app/db.py b/app/db.py new file mode 100644 index 000000000..3ada8d10c --- /dev/null +++ b/app/db.py @@ -0,0 +1,6 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from .models.base import Base + +db = SQLAlchemy(model_class=Base) +migrate = Migrate() \ No newline at end of file diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 000000000..227841686 --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,4 @@ +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass \ No newline at end of file diff --git a/app/models/goal.py b/app/models/goal.py index b0ed11dd8..ef9ac71be 100644 --- a/app/models/goal.py +++ b/app/models/goal.py @@ -1,5 +1,18 @@ -from app import db - +from sqlalchemy.orm import Mapped, mapped_column, relationship +from ..db import db class Goal(db.Model): - goal_id = db.Column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] + tasks: Mapped[list["Task"]] = relationship(back_populates="goal") + + def to_dict(self): + return { + "id": self.id, + "title": self.title, + } + + @classmethod + def from_dict(cls, goal_data): + new_goal = cls(title=goal_data["title"]) + return new_goal \ No newline at end of file diff --git a/app/models/task.py b/app/models/task.py index c91ab281f..a6b4d97b1 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -1,5 +1,46 @@ -from app import db +from sqlalchemy.orm import Mapped, mapped_column, relationship +from ..db import db +from datetime import datetime +from typing import Optional +from sqlalchemy import ForeignKey class Task(db.Model): - task_id = db.Column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] + description: Mapped[str] + completed_at: Mapped[Optional[datetime]] + goal_id: Mapped[Optional[int]] = mapped_column(ForeignKey("goal.id")) + goal: Mapped[Optional["Goal"]] = relationship(back_populates="tasks") + + def to_dict(self): + if self.completed_at: + completed = True + else: + completed = False + + task_dict = { + "id": self.id, + "title": self.title, + "description": self.description, + "is_complete": completed, + } + + if self.goal_id: + task_dict["goal_id"] = self.goal_id + + return task_dict + + @classmethod + def from_dict(cls, task_data): + completed_at = task_data.get("completed_at") + goal_id = task_data.get("goal_id") + + new_task = cls( + title=task_data["title"], + description=task_data["description"], + completed_at=completed_at, + goal_id=goal_id + ) + + return new_task \ No newline at end of file diff --git a/app/routes.py b/app/routes.py deleted file mode 100644 index 3aae38d49..000000000 --- a/app/routes.py +++ /dev/null @@ -1 +0,0 @@ -from flask import Blueprint \ No newline at end of file diff --git a/app/routes/goal_routes.py b/app/routes/goal_routes.py new file mode 100644 index 000000000..97a02403a --- /dev/null +++ b/app/routes/goal_routes.py @@ -0,0 +1,89 @@ +from flask import Blueprint, request, abort, make_response, Response +from .route_utilities import validate_model +from app.models.goal import Goal +from app.models.task import Task +from ..db import db + + +bp = Blueprint("goals_bp", __name__, url_prefix="/goals") + +@bp.get("") +def get_all_goals(): + query = db.select(Goal) + goals = db.session.scalars(query) + goals_response = [goal.to_dict() for goal in goals] + return goals_response + + +@bp.get("/") +def get_one_goal(goal_id): + goal = validate_model(Goal, goal_id) + return {"goal": goal.to_dict()} + + +@bp.put("/") +def update_goal(goal_id): + goal = validate_model(Goal, goal_id) + request_body = request.get_json() + goal.title = request_body["title"] + + db.session.add(goal) + db.session.commit() + + return {"goal": goal.to_dict()} + + +@bp.post("") +def create_goal(): + request_body = request.get_json() + + try: + new_goal = Goal.from_dict(request_body) + + except KeyError as error: + response = {"details": f"Invalid data"} + abort(make_response(response, 400)) + + db.session.add(new_goal) + db.session.commit() + + return {"goal": new_goal.to_dict()}, 201 + + +@bp.delete("/") +def delete_goal(goal_id): + goal = validate_model(Goal, goal_id) + db.session.delete(goal) + db.session.commit() + + message = f"Goal {goal_id} \"{goal.title}\" successfully deleted" + return {"details": message} + + +@bp.get("//tasks") +def get_tasks_from_goal(goal_id): + goal = validate_model(Goal, goal_id) + tasks = [task.to_dict() for task in goal.tasks] + + response = goal.to_dict() + response["tasks"] = tasks + return response + + +@bp.post("//tasks") +def add_tasks_tp_goal(goal_id): + goal = validate_model(Goal, goal_id) + request_body = request.get_json() + task_ids = request_body["task_ids"] + + for task_id in task_ids: + task = validate_model(Task, task_id) + goal.tasks.append(task) + + db.session.commit() + + response = { + "id": goal.id, + "task_ids": task_ids, + } + return response \ No newline at end of file diff --git a/app/routes/route_utilities.py b/app/routes/route_utilities.py new file mode 100644 index 000000000..2dc8ede2d --- /dev/null +++ b/app/routes/route_utilities.py @@ -0,0 +1,18 @@ +from flask import abort, make_response +from ..db import db + +def validate_model(cls, model_id): + try: + model_id = int(model_id) + except: + response = {"message": f"{cls.__name__} {model_id} invalid"} + abort(make_response(response , 400)) + + query = db.select(cls).where(cls.id == model_id) + model = db.session.scalar(query) + + if not model: + response = {"message": f"{cls.__name__} {model_id} not found"} + abort(make_response(response, 404)) + + return model \ No newline at end of file diff --git a/app/routes/task_routes.py b/app/routes/task_routes.py new file mode 100644 index 000000000..569567b9c --- /dev/null +++ b/app/routes/task_routes.py @@ -0,0 +1,119 @@ +from flask import Blueprint, request, abort, make_response, Response +from .route_utilities import validate_model +from app.models.task import Task +from datetime import datetime +from ..db import db +import requests +import os + + +SLACK_API_URL = os.environ["SLACK_API_URL"] +SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"] + +bp = Blueprint("tasks_bp", __name__, url_prefix="/tasks") + +@bp.get("") +def get_all_tasks(): + query = db.select(Task) + sort_method = request.args.get('sort') + + if sort_method and sort_method == "asc": + query = query.order_by(Task.title.asc()) + if sort_method and sort_method == "desc": + query = query.order_by(Task.title.desc()) + + tasks = db.session.scalars(query) + tasks_response = [task.to_dict() for task in tasks] + + return tasks_response + + +@bp.post("") +def create_task(): + request_body = request.get_json() + + try: + new_task = Task.from_dict(request_body) + + except KeyError as error: + response = {"details": f"Invalid data"} + abort(make_response(response, 400)) + + db.session.add(new_task) + db.session.commit() + + return {"task": new_task.to_dict()}, 201 + + +@bp.get("/") +def get_one_task(task_id): + task = validate_model(Task, task_id) + return {"task": task.to_dict()} + + +@bp.put("/") +def update_task(task_id): + task = validate_model(Task, task_id) + request_body = request.get_json() + + task.title = request_body["title"] + task.description = request_body["description"] + + db.session.add(task) + db.session.commit() + + return {"task": task.to_dict()} + + +@bp.delete("/") +def delete_task(task_id): + task = validate_model(Task, task_id) + db.session.delete(task) + db.session.commit() + + message = f"Task {task_id} \"{task.title}\" successfully deleted" + return {"details": message} + + +@bp.patch("//mark_complete") +def mark_task_complete(task_id): + task = validate_model(Task, task_id) + task.completed_at = datetime.now() + + db.session.add(task) + db.session.commit() + + #post_to_slack(task) + + return {"task": task.to_dict()} + + +@bp.patch("//mark_incomplete") +def mark_task_incomplete(task_id): + task = validate_model(Task, task_id) + task.completed_at = None + + db.session.add(task) + db.session.commit() + + #post_to_slack(task) + + return {"task": task.to_dict()} + + +def post_to_slack(task): + headers = { + "Authorization": f"Bearer {SLACK_BOT_TOKEN}", + } + if task.completed_at: + data = { + "channel": "instructors", + "text": f"Task {task.title} has been marked complete", + } + else: + data = { + "channel": "general", + "text": f"Task {task.title} has been marked incomplete", + } + + r = requests.post(SLACK_API_URL, headers=headers, data=data) \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 000000000..0e0484415 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 000000000..ec9d45c26 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# 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,flask_migrate + +[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 + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[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 diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 000000000..4c9709271 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +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') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# 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', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# 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 get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +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=get_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.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/373d97eafa7b_adds_models.py b/migrations/versions/373d97eafa7b_adds_models.py new file mode 100644 index 000000000..ad465c4a6 --- /dev/null +++ b/migrations/versions/373d97eafa7b_adds_models.py @@ -0,0 +1,42 @@ +"""adds models + +Revision ID: 373d97eafa7b +Revises: +Create Date: 2024-09-19 16:29:07.548857 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '373d97eafa7b' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('goal', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('task', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=False), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('goal_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['goal_id'], ['goal.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('task') + op.drop_table('goal') + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 453f0ef6a..af8fc4cf4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,34 +1,26 @@ -alembic==1.5.4 -attrs==20.3.0 -autopep8==1.5.5 -blinker==1.4 -certifi==2020.12.5 -chardet==4.0.0 -click==7.1.2 -Flask==1.1.2 -Flask-Migrate==2.6.0 -Flask-SQLAlchemy==2.4.4 -gunicorn==20.1.0 -idna==2.10 -iniconfig==1.1.1 -itsdangerous==1.1.0 -Jinja2==2.11.3 -Mako==1.1.4 -MarkupSafe==1.1.1 -packaging==20.9 -pluggy==0.13.1 -psycopg2-binary==2.9.5 -py==1.11.0 -pycodestyle==2.6.0 -pyparsing==2.4.7 -pytest==7.1.1 -pytest-cov==2.12.1 -python-dateutil==2.8.1 -python-dotenv==0.15.0 -python-editor==1.0.4 -requests==2.25.1 -six==1.15.0 -SQLAlchemy==1.3.23 -toml==0.10.2 -urllib3==1.26.5 -Werkzeug==1.0.1 +alembic==1.13.1 +blinker==1.7.0 +certifi==2024.8.30 +charset-normalizer==3.3.2 +click==8.1.7 +Flask==3.0.2 +Flask-Migrate==4.0.5 +Flask-SQLAlchemy==3.1.1 +greenlet==3.0.3 +gunicorn==21.2.0 +idna==3.10 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.3 +Mako==1.3.2 +MarkupSafe==2.1.5 +packaging==23.2 +pluggy==1.4.0 +psycopg2-binary==2.9.9 +pytest==8.0.0 +python-dotenv==1.0.1 +requests==2.32.3 +SQLAlchemy==2.0.25 +typing_extensions==4.9.0 +urllib3==2.2.3 +Werkzeug==3.0.1 diff --git a/tests/conftest.py b/tests/conftest.py index 6639378e6..e370e597b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,23 @@ import pytest from app import create_app +from app.db import db +from flask.signals import request_finished +from dotenv import load_dotenv +import os from app.models.task import Task from app.models.goal import Goal -from app import db from datetime import datetime -from flask.signals import request_finished +load_dotenv() @pytest.fixture def app(): - # create the app with a test config dictionary - app = create_app({"TESTING": True}) + # create the app with a test configuration + test_config = { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": os.environ.get('SQLALCHEMY_TEST_DATABASE_URI') + } + app = create_app(test_config) @request_finished.connect_via(app) def expire_session(sender, response, **extra): @@ -35,8 +42,9 @@ def client(app): # This fixture creates a task and saves it in the database @pytest.fixture def one_task(app): - new_task = Task( - title="Go on my daily walk 🏞", description="Notice something new every day", completed_at=None) + new_task = Task(title="Go on my daily walk 🏞", + description="Notice something new every day", + completed_at=None) db.session.add(new_task) db.session.commit() @@ -48,12 +56,15 @@ def one_task(app): @pytest.fixture def three_tasks(app): db.session.add_all([ - Task( - title="Water the garden 🌷", description="", completed_at=None), - Task( - title="Answer forgotten email 📧", description="", completed_at=None), - Task( - title="Pay my outstanding tickets 😭", description="", completed_at=None) + Task(title="Water the garden 🌷", + description="", + completed_at=None), + Task(title="Answer forgotten email 📧", + description="", + completed_at=None), + Task(title="Pay my outstanding tickets 😭", + description="", + completed_at=None) ]) db.session.commit() @@ -64,8 +75,9 @@ def three_tasks(app): # valid completed_at date @pytest.fixture def completed_task(app): - new_task = Task( - title="Go on my daily walk 🏞", description="Notice something new every day", completed_at=datetime.utcnow()) + new_task = Task(title="Go on my daily walk 🏞", + description="Notice something new every day", + completed_at=datetime.utcnow()) db.session.add(new_task) db.session.commit() diff --git a/tests/test_wave_01.py b/tests/test_wave_01.py index dca626d78..527153e09 100644 --- a/tests/test_wave_01.py +++ b/tests/test_wave_01.py @@ -2,7 +2,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_no_saved_tasks(client): # Act response = client.get("/tasks") @@ -13,7 +13,7 @@ def test_get_tasks_no_saved_tasks(client): assert response_body == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_one_saved_tasks(client, one_task): # Act response = client.get("/tasks") @@ -32,7 +32,7 @@ def test_get_tasks_one_saved_tasks(client, one_task): ] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task(client, one_task): # Act response = client.get("/tasks/1") @@ -51,7 +51,7 @@ def test_get_task(client, one_task): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task_not_found(client): # Act response = client.get("/tasks/1") @@ -59,14 +59,11 @@ def test_get_task_not_found(client): # Assert assert response.status_code == 404 + assert "message" in response_body + assert response_body["message"] == "Task 1 not found" - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task(client): # Act response = client.post("/tasks", json={ @@ -93,7 +90,7 @@ def test_create_task(client): assert new_task.completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_update_task(client, one_task): # Act response = client.put("/tasks/1", json={ @@ -119,7 +116,7 @@ def test_update_task(client, one_task): assert task.completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_update_task_not_found(client): # Act response = client.put("/tasks/1", json={ @@ -130,14 +127,11 @@ def test_update_task_not_found(client): # Assert assert response.status_code == 404 - - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** + assert "message" in response_body + assert response_body["message"] == "Task 1 not found" -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_delete_task(client, one_task): # Act response = client.delete("/tasks/1") @@ -152,7 +146,7 @@ def test_delete_task(client, one_task): assert Task.query.get(1) == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_delete_task_not_found(client): # Act response = client.delete("/tasks/1") @@ -161,15 +155,12 @@ def test_delete_task_not_found(client): # Assert assert response.status_code == 404 - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - + assert "message" in response_body + assert response_body["message"] == "Task 1 not found" assert Task.query.all() == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task_must_contain_title(client): # Act response = client.post("/tasks", json={ @@ -186,7 +177,7 @@ def test_create_task_must_contain_title(client): assert Task.query.all() == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_task_must_contain_description(client): # Act response = client.post("/tasks", json={ @@ -200,4 +191,4 @@ def test_create_task_must_contain_description(client): assert response_body == { "details": "Invalid data" } - assert Task.query.all() == [] + assert Task.query.all() == [] \ No newline at end of file diff --git a/tests/test_wave_02.py b/tests/test_wave_02.py index a087e0909..ea508eb03 100644 --- a/tests/test_wave_02.py +++ b/tests/test_wave_02.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +#@pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_sorted_asc(client, three_tasks): # Act response = client.get("/tasks?sort=asc") @@ -29,7 +29,7 @@ def test_get_tasks_sorted_asc(client, three_tasks): ] -@pytest.mark.skip(reason="No way to test this feature yet") +#@pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_sorted_desc(client, three_tasks): # Act response = client.get("/tasks?sort=desc") @@ -54,4 +54,4 @@ def test_get_tasks_sorted_desc(client, three_tasks): "id": 2, "is_complete": False, "title": "Answer forgotten email 📧"}, - ] + ] \ No newline at end of file diff --git a/tests/test_wave_03.py b/tests/test_wave_03.py index 32d379822..4919a93df 100644 --- a/tests/test_wave_03.py +++ b/tests/test_wave_03.py @@ -5,7 +5,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_on_incomplete_task(client, one_task): # Arrange """ @@ -42,7 +42,7 @@ def test_mark_complete_on_incomplete_task(client, one_task): assert Task.query.get(1).completed_at -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_on_complete_task(client, completed_task): # Act response = client.patch("/tasks/1/mark_incomplete") @@ -62,7 +62,7 @@ def test_mark_incomplete_on_complete_task(client, completed_task): assert Task.query.get(1).completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_on_completed_task(client, completed_task): # Arrange """ @@ -99,7 +99,7 @@ def test_mark_complete_on_completed_task(client, completed_task): assert Task.query.get(1).completed_at -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_on_incomplete_task(client, one_task): # Act response = client.patch("/tasks/1/mark_incomplete") @@ -119,7 +119,7 @@ def test_mark_incomplete_on_incomplete_task(client, one_task): assert Task.query.get(1).completed_at == None -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_complete_missing_task(client): # Act response = client.patch("/tasks/1/mark_complete") @@ -127,14 +127,11 @@ def test_mark_complete_missing_task(client): # Assert assert response.status_code == 404 + assert "message" in response_body + assert response_body["message"] == "Task 1 not found" - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_mark_incomplete_missing_task(client): # Act response = client.patch("/tasks/1/mark_incomplete") @@ -142,8 +139,64 @@ def test_mark_incomplete_missing_task(client): # Assert assert response.status_code == 404 + assert "message" in response_body + assert response_body["message"] == "Task 1 not found" + + +# Let's add this test for creating tasks, now that +# the completion functionality has been implemented +# @pytest.mark.skip(reason="No way to test this feature yet") +def test_create_task_with_valid_completed_at(client): + # Act + response = client.post("/tasks", json={ + "title": "A Brand New Task", + "description": "Test Description", + "completed_at": datetime.utcnow() + }) + response_body = response.get_json() + + # Assert + assert response.status_code == 201 + assert "task" in response_body + assert response_body == { + "task": { + "id": 1, + "title": "A Brand New Task", + "description": "Test Description", + "is_complete": True + } + } + new_task = Task.query.get(1) + assert new_task + assert new_task.title == "A Brand New Task" + assert new_task.description == "Test Description" + assert new_task.completed_at - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** + +# Let's add this test for updating tasks, now that +# the completion functionality has been implemented +# @pytest.mark.skip(reason="No way to test this feature yet") +def test_update_task_with_completed_at_date(client, completed_task): + # Act + response = client.put("/tasks/1", json={ + "title": "Updated Task Title", + "description": "Updated Test Description", + "completed_at": datetime.utcnow() + }) + response_body = response.get_json() + + # Assert + assert response.status_code == 200 + assert "task" in response_body + assert response_body == { + "task": { + "id": 1, + "title": "Updated Task Title", + "description": "Updated Test Description", + "is_complete": True + } + } + task = Task.query.get(1) + assert task.title == "Updated Task Title" + assert task.description == "Updated Test Description" + assert task.completed_at \ No newline at end of file diff --git a/tests/test_wave_05.py b/tests/test_wave_05.py index aee7c52a1..d2db1900b 100644 --- a/tests/test_wave_05.py +++ b/tests/test_wave_05.py @@ -1,7 +1,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goals_no_saved_goals(client): # Act response = client.get("/goals") @@ -12,7 +12,7 @@ def test_get_goals_no_saved_goals(client): assert response_body == [] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goals_one_saved_goal(client, one_goal): # Act response = client.get("/goals") @@ -29,7 +29,7 @@ def test_get_goals_one_saved_goal(client, one_goal): ] -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_goal(client, one_goal): # Act response = client.get("/goals/1") @@ -46,22 +46,19 @@ def test_get_goal(client, one_goal): } -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_get_goal_not_found(client): - pass # Act response = client.get("/goals/1") response_body = response.get_json() - raise Exception("Complete test") # Assert - # ---- Complete Test ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Test ---- + assert response.status_code == 404 + assert "message" in response_body + assert response_body["message"] == "Goal 1 not found" -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_create_goal(client): # Act response = client.post("/goals", json={ @@ -80,34 +77,38 @@ def test_create_goal(client): } -@pytest.mark.skip(reason="test to be completed by student") +# @pytest.mark.skip(reason="test to be completed by student") def test_update_goal(client, one_goal): - raise Exception("Complete test") # Act - # ---- Complete Act Here ---- + response = client.put("/goals/1", json={ + "title": "Updated Goal Title" + }) + response_body = response.get_json() # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # assertion 3 goes here - # ---- Complete Assertions Here ---- + assert response.status_code == 200 + assert "goal" in response_body + assert response_body == { + "goal": { + "id": 1, + "title": "Updated Goal Title" + } + } -@pytest.mark.skip(reason="test to be completed by student") def test_update_goal_not_found(client): - raise Exception("Complete test") # Act - # ---- Complete Act Here ---- + response = client.put("/goals/1", json={ + "title": "Updated Goal Title" + }) + response_body = response.get_json() # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Assertions Here ---- + assert response.status_code == 404 + assert "message" in response_body + assert response_body["message"] == "Goal 1 not found" -@pytest.mark.skip(reason="No way to test this feature yet") def test_delete_goal(client, one_goal): # Act response = client.delete("/goals/1") @@ -124,27 +125,24 @@ def test_delete_goal(client, one_goal): response = client.get("/goals/1") assert response.status_code == 404 - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** + response_body = response.get_json() + assert "message" in response_body + assert response_body["message"] == "Goal 1 not found" -@pytest.mark.skip(reason="test to be completed by student") -def test_delete_goal_not_found(client): - raise Exception("Complete test") +# @pytest.mark.skip(reason="test to be completed by student") +def test_delete_goal_not_found(client): # Act - # ---- Complete Act Here ---- + response = client.delete("/goals/1") + response_body = response.get_json() # Assert - # ---- Complete Assertions Here ---- - # assertion 1 goes here - # assertion 2 goes here - # ---- Complete Assertions Here ---- + assert response.status_code == 404 + assert "message" in response_body + assert response_body["message"] == "Goal 1 not found" -@pytest.mark.skip(reason="No way to test this feature yet") def test_create_goal_missing_title(client): # Act response = client.post("/goals", json={}) @@ -154,4 +152,4 @@ def test_create_goal_missing_title(client): assert response.status_code == 400 assert response_body == { "details": "Invalid data" - } + } \ No newline at end of file diff --git a/tests/test_wave_06.py b/tests/test_wave_06.py index 8afa4325e..0987c684a 100644 --- a/tests/test_wave_06.py +++ b/tests/test_wave_06.py @@ -2,7 +2,7 @@ import pytest -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_post_task_ids_to_goal(client, one_goal, three_tasks): # Act response = client.post("/goals/1/tasks", json={ @@ -23,7 +23,7 @@ def test_post_task_ids_to_goal(client, one_goal, three_tasks): assert len(Goal.query.get(1).tasks) == 3 -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_post_task_ids_to_goal_already_with_goals(client, one_task_belongs_to_one_goal, three_tasks): # Act response = client.post("/goals/1/tasks", json={ @@ -42,7 +42,7 @@ def test_post_task_ids_to_goal_already_with_goals(client, one_task_belongs_to_on assert len(Goal.query.get(1).tasks) == 2 -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal_no_goal(client): # Act response = client.get("/goals/1/tasks") @@ -50,14 +50,11 @@ def test_get_tasks_for_specific_goal_no_goal(client): # Assert assert response.status_code == 404 + assert "message" in response_body + assert response_body["message"] == "Goal 1 not found" - raise Exception("Complete test with assertion about response body") - # ***************************************************************** - # **Complete test with assertion about response body*************** - # ***************************************************************** - -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal_no_tasks(client, one_goal): # Act response = client.get("/goals/1/tasks") @@ -74,7 +71,7 @@ def test_get_tasks_for_specific_goal_no_tasks(client, one_goal): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_tasks_for_specific_goal(client, one_task_belongs_to_one_goal): # Act response = client.get("/goals/1/tasks") @@ -99,7 +96,7 @@ def test_get_tasks_for_specific_goal(client, one_task_belongs_to_one_goal): } -@pytest.mark.skip(reason="No way to test this feature yet") +# @pytest.mark.skip(reason="No way to test this feature yet") def test_get_task_includes_goal_id(client, one_task_belongs_to_one_goal): response = client.get("/tasks/1") response_body = response.get_json() @@ -115,4 +112,4 @@ def test_get_task_includes_goal_id(client, one_task_belongs_to_one_goal): "description": "Notice something new every day", "is_complete": False } - } + } \ No newline at end of file