diff --git a/backend/pdm.lock b/backend/pdm.lock index 809be35..ad8a011 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -393,12 +393,12 @@ files = [ [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" requires_python = ">=3.8" summary = "plugin and hook calling mechanisms for python" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [[package]] @@ -529,31 +529,31 @@ files = [ [[package]] name = "pytest" -version = "8.0.2" +version = "8.1.1" requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" dependencies = [ "colorama; sys_platform == \"win32\"", "iniconfig", "packaging", - "pluggy<2.0,>=1.3.0", + "pluggy<2.0,>=1.4", ] files = [ - {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, - {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, ] [[package]] name = "pytest-asyncio" -version = "0.23.5" +version = "0.23.5.post1" requires_python = ">=3.8" summary = "Pytest support for asyncio" dependencies = [ "pytest<9,>=7.0.0", ] files = [ - {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, - {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, + {file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"}, + {file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"}, ] [[package]] @@ -631,27 +631,27 @@ files = [ [[package]] name = "ruff" -version = "0.3.0" +version = "0.3.2" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"}, - {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"}, - {file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"}, - {file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"}, - {file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"}, - {file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"}, + {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, + {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, + {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, + {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, + {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, + {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, ] [[package]] @@ -709,7 +709,7 @@ files = [ [[package]] name = "uvicorn" -version = "0.27.1" +version = "0.28.0" requires_python = ">=3.8" summary = "The lightning-fast ASGI server." dependencies = [ @@ -717,13 +717,13 @@ dependencies = [ "h11>=0.8", ] files = [ - {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, - {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, + {file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"}, + {file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"}, ] [[package]] name = "uvicorn" -version = "0.27.1" +version = "0.28.0" extras = ["standard"] requires_python = ">=3.8" summary = "The lightning-fast ASGI server." @@ -732,14 +732,14 @@ dependencies = [ "httptools>=0.5.0", "python-dotenv>=0.13", "pyyaml>=5.1", - "uvicorn==0.27.1", + "uvicorn==0.28.0", "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", "watchfiles>=0.13", "websockets>=10.4", ] files = [ - {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, - {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, + {file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"}, + {file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"}, ] [[package]] diff --git a/backend/src/neo_neo_todo/models/todo.py b/backend/src/neo_neo_todo/models/todo.py index bab997e..045928a 100644 --- a/backend/src/neo_neo_todo/models/todo.py +++ b/backend/src/neo_neo_todo/models/todo.py @@ -72,3 +72,103 @@ async def select_todo(db: AsyncConnectionPool, id: int, member_id: int) -> Todo member = await curr.fetchone() return None if member is None else member + + +async def insert_todo( + db: AsyncConnectionPool, + member_id: int, + task: str, + complete: bool, + due: datetime | None, +) -> Todo | None: + """ + Insert a todo into the database + + Return the inserted todo object or None + """ + async with db.connection() as conn: + async with conn.cursor(row_factory=class_row(Todo)) as curr: + query = sql.SQL( + """ + INSERT INTO todos (complete, due, member_id, task) + VALUES ((%s), (%s), (%s), (%s)) + RETURNING id, complete, due, task + """ + ) + await curr.execute( + query, + ( + complete, + due, + member_id, + task, + ), + ) + + todo = await curr.fetchone() + + return None if todo is None else todo + + +async def update_todo( + db: AsyncConnectionPool, + id: int, + member_id: int, + task: str, + complete: bool, + due: datetime | None, +) -> Todo | None: + """ + Update a todo in the database + + Return the updated todo object or None + """ + async with db.connection() as conn: + async with conn.cursor(row_factory=class_row(Todo)) as curr: + query = sql.SQL( + """ + UPDATE todos + SET complete = (%s), due = (%s), task = (%s) + WHERE id = (%s) AND member_id = (%s) + RETURNING id, complete, due, task + """ + ) + await curr.execute( + query, + ( + complete, + due, + task, + id, + member_id, + ), + ) + + todo = await curr.fetchone() + + return None if todo is None else todo + + +async def delete_todo(db: AsyncConnectionPool, id: int, member_id: int) -> bool: + """ + Delete a todo with an id and its member_id. Note that you can not delete other users' todos + + Return True if deletion is successful and False otherwise + """ + async with db.connection() as conn: + async with conn.cursor(row_factory=class_row(Todo)) as curr: + query = sql.SQL( + """ + DELETE FROM todos + WHERE id = (%s) AND member_id = (%s) + """ + ) + await curr.execute( + query, + ( + id, + member_id, + ), + ) + + return curr.rowcount > 0 diff --git a/backend/tests/test_model_todo.py b/backend/tests/test_model_todo.py index 7e40de8..28faedc 100644 --- a/backend/tests/test_model_todo.py +++ b/backend/tests/test_model_todo.py @@ -1,6 +1,13 @@ import asyncio +from datetime import datetime -from src.neo_neo_todo.models.todo import select_todo, select_todos +from src.neo_neo_todo.models.todo import ( + delete_todo, + insert_todo, + select_todo, + select_todos, + update_todo, +) async def test_model_todo_select_todos(db_pool): @@ -39,3 +46,54 @@ async def test_model_todo_select_todo(db_pool): non_existent_member_id_todo = await select_todo(db_pool, member_id=1234123, id=1) assert non_existent_member_id_todo is None + + +async def test_model_todo_insert_todo(db_pool): + todo_insert_success = await insert_todo( + db_pool, member_id=1, task="Test Task 4", complete=False, due=None + ) + + assert todo_insert_success + assert not todo_insert_success.complete + assert todo_insert_success.task == "Test Task 4" + + +async def test_model_todo_update_todo(db_pool): + due = datetime.now() + + todo_updated_1 = await update_todo( + db_pool, + id=3, + member_id=1, + task="Updated Task Test", + complete=True, + due=due, + ) + + assert todo_updated_1 + assert todo_updated_1.id == 3 + assert todo_updated_1.task == "Updated Task Test" + assert todo_updated_1.complete + assert todo_updated_1.due + + # non existent id and member_id + todo_updated_2 = await update_todo( + db_pool, + id=99999, + member_id=2, + task="Updated Task Test", + complete=True, + due=due, + ) + + assert not todo_updated_2 + + +async def test_model_todo_delete_todo(db_pool): + delete_todo_success = await delete_todo(db_pool, id=1, member_id=1) + + assert delete_todo_success + + delete_todo_fail = await delete_todo(db_pool, id=1, member_id=2) + + assert not delete_todo_fail