Compare commits
	
		
			3 Commits
		
	
	
		
			21fd769fa9
			...
			d92982479c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d92982479c | ||
|  | 9564b13879 | ||
|  | 3c78fe9133 | 
							
								
								
									
										122
									
								
								backend/src/backend/blueprints/todos.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								backend/src/backend/blueprints/todos.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | from dataclasses import dataclass | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from typing import cast | ||||||
|  |  | ||||||
|  | from quart import Blueprint, ResponseReturnValue, g | ||||||
|  | from quart_auth import current_user, login_required | ||||||
|  | from quart_rate_limiter import rate_limit | ||||||
|  | from quart_schema import validate_querystring, validate_request, validate_response | ||||||
|  |  | ||||||
|  | from backend.lib.api_error import APIError | ||||||
|  | from backend.models.todo import ( | ||||||
|  |     Todo, | ||||||
|  |     delete_todo, | ||||||
|  |     insert_todo, | ||||||
|  |     select_todo, | ||||||
|  |     select_todos, | ||||||
|  |     update_todo, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | blueprint = Blueprint("todos", __name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class TodoData: | ||||||
|  |     complete: bool | ||||||
|  |     due: datetime | None | ||||||
|  |     task: str | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @blueprint.post("/todos/") | ||||||
|  | @rate_limit(10, timedelta(seconds=10)) | ||||||
|  | @login_required | ||||||
|  | @validate_request(TodoData) | ||||||
|  | @validate_response(Todo, 201) | ||||||
|  | async def post_todo(data: TodoData) -> tuple[Todo, int]: | ||||||
|  |     """Create a new Todo. | ||||||
|  |     This allows todos to be created and stored. | ||||||
|  |     """ | ||||||
|  |     todo = await insert_todo( | ||||||
|  |         g.connection, | ||||||
|  |         int(cast(str, current_user.auth_id)), | ||||||
|  |         data.task, | ||||||
|  |         data.complete, | ||||||
|  |         data.due, | ||||||
|  |     ) | ||||||
|  |     return todo, 201 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @blueprint.get("/todos/<int:id>/") | ||||||
|  | @rate_limit(10, timedelta(seconds=10)) | ||||||
|  | @login_required | ||||||
|  | @validate_response(Todo) | ||||||
|  | async def get_todo(id: int) -> Todo: | ||||||
|  |     """Get a todo. | ||||||
|  |     Fetch a Todo by its ID. | ||||||
|  |     """ | ||||||
|  |     todo = await select_todo(g.connection, id, int(cast(str, current_user.auth_id))) | ||||||
|  |     if todo is None: | ||||||
|  |         raise APIError(404, "NOT_FOUND") | ||||||
|  |     else: | ||||||
|  |         return todo | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class Todos: | ||||||
|  |     todos: list[Todo] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass | ||||||
|  | class TodoFilter: | ||||||
|  |     complete: bool | None = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @blueprint.get("/todos/") | ||||||
|  | @rate_limit(10, timedelta(seconds=10)) | ||||||
|  | @login_required | ||||||
|  | @validate_response(Todos) | ||||||
|  | @validate_querystring(TodoFilter) | ||||||
|  | async def get_todos(query_args: TodoFilter) -> Todos: | ||||||
|  |     """Get the todos. | ||||||
|  |     Fetch all the Todos optionally based on the complete status. | ||||||
|  |     """ | ||||||
|  |     todos = await select_todos( | ||||||
|  |         g.connection, | ||||||
|  |         int(cast(str, current_user.auth_id)), | ||||||
|  |         query_args.complete, | ||||||
|  |     ) | ||||||
|  |     return Todos(todos=todos) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @blueprint.put("/todos/<int:id>/") | ||||||
|  | @rate_limit(10, timedelta(seconds=10)) | ||||||
|  | @login_required | ||||||
|  | @validate_request(TodoData) | ||||||
|  | @validate_response(Todo) | ||||||
|  | async def put_todo(id: int, data: TodoData) -> Todo: | ||||||
|  |     """Update the identified todo | ||||||
|  |     This allows the todo to be replaced with the request data. | ||||||
|  |     """ | ||||||
|  |     todo = await update_todo( | ||||||
|  |         g.connection, | ||||||
|  |         id, | ||||||
|  |         int(cast(str, current_user.auth_id)), | ||||||
|  |         data.task, | ||||||
|  |         data.complete, | ||||||
|  |         data.due, | ||||||
|  |     ) | ||||||
|  |     if todo is None: | ||||||
|  |         raise APIError(404, "NOT_FOUND") | ||||||
|  |     else: | ||||||
|  |         return todo | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @blueprint.delete("/todos/<int:id>/") | ||||||
|  | @rate_limit(10, timedelta(seconds=10)) | ||||||
|  | @login_required | ||||||
|  | async def todo_delete(id: int) -> ResponseReturnValue: | ||||||
|  |     """Delete the identified todo | ||||||
|  |     This will delete the todo. | ||||||
|  |     """ | ||||||
|  |     await delete_todo(g.connection, id, int(cast(str, current_user.auth_id))) | ||||||
|  |     return "", 202 | ||||||
| @@ -22,6 +22,7 @@ from quart_schema import QuartSchema, RequestSchemaValidationError | |||||||
| from backend.blueprints.control import blueprint as control_blueprint | from backend.blueprints.control import blueprint as control_blueprint | ||||||
| from backend.blueprints.members import blueprint as members_blueprint | from backend.blueprints.members import blueprint as members_blueprint | ||||||
| from backend.blueprints.sessions import blueprint as sessions_blueprint | from backend.blueprints.sessions import blueprint as sessions_blueprint | ||||||
|  | from backend.blueprints.todos import blueprint as todos_blueprint | ||||||
|  |  | ||||||
| # For making sure error responses are in JSON format | # For making sure error responses are in JSON format | ||||||
| from backend.lib.api_error import APIError | from backend.lib.api_error import APIError | ||||||
| @@ -42,6 +43,7 @@ logging.basicConfig(level=logging.INFO) | |||||||
| app.register_blueprint(control_blueprint) | app.register_blueprint(control_blueprint) | ||||||
| app.register_blueprint(sessions_blueprint) | app.register_blueprint(sessions_blueprint) | ||||||
| app.register_blueprint(members_blueprint) | app.register_blueprint(members_blueprint) | ||||||
|  | app.register_blueprint(todos_blueprint) | ||||||
|  |  | ||||||
|  |  | ||||||
| # rate limiting | # rate limiting | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								backend/tests/blueprints/test_todos.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								backend/tests/blueprints/test_todos.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | from quart import Quart | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def test_post_todo(app: Quart) -> None: | ||||||
|  |     test_client = app.test_client() | ||||||
|  |     async with test_client.authenticated("1"):  # type: ignore | ||||||
|  |         response = await test_client.post( | ||||||
|  |             "/todos/", | ||||||
|  |             json={"complete": False, "due": None, "task": "Test task"}, | ||||||
|  |         ) | ||||||
|  |         assert response.status_code == 201 | ||||||
|  |         assert (await response.get_json())["id"] > 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def test_get_todo(app: Quart) -> None: | ||||||
|  |     test_client = app.test_client() | ||||||
|  |     async with test_client.authenticated("1"):  # type: ignore | ||||||
|  |         response = await test_client.get("/todos/1/") | ||||||
|  |         assert response.status_code == 200 | ||||||
|  |         assert (await response.get_json())["task"] == "Test Task" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def test_put_todo(app: Quart) -> None: | ||||||
|  |     test_client = app.test_client() | ||||||
|  |     async with test_client.authenticated("1"):  # type: ignore | ||||||
|  |         response = await test_client.post( | ||||||
|  |             "/todos/", | ||||||
|  |             json={"complete": False, "due": None, "task": "Test task"}, | ||||||
|  |         ) | ||||||
|  |         todo_id = (await response.get_json())["id"] | ||||||
|  |  | ||||||
|  |         response = await test_client.put( | ||||||
|  |             f"/todos/{todo_id}/", | ||||||
|  |             json={"complete": False, "due": None, "task": "Updated"}, | ||||||
|  |         ) | ||||||
|  |         assert (await response.get_json())["task"] == "Updated" | ||||||
|  |  | ||||||
|  |         response = await test_client.get(f"/todos/{todo_id}/") | ||||||
|  |         assert (await response.get_json())["task"] == "Updated" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def test_delete_todo(app: Quart) -> None: | ||||||
|  |     test_client = app.test_client() | ||||||
|  |     async with test_client.authenticated("1"):  # type: ignore | ||||||
|  |         response = await test_client.post( | ||||||
|  |             "/todos/", | ||||||
|  |             json={"complete": False, "due": None, "task": "Test task"}, | ||||||
|  |         ) | ||||||
|  |         todo_id = (await response.get_json())["id"] | ||||||
|  |  | ||||||
|  |         await test_client.delete(f"/todos/{todo_id}/") | ||||||
|  |  | ||||||
|  |         response = await test_client.get(f"/todos/{todo_id}/") | ||||||
|  |         assert response.status_code == 404 | ||||||
		Reference in New Issue
	
	Block a user