From 46f5ef806717798bb5e6babe7da3d6672c6eb487 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 7 Sep 2024 17:35:52 +0900 Subject: [PATCH 001/229] fix : delete __init__ Content --- onestep_be/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/onestep_be/__init__.py b/onestep_be/__init__.py index cc9e574..e69de29 100644 --- a/onestep_be/__init__.py +++ b/onestep_be/__init__.py @@ -1,5 +0,0 @@ -import datetime - -# 날짜와 시간을 "YYYY.MM.DD.HHMMSS" 형식으로 포맷 -__version__ = datetime.datetime.now().strftime("%Y.%m.%d.%H%M%S") -VERSION = __version__ From 444317ae26b30d756b7c76b40768d2f976fe1060 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 12:39:38 +0900 Subject: [PATCH 002/229] feat: add version.txt in workflow file --- .github/workflows/deploy_to_ecs_prod.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 2b7bf5a..cfdb6ae 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -57,3 +57,8 @@ jobs: service: ${{ secrets.ECS_SERVICE_NAME_PROD }} cluster: ${{ secrets.ECS_CLUSTER_NAME_PROD }} wait-for-service-stability: false + + - name: Update version.txt + run: | + VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 + echo "${VERSION}" > version.txt From 0a8f85cc7915e2c62e5b23d3a6c5ac5edaf38fc2 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 12:39:45 +0900 Subject: [PATCH 003/229] feat: add version.txt in all workflow file --- .github/workflows/deploy_to_ecs.yml | 5 +++++ .github/workflows/deploy_to_ecs_test.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index a33e124..3b39291 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -56,3 +56,8 @@ jobs: service: ${{ secrets.ECS_SERVICE_NAME }} cluster: ${{ secrets.ECS_CLUSTER_NAME }} wait-for-service-stability: false + + - name: Update version.txt + run: | + VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 + echo "${VERSION}" > version.txt diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index 6f3dc21..06aab66 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -56,3 +56,8 @@ jobs: service: ${{ secrets.ECS_SERVICE_NAME }} cluster: ${{ secrets.ECS_CLUSTER_NAME }} wait-for-service-stability: false + + - name: Update version.txt + run: | + VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 + echo "${VERSION}" > version.txt From b8b0e718e972478d8f0721492cf992e9974ccd1d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 12:56:45 +0900 Subject: [PATCH 004/229] fix : add commit when version changed --- .github/workflows/deploy_to_ecs.yml | 8 ++++++++ .github/workflows/deploy_to_ecs_prod.yml | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index 3b39291..37f4b6c 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -61,3 +61,11 @@ jobs: run: | VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 echo "${VERSION}" > version.txt + + - name: Commit version update + run: | + git config --global user.name ${{ secrets.GIT_USER_NAME }} + git config --global user.email ${{ secrets.GIT_USER_EMAIL }} + git add version.txt + git commit -m "Update version to ${VERSION}" + git push origin main diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index cfdb6ae..cdde2f2 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -62,3 +62,11 @@ jobs: run: | VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 echo "${VERSION}" > version.txt + + - name: Commit version update + run: | + git config --global user.name ${{ secrets.GIT_USER_NAME }} + git config --global user.email ${{ secrets.GIT_USER_EMAIL }} + git add version.txt + git commit -m "Update version to ${VERSION}" + git push origin develop From ea9d11047a4e5bcb80839a24b980103fa9b166e8 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 13:08:47 +0900 Subject: [PATCH 005/229] fix : workflow yml commit origin to develop --- .github/workflows/deploy_to_ecs.yml | 2 +- .github/workflows/deploy_to_ecs_test.yml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index 37f4b6c..886ead0 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -68,4 +68,4 @@ jobs: git config --global user.email ${{ secrets.GIT_USER_EMAIL }} git add version.txt git commit -m "Update version to ${VERSION}" - git push origin main + git push origin develop diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index 06aab66..b65aaeb 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -61,3 +61,11 @@ jobs: run: | VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 echo "${VERSION}" > version.txt + + - name: Commit version update + run: | + git config --global user.name ${{ secrets.GIT_USER_NAME }} + git config --global user.email ${{ secrets.GIT_USER_EMAIL }} + git add version.txt + git commit -m "Update version to ${VERSION}" + git push origin develop From 046300cdac598784ae1eaf84b566a8cd9b925c27 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 13:30:42 +0900 Subject: [PATCH 006/229] fix : workflow yml add token --- .github/workflows/deploy_to_ecs.yml | 2 +- .github/workflows/deploy_to_ecs_prod.yml | 2 +- .github/workflows/deploy_to_ecs_test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index 886ead0..1b3467b 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -68,4 +68,4 @@ jobs: git config --global user.email ${{ secrets.GIT_USER_EMAIL }} git add version.txt git commit -m "Update version to ${VERSION}" - git push origin develop + git push https://x-access-token:${{ secrets.ACTIONS_PAT }}@github.com/SWM-OneStep/backend.git develop diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index cdde2f2..d3e1aeb 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -69,4 +69,4 @@ jobs: git config --global user.email ${{ secrets.GIT_USER_EMAIL }} git add version.txt git commit -m "Update version to ${VERSION}" - git push origin develop + git push https://x-access-token:${{ secrets.ACTIONS_PAT }}@github.com/SWM-OneStep/backend.git develop diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index b65aaeb..df8d469 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -68,4 +68,4 @@ jobs: git config --global user.email ${{ secrets.GIT_USER_EMAIL }} git add version.txt git commit -m "Update version to ${VERSION}" - git push origin develop + git push https://x-access-token:${{ secrets.ACTIONS_PAT }}@github.com/SWM-OneStep/backend.git develop From 944e03ac17afcb415fe05d8d2ea6af04bec2ca6d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 13:51:32 +0900 Subject: [PATCH 007/229] fix : add action pat --- .github/workflows/deploy_to_ecs.yml | 4 +++- .github/workflows/deploy_to_ecs_prod.yml | 4 +++- .github/workflows/deploy_to_ecs_test.yml | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index 1b3467b..e946f56 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + token: ${{ secrets.ACTIONS_PAT }} - name: Set Dockerfile Path id: dockerfile-path @@ -68,4 +70,4 @@ jobs: git config --global user.email ${{ secrets.GIT_USER_EMAIL }} git add version.txt git commit -m "Update version to ${VERSION}" - git push https://x-access-token:${{ secrets.ACTIONS_PAT }}@github.com/SWM-OneStep/backend.git develop + git push origin develop diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index d3e1aeb..00ffcd2 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + token: ${{ secrets.ACTIONS_PAT }} - name: Set Dockerfile Path id: dockerfile-path @@ -69,4 +71,4 @@ jobs: git config --global user.email ${{ secrets.GIT_USER_EMAIL }} git add version.txt git commit -m "Update version to ${VERSION}" - git push https://x-access-token:${{ secrets.ACTIONS_PAT }}@github.com/SWM-OneStep/backend.git develop + git push origin develop diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index df8d469..f15f0fe 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + token: ${{ secrets.ACTIONS_PAT }} - name: Set Dockerfile Path id: dockerfile-path @@ -68,4 +70,4 @@ jobs: git config --global user.email ${{ secrets.GIT_USER_EMAIL }} git add version.txt git commit -m "Update version to ${VERSION}" - git push https://x-access-token:${{ secrets.ACTIONS_PAT }}@github.com/SWM-OneStep/backend.git develop + git push origin develop From b6bba086b42d74a9e76ffb3db2b1cc676c218f78 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 04:53:45 +0000 Subject: [PATCH 008/229] Update version to --- version.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 version.txt diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..f4af126 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +2024.09.09.04.53.45 From 0f9358f451af8453714ef2995829b598d8a37900 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 04:55:23 +0000 Subject: [PATCH 009/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index f4af126..71fa15b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.04.53.45 +2024.09.09.04.55.23 From 3060a074d408fdc383a512d5e4f14bbae0ea2b09 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 04:57:01 +0000 Subject: [PATCH 010/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 71fa15b..4668c67 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.04.55.23 +2024.09.09.04.57.01 From a560d9e40bf3e8dec5e78dfa51f761c99fe2c53d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 13:57:29 +0900 Subject: [PATCH 011/229] fix : add git pull --- .github/workflows/deploy_to_ecs.yml | 1 + .github/workflows/deploy_to_ecs_prod.yml | 1 + .github/workflows/deploy_to_ecs_test.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index e946f56..f31ecce 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -68,6 +68,7 @@ jobs: run: | git config --global user.name ${{ secrets.GIT_USER_NAME }} git config --global user.email ${{ secrets.GIT_USER_EMAIL }} + git pull origin develop git add version.txt git commit -m "Update version to ${VERSION}" git push origin develop diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 00ffcd2..30e011d 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -69,6 +69,7 @@ jobs: run: | git config --global user.name ${{ secrets.GIT_USER_NAME }} git config --global user.email ${{ secrets.GIT_USER_EMAIL }} + git pull origin develop git add version.txt git commit -m "Update version to ${VERSION}" git push origin develop diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index f15f0fe..31bc0de 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -68,6 +68,7 @@ jobs: run: | git config --global user.name ${{ secrets.GIT_USER_NAME }} git config --global user.email ${{ secrets.GIT_USER_EMAIL }} + git pull origin develop git add version.txt git commit -m "Update version to ${VERSION}" git push origin develop From 7acaab92cbf1a9cdcd7dcfe6593df8efd3e6c8cb Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:00:00 +0000 Subject: [PATCH 012/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 4668c67..04809e4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.04.57.01 +2024.09.09.05.00.00 From c7e391bc144a382a4612a4ef059366488b50035d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:01:35 +0000 Subject: [PATCH 013/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 04809e4..091ce5c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.00.00 +2024.09.09.05.01.35 From 1050ee5bcab21ee3c7b23e11ca18de53c416c918 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:03:09 +0000 Subject: [PATCH 014/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 091ce5c..3461dc2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.01.35 +2024.09.09.05.03.09 From 6e78f1d8c017b5981831d82b03cc0e4bad95c260 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:04:48 +0000 Subject: [PATCH 015/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 3461dc2..5aa487a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.03.09 +2024.09.09.05.04.48 From 6923adc9688b130f822bcd7572d29e778e4f8416 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:06:23 +0000 Subject: [PATCH 016/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 5aa487a..e80e989 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.04.48 +2024.09.09.05.06.23 From 57ccd6bb68fe93addbc185c609f0a3e18941c12a Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 14:06:24 +0900 Subject: [PATCH 017/229] fix : fix branch prod and delete version logic in test --- .github/workflows/deploy_to_ecs_test.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index 31bc0de..6f3dc21 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -12,8 +12,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - with: - token: ${{ secrets.ACTIONS_PAT }} - name: Set Dockerfile Path id: dockerfile-path @@ -58,17 +56,3 @@ jobs: service: ${{ secrets.ECS_SERVICE_NAME }} cluster: ${{ secrets.ECS_CLUSTER_NAME }} wait-for-service-stability: false - - - name: Update version.txt - run: | - VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 - echo "${VERSION}" > version.txt - - - name: Commit version update - run: | - git config --global user.name ${{ secrets.GIT_USER_NAME }} - git config --global user.email ${{ secrets.GIT_USER_EMAIL }} - git pull origin develop - git add version.txt - git commit -m "Update version to ${VERSION}" - git push origin develop From 7ecd675626fa1b73b11c32a2afb78081ae2d2ba1 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 14:07:14 +0900 Subject: [PATCH 018/229] fix : fix branch prod and delete version logic in test --- .github/workflows/deploy_to_ecs_prod.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 30e011d..415a1a6 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -69,7 +69,7 @@ jobs: run: | git config --global user.name ${{ secrets.GIT_USER_NAME }} git config --global user.email ${{ secrets.GIT_USER_EMAIL }} - git pull origin develop + git pull origin main git add version.txt git commit -m "Update version to ${VERSION}" - git push origin develop + git push origin main From 95d695af3d9fb349438494a75bda249553bbac4e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:07:59 +0000 Subject: [PATCH 019/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index e80e989..bce85f9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.06.23 +2024.09.09.05.07.59 From 9a7932b00c5070d40666b0d0813a9c867e243e26 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:09:34 +0000 Subject: [PATCH 020/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index bce85f9..5b2dbb1 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.07.59 +2024.09.09.05.09.34 From 99b54c307e1e56ff545b9d03870d2b3871d993eb Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:11:37 +0000 Subject: [PATCH 021/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 5b2dbb1..56d41e8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.09.34 +2024.09.09.05.11.36 From ccfb75ab845517b1f3ea68ccfa420ed767df5d70 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:13:14 +0000 Subject: [PATCH 022/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 56d41e8..9f30a13 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.11.36 +2024.09.09.05.13.14 From 9074f5d32e2d37d01f4c3acb136965139d47b137 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:14:59 +0000 Subject: [PATCH 023/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 9f30a13..481f41f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.13.14 +2024.09.09.05.14.58 From 565195d5590cf8a89c61331c744db0d306453a22 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 14:15:53 +0900 Subject: [PATCH 024/229] fix : delete commit logic --- .github/workflows/deploy_to_ecs.yml | 11 ----------- .github/workflows/deploy_to_ecs_prod.yml | 11 ----------- 2 files changed, 22 deletions(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index f31ecce..3b39291 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -12,8 +12,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - with: - token: ${{ secrets.ACTIONS_PAT }} - name: Set Dockerfile Path id: dockerfile-path @@ -63,12 +61,3 @@ jobs: run: | VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 echo "${VERSION}" > version.txt - - - name: Commit version update - run: | - git config --global user.name ${{ secrets.GIT_USER_NAME }} - git config --global user.email ${{ secrets.GIT_USER_EMAIL }} - git pull origin develop - git add version.txt - git commit -m "Update version to ${VERSION}" - git push origin develop diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 415a1a6..cfdb6ae 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -12,8 +12,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - with: - token: ${{ secrets.ACTIONS_PAT }} - name: Set Dockerfile Path id: dockerfile-path @@ -64,12 +62,3 @@ jobs: run: | VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 echo "${VERSION}" > version.txt - - - name: Commit version update - run: | - git config --global user.name ${{ secrets.GIT_USER_NAME }} - git config --global user.email ${{ secrets.GIT_USER_EMAIL }} - git pull origin main - git add version.txt - git commit -m "Update version to ${VERSION}" - git push origin main From 86b1261c15971166e38726a8335c5811012a931d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:16:38 +0000 Subject: [PATCH 025/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 481f41f..cb1abee 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.14.58 +2024.09.09.05.16.38 From ec40456a72809318558625bf8fd3717cf4a84464 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:18:16 +0000 Subject: [PATCH 026/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index cb1abee..5a23c0e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.16.38 +2024.09.09.05.18.16 From 005f5fafbe617c5e10b1c48db3a172be1ef31039 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:19:56 +0000 Subject: [PATCH 027/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 5a23c0e..826ade9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.18.16 +2024.09.09.05.19.56 From ffd6eaed7b1b42ae40498f8c279a28c3e17591f8 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:21:33 +0000 Subject: [PATCH 028/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 826ade9..84226b6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.19.56 +2024.09.09.05.21.33 From 93061b90dc07c8848b9a1375bf633e6d3bb6cb21 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:23:12 +0000 Subject: [PATCH 029/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 84226b6..bf0af28 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.21.33 +2024.09.09.05.23.11 From 508afe2d8f97c006ba25dc3779e84f634820e781 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:24:44 +0000 Subject: [PATCH 030/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index bf0af28..281e0ce 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.23.11 +2024.09.09.05.24.44 From d56e14ed190f1ac320b19d93732c228cd8453a60 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:26:22 +0000 Subject: [PATCH 031/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 281e0ce..c966c3a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.24.44 +2024.09.09.05.26.22 From 48fa936f1c9041ad755748ea97d0a20d1fe5cdda Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:27:59 +0000 Subject: [PATCH 032/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index c966c3a..16e6b8a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.26.22 +2024.09.09.05.27.59 From d25cffedebe88160acf52a4d76e28c7dc58c7f34 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:29:39 +0000 Subject: [PATCH 033/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 16e6b8a..fe9954f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.27.59 +2024.09.09.05.29.39 From 1de20cf85c7a9ca8e6ea122b248433ca07936f5d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:31:26 +0000 Subject: [PATCH 034/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index fe9954f..10e12cd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.29.39 +2024.09.09.05.31.25 From 2d55600bd0c63621b0489faebf4f96057bdc561f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:33:02 +0000 Subject: [PATCH 035/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 10e12cd..ac589ff 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.31.25 +2024.09.09.05.33.02 From 0a1aa38c0b3096387295b31d0521e90baef85dfa Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:34:37 +0000 Subject: [PATCH 036/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index ac589ff..c19c89d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.33.02 +2024.09.09.05.34.37 From 4642f4caef44833ae1cf5187af3155cb26d7c05a Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:36:13 +0000 Subject: [PATCH 037/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index c19c89d..1654dac 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.34.37 +2024.09.09.05.36.13 From b23811e10620611a2b44d607663ca00a3fc9c622 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:37:52 +0000 Subject: [PATCH 038/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 1654dac..01577a4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.36.13 +2024.09.09.05.37.52 From d4e8e22536dfcc047ba2b49ef3853bb31f51e875 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:39:29 +0000 Subject: [PATCH 039/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 01577a4..d6aea75 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.37.52 +2024.09.09.05.39.29 From 95b9411098339b86737a0c58ae8d107e1d521dfa Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:41:03 +0000 Subject: [PATCH 040/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index d6aea75..c3ec844 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.39.29 +2024.09.09.05.41.03 From 5ef15cf6bf6d652340d70dfa4ad9acc315372de8 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:42:43 +0000 Subject: [PATCH 041/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index c3ec844..8d79972 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.41.03 +2024.09.09.05.42.43 From 6078f90c0b95ad2d4ab978fdfdc736d8fdac301e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:44:16 +0000 Subject: [PATCH 042/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 8d79972..ec998b0 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.42.43 +2024.09.09.05.44.16 From fdcf815780d40debb59631f42531c2a23ee12525 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:46:04 +0000 Subject: [PATCH 043/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index ec998b0..e56646a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.44.16 +2024.09.09.05.46.03 From 10f4f80fa2e0b838981559a55e6c4a6651db3287 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:47:57 +0000 Subject: [PATCH 044/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index e56646a..b30dbb3 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.46.03 +2024.09.09.05.47.57 From 5511aed1086f343fe2912bd078c1f34a88804e9e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:49:34 +0000 Subject: [PATCH 045/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index b30dbb3..3ede311 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.47.57 +2024.09.09.05.49.34 From 3d68371ed9a85a46a376f6eb9438249b6f222c09 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:51:13 +0000 Subject: [PATCH 046/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 3ede311..52dabd3 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.49.34 +2024.09.09.05.51.13 From 4e04089865814dcbaabe784a19ff5b355db78115 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:52:49 +0000 Subject: [PATCH 047/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 52dabd3..30a5860 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.51.13 +2024.09.09.05.52.48 From fa05d1b1ed8ce47b28e95ee1a0f02ccc737c38e8 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:54:26 +0000 Subject: [PATCH 048/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 30a5860..53b0f2c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.52.48 +2024.09.09.05.54.25 From 091d890c297237703e9caee5a470dce0abf5e242 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:56:07 +0000 Subject: [PATCH 049/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 53b0f2c..152ddba 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.54.25 +2024.09.09.05.56.06 From 0d9ea5bb047a4baadf2ad17223f015dd64394a4b Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:57:46 +0000 Subject: [PATCH 050/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 152ddba..733d18e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.56.06 +2024.09.09.05.57.45 From 4ed75b6de82f13e3b6f30709fd57bf3a0e82f194 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 05:59:24 +0000 Subject: [PATCH 051/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 733d18e..015dd57 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.57.45 +2024.09.09.05.59.24 From 3dc9fb44c99d431750b014aecfc9473f95cba84e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:01:03 +0000 Subject: [PATCH 052/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 015dd57..b6e98be 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.05.59.24 +2024.09.09.06.01.03 From 97960d70588d752b75a32e238d0077549b3bdb1e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:02:44 +0000 Subject: [PATCH 053/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index b6e98be..4d8bade 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.01.03 +2024.09.09.06.02.44 From c14598943b5a76b70b7ab7d4a7a930953ea6fac1 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:04:22 +0000 Subject: [PATCH 054/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 4d8bade..b9280d5 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.02.44 +2024.09.09.06.04.22 From 3c03db1142816a9acc3169bdfe7dcf909439ff94 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:06:10 +0000 Subject: [PATCH 055/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index b9280d5..b221686 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.04.22 +2024.09.09.06.06.10 From c17aca877b5459107c799b15a4829439f18267a4 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:07:51 +0000 Subject: [PATCH 056/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index b221686..d77a521 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.06.10 +2024.09.09.06.07.51 From 12c9cc5bc6fc0135d01b88d29eb6de58d69796b3 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:09:35 +0000 Subject: [PATCH 057/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index d77a521..40afc54 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.07.51 +2024.09.09.06.09.35 From ac5559c59d584fe125eef109636d69d84fed9ad0 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:11:13 +0000 Subject: [PATCH 058/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 40afc54..b4c1121 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.09.35 +2024.09.09.06.11.13 From f2364e8095baed52c6b386f8a58aad26bb1a8c9d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:13:10 +0000 Subject: [PATCH 059/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index b4c1121..791c497 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.11.13 +2024.09.09.06.13.09 From 0a5d75dd255c34f6bf6d6efff844651a2485ef03 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:14:48 +0000 Subject: [PATCH 060/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 791c497..9212428 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.13.09 +2024.09.09.06.14.48 From 265546edd2419a1928f9d74a661d068305cc853d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:16:20 +0000 Subject: [PATCH 061/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 9212428..cffc01e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.14.48 +2024.09.09.06.16.20 From fc71c03a003ce04a3bc9b910a242d8a2d782052e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:17:58 +0000 Subject: [PATCH 062/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index cffc01e..48e972e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.16.20 +2024.09.09.06.17.58 From dcd552b78a7a1b0b456e2e4938325bd6cebdc121 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:19:40 +0000 Subject: [PATCH 063/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 48e972e..cb54ee1 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.17.58 +2024.09.09.06.19.40 From ab966058846c3f5be5d3131b166e6bf6b06427b2 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:21:18 +0000 Subject: [PATCH 064/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index cb54ee1..94710dd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.19.40 +2024.09.09.06.21.13 From 97f4c68177070ef7eb505a4c2af482d60df938fa Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:22:55 +0000 Subject: [PATCH 065/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 94710dd..b8018e3 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.21.13 +2024.09.09.06.22.54 From c0e0c4bd923ed2cf8b418390e5117dec4bc2d2b0 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:24:32 +0000 Subject: [PATCH 066/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index b8018e3..6add140 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.22.54 +2024.09.09.06.24.32 From 3558ce7bee44ee1ba7b72f9e93e3be58c3a394cb Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:26:13 +0000 Subject: [PATCH 067/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 6add140..89c959b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.24.32 +2024.09.09.06.26.13 From 904223f4646cea1e81def6cc195d10e33963c95d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:27:51 +0000 Subject: [PATCH 068/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 89c959b..52b38e8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.26.13 +2024.09.09.06.27.51 From 7ba6aff51071b3d0936c4de98f440df897cdc2ea Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:29:28 +0000 Subject: [PATCH 069/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 52b38e8..c956f14 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.27.51 +2024.09.09.06.29.28 From c95d904b0357b501ba5f45103f83c95d7a2c1873 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:31:07 +0000 Subject: [PATCH 070/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index c956f14..c487186 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.29.28 +2024.09.09.06.31.07 From db9843fc938d857439a3f5db379a7a95abd6cef4 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:32:39 +0000 Subject: [PATCH 071/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index c487186..8dfc045 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.31.07 +2024.09.09.06.32.39 From 463e9133d65f3418102f2c17ff39a8c46f71a9bc Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:34:17 +0000 Subject: [PATCH 072/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 8dfc045..9808bc7 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.32.39 +2024.09.09.06.34.17 From 06148b4d178f6ccf2ddfdf994c79d0f9404ab1d7 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:35:58 +0000 Subject: [PATCH 073/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 9808bc7..3595c69 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.34.17 +2024.09.09.06.35.57 From e2d39966986f69e38037baa55ee0b1aa5c393dab Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:37:37 +0000 Subject: [PATCH 074/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 3595c69..c9bd961 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.35.57 +2024.09.09.06.37.37 From 6061e313736b92e154849ed3cfcc86a14baab287 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:39:14 +0000 Subject: [PATCH 075/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index c9bd961..bd3cafc 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.37.37 +2024.09.09.06.39.14 From 9a0c9b12ca678e763729a84b7f08fba72735b46d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:40:56 +0000 Subject: [PATCH 076/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index bd3cafc..ca8b3a8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.39.14 +2024.09.09.06.40.55 From 9ea29651b8dc21a2fd25858ee41a12bffaaea47a Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:42:32 +0000 Subject: [PATCH 077/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index ca8b3a8..152e7d2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.40.55 +2024.09.09.06.42.32 From 4a035c470f82007e1ba286f9b9bf5fbcee03af90 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:44:09 +0000 Subject: [PATCH 078/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 152e7d2..ac35ac6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.42.32 +2024.09.09.06.44.09 From c8e113795c6f481d0164bc34eb5abcbb5734c8e6 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:45:47 +0000 Subject: [PATCH 079/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index ac35ac6..3eeef37 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.44.09 +2024.09.09.06.45.47 From e8e2a92a959084625b64083a3689d91d5c6fff1f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:47:22 +0000 Subject: [PATCH 080/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 3eeef37..80eb9c8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.45.47 +2024.09.09.06.47.22 From d73734853dd59743dc371c639bd614e3ab5a7d2f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:49:14 +0000 Subject: [PATCH 081/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 80eb9c8..714ff41 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.47.22 +2024.09.09.06.49.13 From 32ae9830624551ba27c0e7683304d2afcd6d77f4 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:50:49 +0000 Subject: [PATCH 082/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 714ff41..13664fb 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.49.13 +2024.09.09.06.50.49 From 5cd04be0c1cac4262b4cbf04951d648b7a50f65f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:52:26 +0000 Subject: [PATCH 083/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 13664fb..e5cb40b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.50.49 +2024.09.09.06.52.26 From 314f2f3f187abb4234903defc35ec89a3cf88c75 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 06:54:01 +0000 Subject: [PATCH 084/229] Update version to --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index e5cb40b..d92c44e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2024.09.09.06.52.26 +2024.09.09.06.54.01 From a112f3d903ac291436314c2c87021c95cebded17 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 17:01:30 +0900 Subject: [PATCH 085/229] feat: sentry release --- .github/workflows/deploy_to_ecs.yml | 2 +- .github/workflows/deploy_to_ecs_prod.yml | 2 +- onestep_be/settings.py | 15 +++++++++++++++ version.txt | 0 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 version.txt diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index 3b39291..e6d1b4a 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -60,4 +60,4 @@ jobs: - name: Update version.txt run: | VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 - echo "${VERSION}" > version.txt + echo "onestep_dev@${VERSION}" > version.txt diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index cfdb6ae..0c710d7 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -61,4 +61,4 @@ jobs: - name: Update version.txt run: | VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 - echo "${VERSION}" > version.txt + echo "onestep_prod@${VERSION}" > version.txt diff --git a/onestep_be/settings.py b/onestep_be/settings.py index a5df2f9..d2060c2 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ +import os from datetime import timedelta from pathlib import Path @@ -221,6 +222,19 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# BASE_DIR은 Django 프로젝트의 루트 디렉토리를 가리킵니다. +# version.txt 파일이 BASE_DIR에 있다고 가정합니다. +VERSION_FILE_PATH = os.path.join(BASE_DIR, "version.txt") + +# 파일 읽기 +try: + with open(VERSION_FILE_PATH, "r") as file: + # 파일의 내용을 읽어서 변수에 저장 + PROJECT_VERSION = file.read().strip() +except FileNotFoundError: + PROJECT_VERSION = "Unknown" # 파일이 없을 경우 기본값 + # sentry settings sentry_sdk.init( @@ -228,6 +242,7 @@ # Set traces_sample_rate to 1.0 to capture 100% # of transactions for performance monitoring. traces_sample_rate=0.5, + release=PROJECT_VERSION, # Set profiles_sample_rate to 1.0 to profile 100% # of sampled transactions. # We recommend adjusting this value in production. diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..e69de29 From cb0e6310b4a6c7a33eb5ab34f18965e5d62ca835 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 17:09:28 +0900 Subject: [PATCH 086/229] =?UTF-8?q?fix=20:=20version=20=ED=95=9C=EA=B5=AD?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=EB=8C=80=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy_to_ecs.yml | 2 +- .github/workflows/deploy_to_ecs_prod.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index e6d1b4a..10920d3 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -59,5 +59,5 @@ jobs: - name: Update version.txt run: | - VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 + VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 echo "onestep_dev@${VERSION}" > version.txt diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 0c710d7..3fb1bb9 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -58,7 +58,7 @@ jobs: cluster: ${{ secrets.ECS_CLUSTER_NAME_PROD }} wait-for-service-stability: false - - name: Update version.txt + - name: Update version.txt with KST run: | - VERSION=$(date +'%Y.%m.%d.%H.%M.%S') # Y.M.D.H.M.S 형식으로 버전 생성 + VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 echo "onestep_prod@${VERSION}" > version.txt From 42f48492bfe41ad3fbfd21f3427b48dd86d4791f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 17:24:51 +0900 Subject: [PATCH 087/229] fix: settings.py --- onestep_be/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index d2060c2..be2dd18 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -232,6 +232,8 @@ with open(VERSION_FILE_PATH, "r") as file: # 파일의 내용을 읽어서 변수에 저장 PROJECT_VERSION = file.read().strip() + if PROJECT_VERSION == "": + PROJECT_VERSION = "Unknown" except FileNotFoundError: PROJECT_VERSION = "Unknown" # 파일이 없을 경우 기본값 From 89e365e26b05172b396ea3b5893bdb74905cfb21 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 17:35:48 +0900 Subject: [PATCH 088/229] fix: add update version logic in test yml --- .github/workflows/deploy_to_ecs_prod.yml | 2 +- .github/workflows/deploy_to_ecs_test.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 3fb1bb9..3614a02 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -58,7 +58,7 @@ jobs: cluster: ${{ secrets.ECS_CLUSTER_NAME_PROD }} wait-for-service-stability: false - - name: Update version.txt with KST + - name: Update version.txt run: | VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 echo "onestep_prod@${VERSION}" > version.txt diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index 6f3dc21..673617a 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -56,3 +56,8 @@ jobs: service: ${{ secrets.ECS_SERVICE_NAME }} cluster: ${{ secrets.ECS_CLUSTER_NAME }} wait-for-service-stability: false + + - name: Update version.txt + run: | + VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 + echo "onestep_dev@${VERSION}" > version.txt From 0bc892a02be86c2741cddc8eb6d556e6a2781731 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 18:19:54 +0900 Subject: [PATCH 089/229] fix : fix order in ecs.yml --- .github/workflows/deploy_to_ecs.yml | 10 +++++----- .github/workflows/deploy_to_ecs_prod.yml | 10 +++++----- .github/workflows/deploy_to_ecs_test.yml | 5 ----- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index 10920d3..adeaf08 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -13,6 +13,11 @@ jobs: - name: Checkout code uses: actions/checkout@v2 + - name: Update version.txt + run: | + VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 + echo "onestep_dev@${VERSION}" > version.txt + - name: Set Dockerfile Path id: dockerfile-path run: | @@ -56,8 +61,3 @@ jobs: service: ${{ secrets.ECS_SERVICE_NAME }} cluster: ${{ secrets.ECS_CLUSTER_NAME }} wait-for-service-stability: false - - - name: Update version.txt - run: | - VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 - echo "onestep_dev@${VERSION}" > version.txt diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 3614a02..7d14bd7 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -13,6 +13,11 @@ jobs: - name: Checkout code uses: actions/checkout@v2 + - name: Update version.txt + run: | + VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 + echo "onestep_prod@${VERSION}" > version.txt + - name: Set Dockerfile Path id: dockerfile-path run: | @@ -57,8 +62,3 @@ jobs: service: ${{ secrets.ECS_SERVICE_NAME_PROD }} cluster: ${{ secrets.ECS_CLUSTER_NAME_PROD }} wait-for-service-stability: false - - - name: Update version.txt - run: | - VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 - echo "onestep_prod@${VERSION}" > version.txt diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index 673617a..6f3dc21 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -56,8 +56,3 @@ jobs: service: ${{ secrets.ECS_SERVICE_NAME }} cluster: ${{ secrets.ECS_CLUSTER_NAME }} wait-for-service-stability: false - - - name: Update version.txt - run: | - VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 - echo "onestep_dev@${VERSION}" > version.txt From 73607307f64a51af7ad549f14f078b4aef03bc82 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 22:44:37 +0900 Subject: [PATCH 090/229] feat: add admin --- accounts/admin.py | 45 ++++++++++++++++++++++++++++ todos/admin.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/accounts/admin.py b/accounts/admin.py index 846f6b4..ad8d97e 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1 +1,46 @@ # Register your models here. +from django.contrib import admin + +from .models import Device, User + + +class UserAdmin(admin.ModelAdmin): + readonly_fields = ["id"] + list_display = [ + "id", + "email", + "username", + "is_active", + "is_staff", + "is_superuser", + ] + fieldsets = [ + ( + None, + { + "fields": [ + "id", + "email", + "username", + "is_active", + "is_staff", + "is_superuser", + ] + }, + ), + ] + + +class DeviceAdmin(admin.ModelAdmin): + readonly_fields = ["id", "created_at"] + list_display = ["id", "user_id", "created_at", "deleted_at"] + fieldsets = [ + ( + None, + {"fields": ["id", "user_id", "created_at", "deleted_at"]}, + ), + ] + + +admin.site.register(User, UserAdmin) +admin.site.register(Device, DeviceAdmin) diff --git a/todos/admin.py b/todos/admin.py index 8b13789..2ec3fc2 100644 --- a/todos/admin.py +++ b/todos/admin.py @@ -1 +1,75 @@ +# Register your models here. +from django.contrib import admin +from .models import Category, SubTodo, Todo + + +class TodoAdmin(admin.ModelAdmin): + readonly_fields = ["id"] + list_display = [ + "id", + "content", + "user_id", + "category_id", + "order", + "start_date", + "is_completed", + ] + fieldsets = [ + ( + None, + { + "fields": [ + "id", + "content", + "user_id", + "category_id", + "order", + "is_completed", + ] + }, + ), + ( + "Date information", + {"fields": ["start_date", "end_date"], "classes": ["collapse"]}, + ), + ] + + +class SubTodoAdmin(admin.ModelAdmin): + readonly_fields = ["id"] + list_display = ["id", "content", "todo", "order", "is_completed"] + fieldsets = [ + ( + None, + { + "fields": [ + "id", + "content", + "todo", + "order", + "is_completed", + ] + }, + ), + ( + "Date information", + {"fields": ["date"], "classes": ["collapse"]}, + ), + ] + + +class CategoryAdmin(admin.ModelAdmin): + readonly_fields = ["id"] + list_display = ["id", "title", "user_id", "color", "order"] + fieldsets = [ + ( + None, + {"fields": ["id", "title", "user_id", "color", "order"]}, + ), + ] + + +admin.site.register(Todo, TodoAdmin) +admin.site.register(SubTodo, SubTodoAdmin) +admin.site.register(Category, CategoryAdmin) From 3465884dc0512c6be66b32cf61359763e75a11de Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 9 Sep 2024 22:47:30 +0900 Subject: [PATCH 091/229] fix: useradmin --- accounts/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/accounts/admin.py b/accounts/admin.py index ad8d97e..2a45b54 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -5,11 +5,11 @@ class UserAdmin(admin.ModelAdmin): - readonly_fields = ["id"] + readonly_fields = ["id", "social_provider", "username"] list_display = [ "id", - "email", "username", + "social_provider", "is_active", "is_staff", "is_superuser", @@ -20,8 +20,8 @@ class UserAdmin(admin.ModelAdmin): { "fields": [ "id", - "email", "username", + "social_provider", "is_active", "is_staff", "is_superuser", From 4eee18eafcc159ac6df624457e3c313125546d77 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 10 Sep 2024 15:44:10 +0900 Subject: [PATCH 092/229] feat : create customer Support API --- conftest.py | 17 ++++++++ feedback/__init__.py | 0 feedback/admin.py | 26 ++++++++++++ feedback/apps.py | 6 +++ feedback/migrations/0001_initial.py | 63 +++++++++++++++++++++++++++++ feedback/migrations/__init__.py | 0 feedback/models.py | 26 ++++++++++++ feedback/serializers.py | 19 +++++++++ feedback/tests.py | 59 +++++++++++++++++++++++++++ feedback/urls.py | 10 +++++ feedback/views.py | 38 +++++++++++++++++ onestep_be/settings.py | 1 + onestep_be/urls.py | 15 ++++--- pyproject.toml | 3 +- todos/admin.py | 1 - 15 files changed, 277 insertions(+), 7 deletions(-) create mode 100644 feedback/__init__.py create mode 100644 feedback/admin.py create mode 100644 feedback/apps.py create mode 100644 feedback/migrations/0001_initial.py create mode 100644 feedback/migrations/__init__.py create mode 100644 feedback/models.py create mode 100644 feedback/serializers.py create mode 100644 feedback/tests.py create mode 100644 feedback/urls.py create mode 100644 feedback/views.py diff --git a/conftest.py b/conftest.py index 4121306..def88ff 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,8 @@ # This file is for Pytest Configuration # This file is used to define fixtures that can be used in multiple test files +import random + import pytest from faker import Faker from rest_framework.test import APIClient @@ -95,3 +97,18 @@ def get_order(index): @pytest.fixture def color(): return fake.color() + + +@pytest.fixture +def title(): + return fake.sentence(nb_words=3) + + +@pytest.fixture +def category(): + CATEGORY_CHOICES = [ + ("bug", "버그"), + ("feature", "기능 요청"), + ("feedback", "일반 피드백"), + ] + return random.choice(CATEGORY_CHOICES)[0] diff --git a/feedback/__init__.py b/feedback/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/feedback/admin.py b/feedback/admin.py new file mode 100644 index 0000000..def84a3 --- /dev/null +++ b/feedback/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin + +from .models import Feedback + + +class FeedbackAdmin(admin.ModelAdmin): + readonly_fields = ["id", "user_id", "created_at"] + fieldsets = [ + ( + None, + { + "fields": [ + "id", + "user_id", + "title", + "category", + "description", + "created_at", + "is_completed", + ] + }, + ), + ] + + +admin.site.register(Feedback, FeedbackAdmin) diff --git a/feedback/apps.py b/feedback/apps.py new file mode 100644 index 0000000..d26ac3c --- /dev/null +++ b/feedback/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FeedbackConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "feedback" diff --git a/feedback/migrations/0001_initial.py b/feedback/migrations/0001_initial.py new file mode 100644 index 0000000..0550b50 --- /dev/null +++ b/feedback/migrations/0001_initial.py @@ -0,0 +1,63 @@ +# Generated by Django 5.0.6 on 2024-09-10 06:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Feedback", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=200)), + ( + "category", + models.CharField( + choices=[ + ("bug", "버그"), + ("feature", "기능 요청"), + ("feedback", "일반 피드백"), + ], + max_length=20, + ), + ), + ("description", models.TextField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "status", + models.CharField( + choices=[ + ("pending", "대기 중"), + ("processing", "처리 중"), + ("completed", "완료"), + ], + default="pending", + max_length=20, + ), + ), + ( + "user_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/feedback/migrations/__init__.py b/feedback/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/feedback/models.py b/feedback/models.py new file mode 100644 index 0000000..3b34fdf --- /dev/null +++ b/feedback/models.py @@ -0,0 +1,26 @@ +from django.db import models + +from accounts.models import User + + +# Create your models here. +class Feedback(models.Model): + CATEGORY_CHOICES = [ + ("bug", "버그"), + ("feature", "기능 요청"), + ("feedback", "일반 피드백"), + ] + STATUS_CHOICES = [ + ("pending", "대기 중"), + ("processing", "처리 중"), + ("completed", "완료"), + ] + + user_id = models.ForeignKey(User, on_delete=models.CASCADE) + title = models.CharField(max_length=200) + category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) + description = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + status = models.CharField( + max_length=20, choices=STATUS_CHOICES, default="pending" + ) diff --git a/feedback/serializers.py b/feedback/serializers.py new file mode 100644 index 0000000..e2cd8a7 --- /dev/null +++ b/feedback/serializers.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + +from accounts.models import User +from feedback.models import Feedback + + +class FeedbackSerializer(serializers.ModelSerializer): + user_id = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), required=True + ) + title = serializers.CharField(max_length=200, required=True) + category = serializers.ChoiceField( + choices=Feedback.CATEGORY_CHOICES, required=True + ) + description = serializers.CharField(required=True) + + class Meta: + model = Feedback + fields = "__all__" diff --git a/feedback/tests.py b/feedback/tests.py new file mode 100644 index 0000000..a4e4b48 --- /dev/null +++ b/feedback/tests.py @@ -0,0 +1,59 @@ +import pytest +from django.urls import reverse + +""" +====================================== +# category Get checklist # +- correct test +- invalid user_id +- invalid category +====================================== +""" + + +@pytest.mark.django_db +def test_create_feedback_success( + create_user, authenticated_client, title, category, content +): + url = reverse("feedback") + data = { + "user_id": create_user.id, + "title": title, + "category": category, + "description": content, + } + + response = authenticated_client.post(url, data, format="json") + assert response.status_code == 201 + + +@pytest.mark.django_db +def test_create_feedback_invalid_user_id( + authenticated_client, title, category, content +): + url = reverse("feedback") + data = { + "user_id": 999, + "title": title, + "category": category, + "description": content, + } + + response = authenticated_client.post(url, data, format="json") + assert response.status_code == 400 + + +@pytest.mark.django_db +def test_create_feedback_invalid_category( + create_user, authenticated_client, title, category, content +): + url = reverse("feedback") + data = { + "user_id": create_user.id, + "title": title, + "category": "invalid Category", + "description": content, + } + + response = authenticated_client.post(url, data, format="json") + assert response.status_code == 400 diff --git a/feedback/urls.py b/feedback/urls.py new file mode 100644 index 0000000..7e1fd11 --- /dev/null +++ b/feedback/urls.py @@ -0,0 +1,10 @@ +# todos/urls.py +from django.urls import path + +from feedback.views import ( + FeedbackView, +) + +urlpatterns = [ + path("", FeedbackView.as_view(), name="feedback"), +] diff --git a/feedback/views.py b/feedback/views.py new file mode 100644 index 0000000..a34f8b0 --- /dev/null +++ b/feedback/views.py @@ -0,0 +1,38 @@ +# Create your views here.# todos/views.py + +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from feedback.serializers import ( + FeedbackSerializer, +) + + +class FeedbackView(APIView): + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + tags=["Feedback"], + request_body=FeedbackSerializer, + operation_summary="Create a Feedback", + responses={201: FeedbackSerializer}, + ) + def post(self, request): + """ + - 이 함수는 Feedback을 생성하는 함수입니다. + - 입력 : user_id, title, category, description + """ + data = request.data + # category_id validation + serializer = FeedbackSerializer( + context={"request": request}, data=data + ) + if serializer.is_valid(raise_exception=True): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response( + {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST + ) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index be2dd18..239587b 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -56,6 +56,7 @@ "rest_framework", "corsheaders", "todos", + "feedback", "rest_framework_simplejwt", "allauth", "allauth.account", diff --git a/onestep_be/urls.py b/onestep_be/urls.py index e24d37b..1cf8c39 100644 --- a/onestep_be/urls.py +++ b/onestep_be/urls.py @@ -1,8 +1,8 @@ from django.contrib import admin -from django.urls import path, include -from rest_framework.permissions import AllowAny -from drf_yasg.views import get_schema_view +from django.urls import include, path from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework.permissions import AllowAny schema_view = get_schema_view( openapi.Info( @@ -26,6 +26,11 @@ schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui", ), - path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), - path('todos/', include('todos.urls')), + path( + "redoc/", + schema_view.with_ui("redoc", cache_timeout=0), + name="schema-redoc", + ), + path("todos/", include("todos.urls")), + path("feedback/", include("feedback.urls")), ] diff --git a/pyproject.toml b/pyproject.toml index b6b53b7..a661462 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,11 @@ requires-python = ">=3.10" [tool.ruff] # Set the maximum line length to 79. line-length = 79 +exclude = ["*/migrations"] [tool.ruff.lint] # Add the `line-too-long` rule to the enforced rule set. extend-select = ["E501"] [tool.ruff.lint.pycodestyle] -ignore-overlong-task-comments = true \ No newline at end of file +ignore-overlong-task-comments = true diff --git a/todos/admin.py b/todos/admin.py index 8b13789..e69de29 100644 --- a/todos/admin.py +++ b/todos/admin.py @@ -1 +0,0 @@ - From 0446dca127f94ae042b66979a88ea299c46fbbc0 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 10 Sep 2024 15:49:33 +0900 Subject: [PATCH 093/229] =?UTF-8?q?fix=20:=20feedback=20admin=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feedback/admin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/feedback/admin.py b/feedback/admin.py index def84a3..5ec822a 100644 --- a/feedback/admin.py +++ b/feedback/admin.py @@ -5,6 +5,14 @@ class FeedbackAdmin(admin.ModelAdmin): readonly_fields = ["id", "user_id", "created_at"] + list_display = [ + "id", + "user_id", + "title", + "category", + "status", + "created_at", + ] fieldsets = [ ( None, @@ -16,7 +24,7 @@ class FeedbackAdmin(admin.ModelAdmin): "category", "description", "created_at", - "is_completed", + "status", ] }, ), From 1aca96fc3d867063edc8e3a61fc73a5368ebda7e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 10 Sep 2024 17:48:20 +0900 Subject: [PATCH 094/229] fix : models Category_list to Categoryprovider --- feedback/models.py | 27 +++++++++++++++------------ feedback/serializers.py | 2 +- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/feedback/models.py b/feedback/models.py index 3b34fdf..a89ac8a 100644 --- a/feedback/models.py +++ b/feedback/models.py @@ -5,22 +5,25 @@ # Create your models here. class Feedback(models.Model): - CATEGORY_CHOICES = [ - ("bug", "버그"), - ("feature", "기능 요청"), - ("feedback", "일반 피드백"), - ] - STATUS_CHOICES = [ - ("pending", "대기 중"), - ("processing", "처리 중"), - ("completed", "완료"), - ] + class CategoryProvider(models.TextChoices): + Bug = "bug", "버그" + Feature = "feature", "기능 요청" + Feedback = "feedback", "일반 피드백" + + class StatusProvider(models.TextChoices): + Pending = "pending", "대기 중" + Processing = "processing", "처리 중" + Completed = "completed", "완료" user_id = models.ForeignKey(User, on_delete=models.CASCADE) title = models.CharField(max_length=200) - category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) + category = models.CharField( + max_length=20, choices=CategoryProvider.choices + ) description = models.TextField() created_at = models.DateTimeField(auto_now_add=True) status = models.CharField( - max_length=20, choices=STATUS_CHOICES, default="pending" + max_length=20, + choices=StatusProvider.choices, + default=StatusProvider.Pending, ) diff --git a/feedback/serializers.py b/feedback/serializers.py index e2cd8a7..8be5d16 100644 --- a/feedback/serializers.py +++ b/feedback/serializers.py @@ -10,7 +10,7 @@ class FeedbackSerializer(serializers.ModelSerializer): ) title = serializers.CharField(max_length=200, required=True) category = serializers.ChoiceField( - choices=Feedback.CATEGORY_CHOICES, required=True + choices=Feedback.CategoryProvider.choices, required=True ) description = serializers.CharField(required=True) From b744340239d1e5d6de918b115fcf3dfcb8403bf6 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Wed, 11 Sep 2024 14:58:46 +0900 Subject: [PATCH 095/229] =?UTF-8?q?chore:=20.gitignore=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 5be46ff..499059d 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ db.sqlite3 # debug .vscode/ + +# custom +dbmodels.py From d2b7dae590d61f1c98af61149551c7e6d67b9b38 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Wed, 11 Sep 2024 15:20:12 +0900 Subject: [PATCH 096/229] =?UTF-8?q?fix:=20staticfiles=5Furlpatterns=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=B4=EC=84=9C=20gunicorn=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=95=EC=A0=81=20=ED=8C=8C=EC=9D=BC=20=EC=84=9C?= =?UTF-8?q?=EB=B9=99=ED=95=98=EB=8A=94=20=EB=B0=A9=EC=8B=9D=20=EB=B0=94?= =?UTF-8?q?=EA=BE=B8=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- onestep_be/urls.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/onestep_be/urls.py b/onestep_be/urls.py index 1cf8c39..767fbb4 100644 --- a/onestep_be/urls.py +++ b/onestep_be/urls.py @@ -3,6 +3,7 @@ from drf_yasg import openapi from drf_yasg.views import get_schema_view from rest_framework.permissions import AllowAny +from django.contrib.staticfiles.urls import staticfiles_urlpatterns schema_view = get_schema_view( openapi.Info( @@ -34,3 +35,5 @@ path("todos/", include("todos.urls")), path("feedback/", include("feedback.urls")), ] + +urlpatterns += staticfiles_urlpatterns() From d43ce9d9eda9b010f5746952fe562a08f9c9d18b Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 11 Sep 2024 15:45:42 +0900 Subject: [PATCH 097/229] feat: add resend api --- accounts/urls.py | 2 ++ accounts/utils.py | 13 +++++++++++++ accounts/views.py | 9 ++++++++- onestep_be/settings.py | 3 +++ requirements.txt | 1 + 5 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 accounts/utils.py diff --git a/accounts/urls.py b/accounts/urls.py index 35de135..cc65674 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -6,6 +6,7 @@ from accounts.views import ( AndroidClientView, + EmailView, GoogleLogin, TestView, UserRetrieveView, @@ -18,4 +19,5 @@ path("test/", TestView.as_view(), name="test"), path("user/", UserRetrieveView.as_view(), name="user"), path("android/", AndroidClientView.as_view(), name="android"), + path("email/", EmailView.as_view(), name="email"), ] diff --git a/accounts/utils.py b/accounts/utils.py new file mode 100644 index 0000000..7dbdf1a --- /dev/null +++ b/accounts/utils.py @@ -0,0 +1,13 @@ +import resend + + +def send_email(send_email, subject, message): + params: resend.Emails.SendParams = { + "from": "Acme ", + "to": ["szonestep@gmail.com"], + "subject": "Work Report", + "html": "it works!", + } + + email = resend.Emails.send(params) + return email diff --git a/accounts/views.py b/accounts/views.py index b3f614e..ca571c7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -3,13 +3,14 @@ from google.auth.transport import requests from google.oauth2 import id_token from rest_framework import status -from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from accounts.models import Device from accounts.serializers import UserSerializer from accounts.tokens import CustomRefreshToken +from accounts.utils import send_email User = get_user_model() @@ -73,3 +74,9 @@ def get(self, request): return Response( {"android_client_id": ANDROID_CLIENT_ID}, status=status.HTTP_200_OK ) + + +class EmailView(APIView): + def post(self, request): + result = send_email("", "", "") + return Response(result, status=status.HTTP_200_OK) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 239587b..2fa7899 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -15,6 +15,7 @@ from pathlib import Path import django.db.models.signals +import resend import sentry_sdk from openai import OpenAI from sentry_sdk.integrations.django import DjangoIntegration @@ -276,3 +277,5 @@ value=94, unit="percent", ) + +resend.api_key = SECRETS.get("RESEND") diff --git a/requirements.txt b/requirements.txt index 317dedb..d7fad68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -88,3 +88,4 @@ Werkzeug==3.0.3 wheel==0.43.0 zope.event==5.0 zope.interface==7.0.1 +resend==2.4.0 From d700ba8e2a0682236729348f80c8c38ed22f75a5 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 11 Sep 2024 18:14:07 +0900 Subject: [PATCH 098/229] feat: welcome mail: exite exit () ; ' --- accounts/welcome.html | 74 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 accounts/welcome.html diff --git a/accounts/welcome.html b/accounts/welcome.html new file mode 100644 index 0000000..9e8577a --- /dev/null +++ b/accounts/welcome.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ user_name }}님!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + From d179936dd010dcb6c1e33afcf73ac3930b7f7aa5 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 11 Sep 2024 18:14:52 +0900 Subject: [PATCH 099/229] feat: welcome mail --- accounts/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/accounts/utils.py b/accounts/utils.py index 7dbdf1a..1c07260 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -1,4 +1,5 @@ import resend +from django.template.loader import render_to_string def send_email(send_email, subject, message): @@ -11,3 +12,10 @@ def send_email(send_email, subject, message): email = resend.Emails.send(params) return email + + +def welcome_email(user_name): + html_message = render_to_string( + "welcome_email.html", {"user_name": user_name} + ) + return html_message From 38836df87b297e6b1c71a4770a4c4c3e2f2086ad Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 21 Sep 2024 00:28:40 +0900 Subject: [PATCH 100/229] feat : develop welcome mail service --- .../welcome_email.html} | 2 +- accounts/tests.py | 53 +++++++++++++++++++ accounts/utils.py | 10 ++-- accounts/views.py | 24 +++++++-- 4 files changed, 78 insertions(+), 11 deletions(-) rename accounts/{welcome.html => templates/welcome_email.html} (96%) diff --git a/accounts/welcome.html b/accounts/templates/welcome_email.html similarity index 96% rename from accounts/welcome.html rename to accounts/templates/welcome_email.html index 9e8577a..dfa627a 100644 --- a/accounts/welcome.html +++ b/accounts/templates/welcome_email.html @@ -48,7 +48,7 @@

환영합니다!

-

안녕하세요, {{ user_name }}님!

+

안녕하세요, {{ username }} 님 환영합니다!

저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. diff --git a/accounts/tests.py b/accounts/tests.py index 269ce49..448ff7c 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -3,12 +3,15 @@ import pytest from django.contrib.auth import get_user_model from django.urls import reverse +from rest_framework import status from rest_framework.test import ( APIClient, APIRequestFactory, force_authenticate, ) +from accounts.models import Device +from accounts.utils import welcome_email from accounts.views import UserRetrieveView User = get_user_model() @@ -35,3 +38,53 @@ def test_google_login(invalid_token): client = APIClient() response = client.post(reverse("google_login"), data=invalid_token) assert response.status_code == 400 + + +@pytest.mark.django_db +class TestGoogleLogin: + @pytest.fixture + def api_client(self): + return APIClient() + + @patch("accounts.views.id_token.verify_oauth2_token") + @patch("accounts.views.send_email") # Ensure the correct patch path + def test_google_login_new_user( + self, mock_send_email, mock_verify_oauth2_token, api_client + ): + # Mock the token verification response + mock_verify_oauth2_token.return_value = { + "iss": "accounts.google.com", + "email": "testuser@example.com", + } + + # Define the URL for the GoogleLogin view + url = reverse( + "google_login" + ) # Replace 'google-login' with your actual URL name + + # Create a mock request + data = {"token": "mock_token", "device_token": "mock_device_token"} + + # Call the view + response = api_client.post(url, data, format="json") + print(response.data) + + # Check that the user was created + user = User.objects.get(username="testuser@example.com") + assert user is not None + + # Check that the device was created + device = Device.objects.get(user_id=user.id, token="mock_device_token") + assert device is not None + + # Check that the email was sent + mock_send_email.assert_called_once_with( + "szonestep@gmail.com", + "Welcome to join us", + welcome_email(user.username), + ) + + # Check the response status + assert response.status_code == status.HTTP_200_OK + assert "refresh" in response.data + assert "access" in response.data diff --git a/accounts/utils.py b/accounts/utils.py index 1c07260..03649e8 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -2,12 +2,12 @@ from django.template.loader import render_to_string -def send_email(send_email, subject, message): +def send_email(to_email_address, subject, message): params: resend.Emails.SendParams = { "from": "Acme ", - "to": ["szonestep@gmail.com"], - "subject": "Work Report", - "html": "it works!", + "to": to_email_address, + "subject": subject, + "html": message, } email = resend.Emails.send(params) @@ -16,6 +16,6 @@ def send_email(send_email, subject, message): def welcome_email(user_name): html_message = render_to_string( - "welcome_email.html", {"user_name": user_name} + "welcome_email.html", {"username": user_name} ) return html_message diff --git a/accounts/views.py b/accounts/views.py index ca571c7..94404b8 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -10,7 +10,7 @@ from accounts.models import Device from accounts.serializers import UserSerializer from accounts.tokens import CustomRefreshToken -from accounts.utils import send_email +from accounts.utils import send_email, welcome_email User = get_user_model() @@ -42,9 +42,7 @@ def post(self, request): ) if "accounts.google.com" in idinfo["iss"]: email = idinfo["email"] - user, _ = User.objects.get_or_create( - username=email, password="" - ) + user = self.get_or_create_user(email) Device.objects.get_or_create(user_id=user, token=device_token) refresh = CustomRefreshToken.for_user(user, device_token) return Response( @@ -56,6 +54,18 @@ def post(self, request): except Exception: return Response(status=status.HTTP_400_BAD_REQUEST) + def get_or_create_user(self, email): + try: + user = User.objects.get(username=email, password="") + except User.DoesNotExist: + user = User.objects.create(username=email, password="") + send_email( + "szonestep@gmail.com", + "Welcome to join us", + welcome_email(user.username), + ) + return user + class UserRetrieveView(APIView): serializer_class = UserSerializer @@ -78,5 +88,9 @@ def get(self, request): class EmailView(APIView): def post(self, request): - result = send_email("", "", "") + result = send_email( + to_email_address="szonestep@gmail.com", + subject="Welcome to join us", + message=welcome_email("user_name_here"), + ) return Response(result, status=status.HTTP_200_OK) From aebcccd6d1710059b8f24c6a87c4049b47191999 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sun, 22 Sep 2024 14:14:01 +0900 Subject: [PATCH 101/229] =?UTF-8?q?fix=20:=20welcome=20email=20=EC=8B=9C?= =?UTF-8?q?=20=EC=88=98=EC=8B=A0=ED=95=98=EB=8A=94=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/views.py b/accounts/views.py index 94404b8..c6e4af5 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -60,7 +60,7 @@ def get_or_create_user(self, email): except User.DoesNotExist: user = User.objects.create(username=email, password="") send_email( - "szonestep@gmail.com", + email, "Welcome to join us", welcome_email(user.username), ) From 59df27bbd08c337f171a9a2c2ca391966913e0d2 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sun, 22 Sep 2024 15:54:52 +0900 Subject: [PATCH 102/229] feat: develop patch note in admin --- accounts/admin.py | 7 +- accounts/migrations/0010_patchnote.py | 23 ++++++ .../0011_alter_patchnote_email_sent.py | 18 +++++ accounts/models.py | 32 ++++++++ accounts/utils.py | 4 + accounts/views.py | 2 +- patch_note/welcome_email.html | 74 +++++++++++++++++++ patch_note/welcome_email_7W7bocU.html | 74 +++++++++++++++++++ patch_note/welcome_email_EpSTiGC.html | 74 +++++++++++++++++++ patch_note/welcome_email_HcEbuxq.html | 74 +++++++++++++++++++ patch_note/welcome_email_PdJQmPA.html | 74 +++++++++++++++++++ patch_note/welcome_email_RHcoJDi.html | 74 +++++++++++++++++++ patch_note/welcome_email_SHBqhXW.html | 74 +++++++++++++++++++ patch_note/welcome_email_V5WiGCZ.html | 74 +++++++++++++++++++ patch_note/welcome_email_ZLS5ot7.html | 74 +++++++++++++++++++ patch_note/welcome_email_Zb6oLez.html | 74 +++++++++++++++++++ patch_note/welcome_email_gWwMOS6.html | 74 +++++++++++++++++++ patch_note/welcome_email_sqQLp6k.html | 74 +++++++++++++++++++ patch_note/welcome_email_vaREQFC.html | 74 +++++++++++++++++++ 19 files changed, 1046 insertions(+), 2 deletions(-) create mode 100644 accounts/migrations/0010_patchnote.py create mode 100644 accounts/migrations/0011_alter_patchnote_email_sent.py create mode 100644 patch_note/welcome_email.html create mode 100644 patch_note/welcome_email_7W7bocU.html create mode 100644 patch_note/welcome_email_EpSTiGC.html create mode 100644 patch_note/welcome_email_HcEbuxq.html create mode 100644 patch_note/welcome_email_PdJQmPA.html create mode 100644 patch_note/welcome_email_RHcoJDi.html create mode 100644 patch_note/welcome_email_SHBqhXW.html create mode 100644 patch_note/welcome_email_V5WiGCZ.html create mode 100644 patch_note/welcome_email_ZLS5ot7.html create mode 100644 patch_note/welcome_email_Zb6oLez.html create mode 100644 patch_note/welcome_email_gWwMOS6.html create mode 100644 patch_note/welcome_email_sqQLp6k.html create mode 100644 patch_note/welcome_email_vaREQFC.html diff --git a/accounts/admin.py b/accounts/admin.py index 2a45b54..192b164 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,7 +1,7 @@ # Register your models here. from django.contrib import admin -from .models import Device, User +from .models import Device, PatchNote, User class UserAdmin(admin.ModelAdmin): @@ -42,5 +42,10 @@ class DeviceAdmin(admin.ModelAdmin): ] +class PatchNoteAdmin(admin.ModelAdmin): + list_display = ("title", "created_at") + + admin.site.register(User, UserAdmin) admin.site.register(Device, DeviceAdmin) +admin.site.register(PatchNote, PatchNoteAdmin) diff --git a/accounts/migrations/0010_patchnote.py b/accounts/migrations/0010_patchnote.py new file mode 100644 index 0000000..b5c2bba --- /dev/null +++ b/accounts/migrations/0010_patchnote.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.6 on 2024-09-22 05:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0009_merge_20240719_1133'), + ] + + operations = [ + migrations.CreateModel( + name='PatchNote', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('html_file', models.FileField(upload_to='patch_note/')), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('email_sent', models.BooleanField(default=False)), + ], + ), + ] diff --git a/accounts/migrations/0011_alter_patchnote_email_sent.py b/accounts/migrations/0011_alter_patchnote_email_sent.py new file mode 100644 index 0000000..7dee1d8 --- /dev/null +++ b/accounts/migrations/0011_alter_patchnote_email_sent.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-09-22 06:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0010_patchnote'), + ] + + operations = [ + migrations.AlterField( + model_name='patchnote', + name='email_sent', + field=models.BooleanField(default=True), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 324a603..887e2bc 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -3,6 +3,11 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from accounts.utils import ( + render_to_string_html, + send_email, +) + class TimeStamp(models.Model): created_at = models.DateTimeField(null=True, auto_now_add=True) @@ -45,3 +50,30 @@ class Device(models.Model): token = models.CharField(max_length=255, null=True) created_at = models.DateTimeField(null=True, auto_now_add=True) deleted_at = models.DateTimeField(blank=True, null=True) + + +class PatchNote(models.Model): + title = models.CharField(max_length=255) + html_file = models.FileField(upload_to="patch_note/") + created_at = models.DateTimeField(null=True, auto_now_add=True) + email_sent = models.BooleanField(default=True) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + # 모든 사용자에게 이메일 보내기 + self.send_email_to_all_users() + + def send_email_to_all_users(self): + # 모든 사용자에게 이메일 보내기 + user_email_list = User.objects.filter( + deleted_at__isnull=True, is_staff=False + ).values_list("username", flat=True) + subject = f"OneStep's New Patch Note: {self.title}" + + message = render_to_string_html("welcome_email.html") + send_email( + to_email_address=list(user_email_list), + subject=subject, + message=message, + ) diff --git a/accounts/utils.py b/accounts/utils.py index 03649e8..ac2ff46 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -19,3 +19,7 @@ def welcome_email(user_name): "welcome_email.html", {"username": user_name} ) return html_message + + +def render_to_string_html(template_name): + return render_to_string(template_name) diff --git a/accounts/views.py b/accounts/views.py index c6e4af5..2f9bb6d 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -90,7 +90,7 @@ class EmailView(APIView): def post(self, request): result = send_email( to_email_address="szonestep@gmail.com", - subject="Welcome to join us", + subject="Welcome to join OneStep", message=welcome_email("user_name_here"), ) return Response(result, status=status.HTTP_200_OK) diff --git a/patch_note/welcome_email.html b/patch_note/welcome_email.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +

+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_7W7bocU.html b/patch_note/welcome_email_7W7bocU.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_7W7bocU.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_EpSTiGC.html b/patch_note/welcome_email_EpSTiGC.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_EpSTiGC.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_HcEbuxq.html b/patch_note/welcome_email_HcEbuxq.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_HcEbuxq.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_PdJQmPA.html b/patch_note/welcome_email_PdJQmPA.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_PdJQmPA.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_RHcoJDi.html b/patch_note/welcome_email_RHcoJDi.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_RHcoJDi.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_SHBqhXW.html b/patch_note/welcome_email_SHBqhXW.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_SHBqhXW.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_V5WiGCZ.html b/patch_note/welcome_email_V5WiGCZ.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_V5WiGCZ.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_ZLS5ot7.html b/patch_note/welcome_email_ZLS5ot7.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_ZLS5ot7.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_Zb6oLez.html b/patch_note/welcome_email_Zb6oLez.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_Zb6oLez.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_gWwMOS6.html b/patch_note/welcome_email_gWwMOS6.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_gWwMOS6.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_sqQLp6k.html b/patch_note/welcome_email_sqQLp6k.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_sqQLp6k.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + diff --git a/patch_note/welcome_email_vaREQFC.html b/patch_note/welcome_email_vaREQFC.html new file mode 100644 index 0000000..dfa627a --- /dev/null +++ b/patch_note/welcome_email_vaREQFC.html @@ -0,0 +1,74 @@ + + + + + + 환영합니다! + + + +
+

환영합니다!

+
+
+

안녕하세요, {{ username }} 님 환영합니다!

+

+ 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 + 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. +

+

저희 서비스의 주요 기능:

+
    +
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • +
  • AI 지원 서비스
  • +
  • 매일매일 피드백을 체크합니다.
  • +
+

+ 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 + 준비가 되어 있습니다. +

+

감사합니다!

+

Onestep 팀 드림

+
+ + + From 0442f91fa3a61f4bb9135a4684bf6598379145c0 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sun, 22 Sep 2024 15:56:17 +0900 Subject: [PATCH 103/229] fix : gitignore patch_note foler --- .gitignore | 1 + patch_note/welcome_email.html | 74 --------------------------- patch_note/welcome_email_7W7bocU.html | 74 --------------------------- patch_note/welcome_email_EpSTiGC.html | 74 --------------------------- patch_note/welcome_email_HcEbuxq.html | 74 --------------------------- patch_note/welcome_email_PdJQmPA.html | 74 --------------------------- patch_note/welcome_email_RHcoJDi.html | 74 --------------------------- patch_note/welcome_email_SHBqhXW.html | 74 --------------------------- patch_note/welcome_email_V5WiGCZ.html | 74 --------------------------- patch_note/welcome_email_ZLS5ot7.html | 74 --------------------------- patch_note/welcome_email_Zb6oLez.html | 74 --------------------------- patch_note/welcome_email_gWwMOS6.html | 74 --------------------------- patch_note/welcome_email_sqQLp6k.html | 74 --------------------------- patch_note/welcome_email_vaREQFC.html | 74 --------------------------- 14 files changed, 1 insertion(+), 962 deletions(-) delete mode 100644 patch_note/welcome_email.html delete mode 100644 patch_note/welcome_email_7W7bocU.html delete mode 100644 patch_note/welcome_email_EpSTiGC.html delete mode 100644 patch_note/welcome_email_HcEbuxq.html delete mode 100644 patch_note/welcome_email_PdJQmPA.html delete mode 100644 patch_note/welcome_email_RHcoJDi.html delete mode 100644 patch_note/welcome_email_SHBqhXW.html delete mode 100644 patch_note/welcome_email_V5WiGCZ.html delete mode 100644 patch_note/welcome_email_ZLS5ot7.html delete mode 100644 patch_note/welcome_email_Zb6oLez.html delete mode 100644 patch_note/welcome_email_gWwMOS6.html delete mode 100644 patch_note/welcome_email_sqQLp6k.html delete mode 100644 patch_note/welcome_email_vaREQFC.html diff --git a/.gitignore b/.gitignore index 5be46ff..eb9ef48 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,7 @@ db.sqlite3 /pyenv /.ruff_cache +/patch_note # debug .vscode/ diff --git a/patch_note/welcome_email.html b/patch_note/welcome_email.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_7W7bocU.html b/patch_note/welcome_email_7W7bocU.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_7W7bocU.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_EpSTiGC.html b/patch_note/welcome_email_EpSTiGC.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_EpSTiGC.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_HcEbuxq.html b/patch_note/welcome_email_HcEbuxq.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_HcEbuxq.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_PdJQmPA.html b/patch_note/welcome_email_PdJQmPA.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_PdJQmPA.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_RHcoJDi.html b/patch_note/welcome_email_RHcoJDi.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_RHcoJDi.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_SHBqhXW.html b/patch_note/welcome_email_SHBqhXW.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_SHBqhXW.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_V5WiGCZ.html b/patch_note/welcome_email_V5WiGCZ.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_V5WiGCZ.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_ZLS5ot7.html b/patch_note/welcome_email_ZLS5ot7.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_ZLS5ot7.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_Zb6oLez.html b/patch_note/welcome_email_Zb6oLez.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_Zb6oLez.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_gWwMOS6.html b/patch_note/welcome_email_gWwMOS6.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_gWwMOS6.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_sqQLp6k.html b/patch_note/welcome_email_sqQLp6k.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_sqQLp6k.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - diff --git a/patch_note/welcome_email_vaREQFC.html b/patch_note/welcome_email_vaREQFC.html deleted file mode 100644 index dfa627a..0000000 --- a/patch_note/welcome_email_vaREQFC.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - 환영합니다! - - - -
-

환영합니다!

-
-
-

안녕하세요, {{ username }} 님 환영합니다!

-

- 저희 서비스에 가입해 주셔서 진심으로 감사드립니다. 귀하의 선택을 - 환영하며, 함께 멋진 경험을 만들어 나가길 기대합니다. -

-

저희 서비스의 주요 기능:

-
    -
  • 큰 목표를 작은 걸음으로 나누어, 한 발씩 쉽게 나아가세요
  • -
  • AI 지원 서비스
  • -
  • 매일매일 피드백을 체크합니다.
  • -
-

- 궁금한 점이 있으시면 언제든 문의해 주세요. 저희 팀이 항상 도와드릴 - 준비가 되어 있습니다. -

-

감사합니다!

-

Onestep 팀 드림

-
- - - From d728e62fd0d966d25806388b874a3962f4cb3bf4 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 23 Sep 2024 15:13:09 +0900 Subject: [PATCH 104/229] =?UTF-8?q?fix=20:=20email=20=EB=B3=B4=EB=82=BC=20?= =?UTF-8?q?=EB=95=8C=20=EB=B3=B4=EB=82=B8=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=99=95=EC=9D=B8=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...e_email_list_alter_patchnote_email_sent.py | 23 +++++++++++++++++++ accounts/models.py | 17 ++++++++++---- accounts/utils.py | 2 +- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 accounts/migrations/0012_patchnote_email_list_alter_patchnote_email_sent.py diff --git a/accounts/migrations/0012_patchnote_email_list_alter_patchnote_email_sent.py b/accounts/migrations/0012_patchnote_email_list_alter_patchnote_email_sent.py new file mode 100644 index 0000000..001b300 --- /dev/null +++ b/accounts/migrations/0012_patchnote_email_list_alter_patchnote_email_sent.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.6 on 2024-09-23 06:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0011_alter_patchnote_email_sent'), + ] + + operations = [ + migrations.AddField( + model_name='patchnote', + name='email_list', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='patchnote', + name='email_sent', + field=models.BooleanField(default=False), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 887e2bc..0357b36 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -4,7 +4,6 @@ from django.utils.translation import gettext_lazy as _ from accounts.utils import ( - render_to_string_html, send_email, ) @@ -56,13 +55,18 @@ class PatchNote(models.Model): title = models.CharField(max_length=255) html_file = models.FileField(upload_to="patch_note/") created_at = models.DateTimeField(null=True, auto_now_add=True) - email_sent = models.BooleanField(default=True) + email_sent = models.BooleanField(default=False) + email_list = models.TextField(null=True, blank=True) def save(self, *args, **kwargs): super().save(*args, **kwargs) # 모든 사용자에게 이메일 보내기 - self.send_email_to_all_users() + if not self.email_sent: # 이메일이 아직 보내지지 않은 경우 + user_email_list = self.send_email_to_all_users() + self.email_sent = True # 이메일을 보냈으므로 True로 변경 + self.email_list = ", ".join(user_email_list) + self.save(update_fields=["email_sent", "email_list"]) def send_email_to_all_users(self): # 모든 사용자에게 이메일 보내기 @@ -71,9 +75,14 @@ def send_email_to_all_users(self): ).values_list("username", flat=True) subject = f"OneStep's New Patch Note: {self.title}" - message = render_to_string_html("welcome_email.html") + # HTML 파일 내용 읽기 + with open(self.html_file.path, "r") as f: + message = f.read() + send_email( + # to_email_address=list(user_email_list), to_email_address=list(user_email_list), subject=subject, message=message, ) + return user_email_list diff --git a/accounts/utils.py b/accounts/utils.py index ac2ff46..4560b4d 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -4,7 +4,7 @@ def send_email(to_email_address, subject, message): params: resend.Emails.SendParams = { - "from": "Acme ", + "from": "developers@stepby.one", "to": to_email_address, "subject": subject, "html": message, From 334150bf01ed8c4d73bcd878433d38f4bb2bb55b Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 23 Sep 2024 15:22:42 +0900 Subject: [PATCH 105/229] fix : delete function render_html --- accounts/utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/accounts/utils.py b/accounts/utils.py index 4560b4d..f020952 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -19,7 +19,3 @@ def welcome_email(user_name): "welcome_email.html", {"username": user_name} ) return html_message - - -def render_to_string_html(template_name): - return render_to_string(template_name) From 5f3e71fa1d6d433dfea6be34a2840a7b89cdbc6f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 23 Sep 2024 20:37:06 +0900 Subject: [PATCH 106/229] feat : develop user patch --- .../migrations/0013_user_is_subscribed.py | 18 +++++++++++++++++ accounts/models.py | 1 + accounts/serializers.py | 8 +++++++- accounts/tests.py | 16 ++++++++++++++- accounts/views.py | 20 +++++++++---------- 5 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 accounts/migrations/0013_user_is_subscribed.py diff --git a/accounts/migrations/0013_user_is_subscribed.py b/accounts/migrations/0013_user_is_subscribed.py new file mode 100644 index 0000000..f9493e9 --- /dev/null +++ b/accounts/migrations/0013_user_is_subscribed.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-09-23 11:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0012_patchnote_email_list_alter_patchnote_email_sent'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_subscribed', + field=models.BooleanField(default=False), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 0357b36..1c90ffa 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -42,6 +42,7 @@ class SocialProvider(models.TextChoices): choices=SocialProvider.choices, default=SocialProvider.GOOGLE, ) + is_subscribed = models.BooleanField(default=False) class Device(models.Model): diff --git a/accounts/serializers.py b/accounts/serializers.py index 483211b..c1a6c77 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -37,4 +37,10 @@ def is_valid(self, *, raise_exception=False): class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ["id", "email", "username", "social_provider"] + fields = [ + "id", + "email", + "username", + "social_provider", + "is_subscribed", + ] diff --git a/accounts/tests.py b/accounts/tests.py index 448ff7c..8e7457e 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -31,9 +31,23 @@ def test_user_info(create_user): "email": create_user.email, "username": create_user.username, "social_provider": "GOOGLE", + "is_subscribed": False, } +@pytest.mark.django_db +def test_update_category_success( + create_user, authenticated_client, content, order, color +): + url = reverse("user") # URL name for the categoryView patch method + data = { + "is_subscribed": True, + } + response = authenticated_client.patch(url, data, format="json") + assert response.status_code == 200 + assert response.data["is_subscribed"] + + def test_google_login(invalid_token): client = APIClient() response = client.post(reverse("google_login"), data=invalid_token) @@ -79,7 +93,7 @@ def test_google_login_new_user( # Check that the email was sent mock_send_email.assert_called_once_with( - "szonestep@gmail.com", + user.username, "Welcome to join us", welcome_email(user.username), ) diff --git a/accounts/views.py b/accounts/views.py index 2f9bb6d..f819208 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -77,6 +77,16 @@ def get(self, request): serializer = UserSerializer(user) return Response(serializer.data) + def patch(self, request): + """ + 입력 : is_subscribe (Boolean) + """ + user = User.objects.get(username=request.user.username) + user.is_subscribed = request.data.get("is_subscribed") + user.save() + serializer = UserSerializer(user) + return Response(serializer.data, status=status.HTTP_200_OK) + class AndroidClientView(APIView): def get(self, request): @@ -84,13 +94,3 @@ def get(self, request): return Response( {"android_client_id": ANDROID_CLIENT_ID}, status=status.HTTP_200_OK ) - - -class EmailView(APIView): - def post(self, request): - result = send_email( - to_email_address="szonestep@gmail.com", - subject="Welcome to join OneStep", - message=welcome_email("user_name_here"), - ) - return Response(result, status=status.HTTP_200_OK) From 02b0d84a476d3df2d34a18a59fbd355d042f918e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 24 Sep 2024 14:11:18 +0900 Subject: [PATCH 107/229] fix : test --- accounts/tests.py | 1 - onestep_be/settings.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/accounts/tests.py b/accounts/tests.py index 8e7457e..f4b085a 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -81,7 +81,6 @@ def test_google_login_new_user( # Call the view response = api_client.post(url, data, format="json") - print(response.data) # Check that the user was created user = User.objects.get(username="testuser@example.com") diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 2fa7899..e925927 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -245,12 +245,12 @@ dsn="https://9425334e0e90c405218fa9613cea9a03@o4507736964136960.ingest.us.sentry.io/4507763025117184", # Set traces_sample_rate to 1.0 to capture 100% # of transactions for performance monitoring. - traces_sample_rate=0.5, + traces_sample_rate=1.0, release=PROJECT_VERSION, # Set profiles_sample_rate to 1.0 to profile 100% # of sampled transactions. # We recommend adjusting this value in production. - profiles_sample_rate=0.5, + profiles_sample_rate=1.0, integrations=[ DjangoIntegration( transaction_style="url", From 58a5cb0b8f242a7939ea6cad32846cc2fd69ceea Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 24 Sep 2024 14:27:23 +0900 Subject: [PATCH 108/229] =?UTF-8?q?fix=20:=20class=20type=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/utils.py | 24 +++++++++++++++++------- accounts/views.py | 1 - 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/accounts/utils.py b/accounts/utils.py index f020952..32d05d7 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -1,14 +1,24 @@ +from dataclasses import dataclass + import resend from django.template.loader import render_to_string -def send_email(to_email_address, subject, message): - params: resend.Emails.SendParams = { - "from": "developers@stepby.one", - "to": to_email_address, - "subject": subject, - "html": message, - } +@dataclass +class SendParams: + from_email: str + to: str + subject: str + html: str + + +def send_email(to_email_address: str, subject: str, message: str): + params = SendParams( + from_email="developers@stepby.one", + to=to_email_address, + subject=subject, + html=message, + ) email = resend.Emails.send(params) return email diff --git a/accounts/views.py b/accounts/views.py index f819208..eb66075 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -27,7 +27,6 @@ def get(self, request): class GoogleLogin(APIView): - google_client_id = settings.SECRETS.get("GCID") authentication_classes = [] permission_classes = [AllowAny] From a3d6ecebd0e459c921ae3fc29cdba7cd561c9c86 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 24 Sep 2024 14:53:38 +0900 Subject: [PATCH 109/229] =?UTF-8?q?fix=20:=20class=20type=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=94=EA=BE=BC=20=ED=9B=84=20=EC=95=BD=EA=B0=84?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 1 - accounts/urls.py | 2 -- accounts/utils.py | 7 +++++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index 1c90ffa..8f3bdf8 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -81,7 +81,6 @@ def send_email_to_all_users(self): message = f.read() send_email( - # to_email_address=list(user_email_list), to_email_address=list(user_email_list), subject=subject, message=message, diff --git a/accounts/urls.py b/accounts/urls.py index cc65674..35de135 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -6,7 +6,6 @@ from accounts.views import ( AndroidClientView, - EmailView, GoogleLogin, TestView, UserRetrieveView, @@ -19,5 +18,4 @@ path("test/", TestView.as_view(), name="test"), path("user/", UserRetrieveView.as_view(), name="user"), path("android/", AndroidClientView.as_view(), name="android"), - path("email/", EmailView.as_view(), name="email"), ] diff --git a/accounts/utils.py b/accounts/utils.py index 32d05d7..1ce92a0 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import List, Union import resend from django.template.loader import render_to_string @@ -7,12 +8,14 @@ @dataclass class SendParams: from_email: str - to: str + to: Union[str, List[str]] subject: str html: str -def send_email(to_email_address: str, subject: str, message: str): +def send_email( + to_email_address: Union[str, List[str]], subject: str, message: str +): params = SendParams( from_email="developers@stepby.one", to=to_email_address, From a156eaf7c72cce003e283712558f6919ced7fc46 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 24 Sep 2024 14:54:42 +0900 Subject: [PATCH 110/229] fix : delete annotation --- accounts/tests.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/accounts/tests.py b/accounts/tests.py index f4b085a..85c5796 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -61,7 +61,7 @@ def api_client(self): return APIClient() @patch("accounts.views.id_token.verify_oauth2_token") - @patch("accounts.views.send_email") # Ensure the correct patch path + @patch("accounts.views.send_email") def test_google_login_new_user( self, mock_send_email, mock_verify_oauth2_token, api_client ): @@ -72,9 +72,7 @@ def test_google_login_new_user( } # Define the URL for the GoogleLogin view - url = reverse( - "google_login" - ) # Replace 'google-login' with your actual URL name + url = reverse("google_login") # Create a mock request data = {"token": "mock_token", "device_token": "mock_device_token"} From 3149f6ce90a9f72fc005eab649972aebd676c47d Mon Sep 17 00:00:00 2001 From: earthyoung Date: Thu, 29 Aug 2024 21:31:23 +0900 Subject: [PATCH 111/229] =?UTF-8?q?feat:=20authentication=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20&=20test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/authentication.py | 27 ++++++++++++++++++++++ accounts/aws.py | 1 - onestep_be/settings.py | 6 +++-- todos/firebase_messaging.py | 37 ++++++++++++++++++++++++++++++ todos/tests/test_firebase_alarm.py | 18 +++++++++++++++ todos/views.py | 2 ++ 6 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 accounts/authentication.py create mode 100644 todos/firebase_messaging.py create mode 100644 todos/tests/test_firebase_alarm.py diff --git a/accounts/authentication.py b/accounts/authentication.py new file mode 100644 index 0000000..09bd28b --- /dev/null +++ b/accounts/authentication.py @@ -0,0 +1,27 @@ +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.exceptions import InvalidToken +from rest_framework_simplejwt.settings import api_settings +from jwt import DecodeError, ExpiredSignatureError +import jwt +from fcm_django.api.rest_framework import * + + +class CustomJWTAuthentication(JWTAuthentication): + def authenticate(self, request): + raw_token = self.get_raw_token(self.get_header(request)) + + if raw_token is None: + return None + + try: + validated_token = self.get_validated_token(raw_token) + token_payload = jwt.decode( + raw_token, + api_settings.SIGNING_KEY, + algorithms=[api_settings.ALGORITHM] + ) + request.auth = token_payload + except (InvalidToken, DecodeError, ExpiredSignatureError) as e: + raise InvalidToken(e) + + return self.get_user(validated_token), token_payload diff --git a/accounts/aws.py b/accounts/aws.py index 0b97aaa..67b53bf 100644 --- a/accounts/aws.py +++ b/accounts/aws.py @@ -1,5 +1,4 @@ import os - import boto3 from dotenv import load_dotenv diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 239587b..6f0b4f2 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -62,6 +62,7 @@ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.google", + 'fcm_django', ] MIDDLEWARE = [ @@ -79,7 +80,8 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework_simplejwt.authentication.JWTAuthentication", + "accounts.authentication.CustomJWTAuthentication", + # "rest_framework_simplejwt.authentication.JWTAuthentication", ), "DEFAULT_RENDERER_CLASSES": ( "djangorestframework_camel_case.render.CamelCaseJSONRenderer", @@ -113,7 +115,7 @@ "USER_ID_FIELD": "id", "USER_ID_CLAIM": "user_id", "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", # noqa : E501 - "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken", "accounts.tokens.CustomRefreshToken"), "TOKEN_TYPE_CLAIM": "token_type", "JTI_CLAIM": "jti", "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", diff --git a/todos/firebase_messaging.py b/todos/firebase_messaging.py new file mode 100644 index 0000000..9efa4fd --- /dev/null +++ b/todos/firebase_messaging.py @@ -0,0 +1,37 @@ +import firebase_admin +import os +from firebase_admin import credentials, messaging +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onestep_be.settings') +from django.conf import settings +from dataclasses import dataclass + + +firebase_info = eval(settings.SECRETS.get("FIREBASE")) +cred = credentials.Certificate(firebase_info) +firebase_admin.initialize_app(cred) + + +@dataclass +class PushNotificationStatus: + status: str + + +PUSH_NOTIFICATION_SUCCESS = PushNotificationStatus("success") +PUSH_NOTIFICATION_ERROR = PushNotificationStatus("error") + + +def send_push_notification(token, title, body): + message = messaging.Message( + notification=messaging.Notification( + title=title, + body=body, + ), + token=token, + ) + + try: + messaging.send(message) + except Exception as e: + # sentry capture exception + return PUSH_NOTIFICATION_ERROR + return PUSH_NOTIFICATION_SUCCESS \ No newline at end of file diff --git a/todos/tests/test_firebase_alarm.py b/todos/tests/test_firebase_alarm.py new file mode 100644 index 0000000..ca8d918 --- /dev/null +++ b/todos/tests/test_firebase_alarm.py @@ -0,0 +1,18 @@ +import unittest +from unittest.mock import patch +from django.test import TestCase +from todos.firebase_messaging import send_push_notification, PUSH_NOTIFICATION_SUCCESS + +class FCMNotificationTest(TestCase): + + @patch("todos.firebase_messaging.send_push_notification") + def test_send_push_notification(self, mock_send_fcm): + mock_send_fcm.return_value = PUSH_NOTIFICATION_SUCCESS + mock_send_fcm() + response = send_push_notification("device_token", "title", "message") + mock_send_fcm.assert_called_once() + self.assertEqual(response, PUSH_NOTIFICATION_SUCCESS) + + +if __name__ == '__main__': + unittest.main() diff --git a/todos/views.py b/todos/views.py index 8e43194..b8b30ed 100644 --- a/todos/views.py +++ b/todos/views.py @@ -98,6 +98,8 @@ def get(self, request): - start_date와 end_date가 있는 경우 user_id에 해당하는 todo 중 start_date와 end_date 사이에 있는 todo를 불러옵니다. - order 의 순서로 정렬합니다. """ # noqa: E501 + print("request.auth\n", request.auth) + print("request.user\n", request.user) start_date = request.GET.get("start_date") end_date = request.GET.get("end_date") user_id = request.GET.get("user_id") From 9671a2d7ba4224f2aa876156c997968e268351d2 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Thu, 29 Aug 2024 21:48:46 +0900 Subject: [PATCH 112/229] =?UTF-8?q?fix:=20accounts.Device=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20dependency=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/authentication.py | 1 - accounts/migrations/0010_delete_device.py | 16 ++++++++++++++++ accounts/models.py | 7 ------- accounts/views.py | 2 -- 4 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 accounts/migrations/0010_delete_device.py diff --git a/accounts/authentication.py b/accounts/authentication.py index 09bd28b..d1d9f14 100644 --- a/accounts/authentication.py +++ b/accounts/authentication.py @@ -3,7 +3,6 @@ from rest_framework_simplejwt.settings import api_settings from jwt import DecodeError, ExpiredSignatureError import jwt -from fcm_django.api.rest_framework import * class CustomJWTAuthentication(JWTAuthentication): diff --git a/accounts/migrations/0010_delete_device.py b/accounts/migrations/0010_delete_device.py new file mode 100644 index 0000000..7b6f9ca --- /dev/null +++ b/accounts/migrations/0010_delete_device.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.6 on 2024-08-29 12:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0009_merge_20240719_1133'), + ] + + operations = [ + migrations.DeleteModel( + name='Device', + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 324a603..219d3bc 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -38,10 +38,3 @@ class SocialProvider(models.TextChoices): choices=SocialProvider.choices, default=SocialProvider.GOOGLE, ) - - -class Device(models.Model): - user_id = models.ForeignKey(User, on_delete=models.CASCADE) - token = models.CharField(max_length=255, null=True) - created_at = models.DateTimeField(null=True, auto_now_add=True) - deleted_at = models.DateTimeField(blank=True, null=True) diff --git a/accounts/views.py b/accounts/views.py index b3f614e..a707bc9 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -7,7 +7,6 @@ from rest_framework.response import Response from rest_framework.views import APIView -from accounts.models import Device from accounts.serializers import UserSerializer from accounts.tokens import CustomRefreshToken @@ -44,7 +43,6 @@ def post(self, request): user, _ = User.objects.get_or_create( username=email, password="" ) - Device.objects.get_or_create(user_id=user, token=device_token) refresh = CustomRefreshToken.for_user(user, device_token) return Response( { From b81dd00158e751c1cda9f2837e61177d6fceba3a Mon Sep 17 00:00:00 2001 From: earthyoung Date: Sat, 31 Aug 2024 16:38:20 +0900 Subject: [PATCH 113/229] =?UTF-8?q?chore:=20=EC=A4=91=EA=B0=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/accounts/views.py b/accounts/views.py index a707bc9..b4b4c0f 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -6,6 +6,7 @@ from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from rest_framework.views import APIView +from fcm_django.api.rest_framework import FCMDeviceCreateOnlyViewSet, FCMDeviceAuthorizedViewSet from accounts.serializers import UserSerializer from accounts.tokens import CustomRefreshToken @@ -43,6 +44,7 @@ def post(self, request): user, _ = User.objects.get_or_create( username=email, password="" ) + response = FCMDeviceCreateOnlyViewSet.as_view(request) refresh = CustomRefreshToken.for_user(user, device_token) return Response( { From 847647f73c091d7f3fec9c460624e6b3ea45bfdd Mon Sep 17 00:00:00 2001 From: earthyoung Date: Sun, 8 Sep 2024 00:47:23 +0900 Subject: [PATCH 114/229] =?UTF-8?q?fix:=20accounts>Device=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=82=AD=EC=A0=9C=ED=95=98=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/migrations/0010_delete_device.py | 16 ---------------- accounts/models.py | 7 +++++++ 2 files changed, 7 insertions(+), 16 deletions(-) delete mode 100644 accounts/migrations/0010_delete_device.py diff --git a/accounts/migrations/0010_delete_device.py b/accounts/migrations/0010_delete_device.py deleted file mode 100644 index 7b6f9ca..0000000 --- a/accounts/migrations/0010_delete_device.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.0.6 on 2024-08-29 12:48 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0009_merge_20240719_1133'), - ] - - operations = [ - migrations.DeleteModel( - name='Device', - ), - ] diff --git a/accounts/models.py b/accounts/models.py index 219d3bc..10014fa 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -38,3 +38,10 @@ class SocialProvider(models.TextChoices): choices=SocialProvider.choices, default=SocialProvider.GOOGLE, ) + + +class Device(models.Model): + user_id = models.ForeignKey(User, on_delete=models.CASCADE) + token = models.CharField(max_length=255, null=True) + created_at = models.DateTimeField(null=True, auto_now_add=True) + deleted_at = models.DateTimeField(blank=True, null=True) \ No newline at end of file From d503121b1331e2cab38952bd01c5541f86b42a7b Mon Sep 17 00:00:00 2001 From: earthyoung Date: Sun, 8 Sep 2024 01:21:45 +0900 Subject: [PATCH 115/229] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20firebase=5Fmessaging=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 5 ++++- todos/firebase_messaging.py | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index b4b4c0f..d9482e7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from fcm_django.api.rest_framework import FCMDeviceCreateOnlyViewSet, FCMDeviceAuthorizedViewSet +from fcm_django.models import FCMDevice from accounts.serializers import UserSerializer from accounts.tokens import CustomRefreshToken @@ -44,7 +45,9 @@ def post(self, request): user, _ = User.objects.get_or_create( username=email, password="" ) - response = FCMDeviceCreateOnlyViewSet.as_view(request) + FCMDevice.objects.get_or_create( + user=user, registration_id=device_token + ) refresh = CustomRefreshToken.for_user(user, device_token) return Response( { diff --git a/todos/firebase_messaging.py b/todos/firebase_messaging.py index 9efa4fd..2f8a448 100644 --- a/todos/firebase_messaging.py +++ b/todos/firebase_messaging.py @@ -4,6 +4,7 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onestep_be.settings') from django.conf import settings from dataclasses import dataclass +from fcm_django.models import FCMDevice firebase_info = eval(settings.SECRETS.get("FIREBASE")) @@ -26,11 +27,10 @@ def send_push_notification(token, title, body): title=title, body=body, ), - token=token, ) - + device = FCMDevice.objects.filter(registration_id=token).first() try: - messaging.send(message) + device.send_message(message) except Exception as e: # sentry capture exception return PUSH_NOTIFICATION_ERROR From 967951a2c526350c0d265f3bfd96ddf60597bbd9 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Wed, 11 Sep 2024 14:26:11 +0900 Subject: [PATCH 116/229] =?UTF-8?q?fix:=20fcm=5Fdjango=20message=20?= =?UTF-8?q?=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20=EC=95=88=20=EB=9C=A8?= =?UTF-8?q?=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/firebase_messaging.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/todos/firebase_messaging.py b/todos/firebase_messaging.py index 2f8a448..c0e5a33 100644 --- a/todos/firebase_messaging.py +++ b/todos/firebase_messaging.py @@ -22,15 +22,16 @@ class PushNotificationStatus: def send_push_notification(token, title, body): - message = messaging.Message( - notification=messaging.Notification( - title=title, - body=body, - ), - ) device = FCMDevice.objects.filter(registration_id=token).first() try: - device.send_message(message) + device.send_message( + messaging.Message( + notification=messaging.Notification( + title=title, + body=body, + ), + ) + ) except Exception as e: # sentry capture exception return PUSH_NOTIFICATION_ERROR From 988f74f6cc7cc1e257a38c8541be499aaff60f7f Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 24 Sep 2024 14:14:36 +0900 Subject: [PATCH 117/229] =?UTF-8?q?feat:=20=EC=95=84=EC=B9=A8,=20=EC=A0=90?= =?UTF-8?q?=EC=8B=AC,=20=EC=A0=80=EB=85=81=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20cronjob?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- onestep_be/settings.py | 8 ++++++ todos/firebase_messaging.py | 3 +-- todos/jobs.py | 50 +++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 todos/jobs.py diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 6f0b4f2..dc36757 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -63,6 +63,7 @@ "allauth.socialaccount", "allauth.socialaccount.providers.google", 'fcm_django', + "django_crontab", ] MIDDLEWARE = [ @@ -78,6 +79,13 @@ "djangorestframework_camel_case.middleware.CamelCaseMiddleWare", ] +# cron jobs +CRONJOBS = [ + ('0 8 * * *', 'todos.jobs.send_morning_alarm'), + ('0 14 * * *', 'todos.jobs.send_afternoon_alarm'), + ('0 20 * * *', 'todos.jobs.send_evening_alarm'), +] + REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "accounts.authentication.CustomJWTAuthentication", diff --git a/todos/firebase_messaging.py b/todos/firebase_messaging.py index c0e5a33..5318619 100644 --- a/todos/firebase_messaging.py +++ b/todos/firebase_messaging.py @@ -32,7 +32,6 @@ def send_push_notification(token, title, body): ), ) ) - except Exception as e: - # sentry capture exception + except Exception: return PUSH_NOTIFICATION_ERROR return PUSH_NOTIFICATION_SUCCESS \ No newline at end of file diff --git a/todos/jobs.py b/todos/jobs.py new file mode 100644 index 0000000..cb6ebd3 --- /dev/null +++ b/todos/jobs.py @@ -0,0 +1,50 @@ +import firebase_admin +import os +from firebase_admin import credentials, messaging +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onestep_be.settings') +from django.conf import settings +from fcm_django.models import FCMDevice +from django.db.models import Prefetch +from todos.models import Todo + + +firebase_info = eval(settings.SECRETS.get("FIREBASE")) +cred = credentials.Certificate(firebase_info) +firebase_admin.initialize_app(cred) + + +MORNING_ALARM_TITLE = "오늘의 할 일을 확인해보세요" +AFTERNOON_ALARM_TITLE = "지금 할 일을 확인해보세요" +EVENING_ALARM_TITLE = "오늘의 남은 할 일을 확인해보세요" + + +def send_morning_alarm(): + send_day_alarm(MORNING_ALARM_TITLE) + + +def send_afternoon_alarm(): + send_day_alarm(AFTERNOON_ALARM_TITLE) + + +def send_evening_alarm(): + send_day_alarm(EVENING_ALARM_TITLE) + + +def send_day_alarm(alarm_title): + users_prefetch = Prefetch('user__todo_set', queryset=Todo.objects.filter(is_completed=False)) + devices = FCMDevice.objects.all().select_related('user').prefetch_related(users_prefetch) + try: + for device in devices: + todos_queryset = device.user.todo_set.filter(is_completed=False).values_list("content", flat=True) + todos_list = "\n".join(todos_queryset) + device.send_message( + messaging.Message( + notification=messaging.Notification( + title=alarm_title, + body=todos_list, + ), + ) + ) + except Exception: + pass + From 0c237599e0f527bc15acfeb8cd97589c978bcac6 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 24 Sep 2024 15:30:39 +0900 Subject: [PATCH 118/229] =?UTF-8?q?fix:=20rebase=20conflict=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/authentication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/accounts/authentication.py b/accounts/authentication.py index d1d9f14..09bd28b 100644 --- a/accounts/authentication.py +++ b/accounts/authentication.py @@ -3,6 +3,7 @@ from rest_framework_simplejwt.settings import api_settings from jwt import DecodeError, ExpiredSignatureError import jwt +from fcm_django.api.rest_framework import * class CustomJWTAuthentication(JWTAuthentication): From 6405a2aedc60dd009c1217962b6f5c70710c15ab Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 24 Sep 2024 15:31:40 +0900 Subject: [PATCH 119/229] =?UTF-8?q?fix:=20rebase=20conflict=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/authentication.py | 1 - accounts/migrations/0010_delete_device.py | 16 ++++++++++++++++ accounts/models.py | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 accounts/migrations/0010_delete_device.py diff --git a/accounts/authentication.py b/accounts/authentication.py index 09bd28b..d1d9f14 100644 --- a/accounts/authentication.py +++ b/accounts/authentication.py @@ -3,7 +3,6 @@ from rest_framework_simplejwt.settings import api_settings from jwt import DecodeError, ExpiredSignatureError import jwt -from fcm_django.api.rest_framework import * class CustomJWTAuthentication(JWTAuthentication): diff --git a/accounts/migrations/0010_delete_device.py b/accounts/migrations/0010_delete_device.py new file mode 100644 index 0000000..7b6f9ca --- /dev/null +++ b/accounts/migrations/0010_delete_device.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.6 on 2024-08-29 12:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0009_merge_20240719_1133'), + ] + + operations = [ + migrations.DeleteModel( + name='Device', + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 10014fa..324a603 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -44,4 +44,4 @@ class Device(models.Model): user_id = models.ForeignKey(User, on_delete=models.CASCADE) token = models.CharField(max_length=255, null=True) created_at = models.DateTimeField(null=True, auto_now_add=True) - deleted_at = models.DateTimeField(blank=True, null=True) \ No newline at end of file + deleted_at = models.DateTimeField(blank=True, null=True) From e50fe9247a2858e8a51e3b99730130bdbe002b07 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 24 Sep 2024 15:33:14 +0900 Subject: [PATCH 120/229] =?UTF-8?q?fix:=20rebase=20conflict=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/migrations/0010_delete_device.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 accounts/migrations/0010_delete_device.py diff --git a/accounts/migrations/0010_delete_device.py b/accounts/migrations/0010_delete_device.py deleted file mode 100644 index 7b6f9ca..0000000 --- a/accounts/migrations/0010_delete_device.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.0.6 on 2024-08-29 12:48 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0009_merge_20240719_1133'), - ] - - operations = [ - migrations.DeleteModel( - name='Device', - ), - ] From 20118900648626c65cc5184fee51951f07621bb8 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 24 Sep 2024 15:38:19 +0900 Subject: [PATCH 121/229] =?UTF-8?q?chore:=20print=EB=AC=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/todos/views.py b/todos/views.py index b8b30ed..8e43194 100644 --- a/todos/views.py +++ b/todos/views.py @@ -98,8 +98,6 @@ def get(self, request): - start_date와 end_date가 있는 경우 user_id에 해당하는 todo 중 start_date와 end_date 사이에 있는 todo를 불러옵니다. - order 의 순서로 정렬합니다. """ # noqa: E501 - print("request.auth\n", request.auth) - print("request.user\n", request.user) start_date = request.GET.get("start_date") end_date = request.GET.get("end_date") user_id = request.GET.get("user_id") From db1f63c18b50d15f9182541e83050c32cca60654 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 24 Sep 2024 16:14:02 +0900 Subject: [PATCH 122/229] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- onestep_be/settings.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index dc36757..c3d3da7 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -79,7 +79,6 @@ "djangorestframework_camel_case.middleware.CamelCaseMiddleWare", ] -# cron jobs CRONJOBS = [ ('0 8 * * *', 'todos.jobs.send_morning_alarm'), ('0 14 * * *', 'todos.jobs.send_afternoon_alarm'), @@ -89,19 +88,15 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "accounts.authentication.CustomJWTAuthentication", - # "rest_framework_simplejwt.authentication.JWTAuthentication", ), "DEFAULT_RENDERER_CLASSES": ( "djangorestframework_camel_case.render.CamelCaseJSONRenderer", "djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer", - # Any other renders ), "DEFAULT_PARSER_CLASSES": ( - # If you use MultiPartFormParser or FormParser, we also have a camel case version # noqa : E501 "djangorestframework_camel_case.parser.CamelCaseFormParser", "djangorestframework_camel_case.parser.CamelCaseMultiPartParser", "djangorestframework_camel_case.parser.CamelCaseJSONParser", - # Any other parsers ), } From 176059a92de987189a6e1a61a6a4fd943eb46052 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 24 Sep 2024 23:42:08 +0900 Subject: [PATCH 123/229] refactor : accounts --- accounts/exceptions.py | 7 +++++++ accounts/models.py | 4 +--- accounts/tests.py | 8 +++++++- accounts/urls.py | 2 -- accounts/views.py | 23 ++++++++++------------- 5 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 accounts/exceptions.py diff --git a/accounts/exceptions.py b/accounts/exceptions.py new file mode 100644 index 0000000..052369e --- /dev/null +++ b/accounts/exceptions.py @@ -0,0 +1,7 @@ +from rest_framework import exceptions, status + + +class LoginException(exceptions.APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "device token and token is required" + default_code = "error" diff --git a/accounts/models.py b/accounts/models.py index 8f3bdf8..5f7387c 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -3,9 +3,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from accounts.utils import ( - send_email, -) +from accounts.utils import send_email class TimeStamp(models.Model): diff --git a/accounts/tests.py b/accounts/tests.py index 85c5796..71939dc 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -36,7 +36,7 @@ def test_user_info(create_user): @pytest.mark.django_db -def test_update_category_success( +def test_update_user_is_subscribed( create_user, authenticated_client, content, order, color ): url = reverse("user") # URL name for the categoryView patch method @@ -54,6 +54,12 @@ def test_google_login(invalid_token): assert response.status_code == 400 +def test_google_login_missing_device_token(): + client = APIClient() + response = client.post(reverse("google_login")) + assert response.status_code == 400 + + @pytest.mark.django_db class TestGoogleLogin: @pytest.fixture diff --git a/accounts/urls.py b/accounts/urls.py index 35de135..04d224c 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -7,7 +7,6 @@ from accounts.views import ( AndroidClientView, GoogleLogin, - TestView, UserRetrieveView, ) @@ -15,7 +14,6 @@ path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"), path("login/google/", GoogleLogin.as_view(), name="google_login"), - path("test/", TestView.as_view(), name="test"), path("user/", UserRetrieveView.as_view(), name="user"), path("android/", AndroidClientView.as_view(), name="android"), ] diff --git a/accounts/views.py b/accounts/views.py index eb66075..02082c0 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +from accounts.exceptions import LoginException from accounts.models import Device from accounts.serializers import UserSerializer from accounts.tokens import CustomRefreshToken @@ -19,13 +20,6 @@ GOOGLE_CLIENT_ID = settings.SECRETS.get("GCID") -class TestView(APIView): - permission_classes = [IsAuthenticated] - - def get(self, request): - return Response({"message": "Hello, World!"}) - - class GoogleLogin(APIView): authentication_classes = [] permission_classes = [AllowAny] @@ -34,28 +28,31 @@ def post(self, request): token = request.data.get("token") device_token = request.data.get("device_token") if not device_token or not token: - raise Exception("device token and token is required") + raise LoginException() try: idinfo = id_token.verify_oauth2_token( token, requests.Request(), audience=GOOGLE_CLIENT_ID ) if "accounts.google.com" in idinfo["iss"]: email = idinfo["email"] - user = self.get_or_create_user(email) + user = User.get_or_create_user(email) Device.objects.get_or_create(user_id=user, token=device_token) refresh = CustomRefreshToken.for_user(user, device_token) return Response( { "refresh": str(refresh), "access": str(refresh.access_token), - } + }, + status=status.HTTP_200_OK, ) - except Exception: - return Response(status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + return Response( + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) def get_or_create_user(self, email): try: - user = User.objects.get(username=email, password="") + user = User.objects.get(username=email) except User.DoesNotExist: user = User.objects.create(username=email, password="") send_email( From c73cd7193047f9ca8bbb7f3f2318a04eab738335 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 25 Sep 2024 15:33:16 +0900 Subject: [PATCH 124/229] =?UTF-8?q?fix=20:=20fat=20model=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 14 +++++++++++++- accounts/tests.py | 10 ++++------ accounts/utils.py | 42 +++++++++++++++++++++++++++++++++--------- accounts/views.py | 13 ------------- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index 5f7387c..2d4045a 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -3,7 +3,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from accounts.utils import send_email +from accounts.utils import send_email, send_welcome_email class TimeStamp(models.Model): @@ -42,6 +42,18 @@ class SocialProvider(models.TextChoices): ) is_subscribed = models.BooleanField(default=False) + @classmethod + def get_or_create_user(self, email): + try: + user = User.objects.get(username=email) + except User.DoesNotExist: + user = User.objects.create(username=email, password="") + send_welcome_email( + email, + (user.username), + ) + return user + class Device(models.Model): user_id = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/accounts/tests.py b/accounts/tests.py index 71939dc..d5dcf12 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -11,7 +11,6 @@ ) from accounts.models import Device -from accounts.utils import welcome_email from accounts.views import UserRetrieveView User = get_user_model() @@ -67,9 +66,9 @@ def api_client(self): return APIClient() @patch("accounts.views.id_token.verify_oauth2_token") - @patch("accounts.views.send_email") + @patch("accounts.models.send_welcome_email") def test_google_login_new_user( - self, mock_send_email, mock_verify_oauth2_token, api_client + self, mock_send_welcome_email, mock_verify_oauth2_token, api_client ): # Mock the token verification response mock_verify_oauth2_token.return_value = { @@ -95,10 +94,9 @@ def test_google_login_new_user( assert device is not None # Check that the email was sent - mock_send_email.assert_called_once_with( + mock_send_welcome_email.assert_called_once_with( + user.username, user.username, - "Welcome to join us", - welcome_email(user.username), ) # Check the response status diff --git a/accounts/utils.py b/accounts/utils.py index 1ce92a0..7b9388b 100644 --- a/accounts/utils.py +++ b/accounts/utils.py @@ -7,27 +7,51 @@ @dataclass class SendParams: - from_email: str - to: Union[str, List[str]] - subject: str - html: str + def __init__( + self, + from_email_address: str, + to_email_address: Union[str, List[str]], + subject: str, + message: str, + ): + self.from_address = from_email_address + self.to_email_address = to_email_address + self.subject = subject + self.message = message + + def to_dict(self): + return { + "from": self.from_address, + "to": self.to_email_address, + "subject": self.subject, + "html": self.message, + } def send_email( to_email_address: Union[str, List[str]], subject: str, message: str ): params = SendParams( - from_email="developers@stepby.one", - to=to_email_address, + from_email_address="developers@stepby.one", + to_email_address=to_email_address, subject=subject, - html=message, + message=message, ) - email = resend.Emails.send(params) + email = resend.Emails.send(params.to_dict()) return email -def welcome_email(user_name): +def send_welcome_email( + to_email_address: Union[str, List[str]], user_name: str +): + html_message = render_to_string( + "welcome_email.html", {"username": user_name} + ) + send_email(to_email_address, "Welcome to join us", html_message) + + +def get_welcome_email(user_name): html_message = render_to_string( "welcome_email.html", {"username": user_name} ) diff --git a/accounts/views.py b/accounts/views.py index 02082c0..027edd1 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -11,7 +11,6 @@ from accounts.models import Device from accounts.serializers import UserSerializer from accounts.tokens import CustomRefreshToken -from accounts.utils import send_email, welcome_email User = get_user_model() @@ -50,18 +49,6 @@ def post(self, request): {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - def get_or_create_user(self, email): - try: - user = User.objects.get(username=email) - except User.DoesNotExist: - user = User.objects.create(username=email, password="") - send_email( - email, - "Welcome to join us", - welcome_email(user.username), - ) - return user - class UserRetrieveView(APIView): serializer_class = UserSerializer From 73a6f42cc48683d1605377755f60b9f764cfc544 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 25 Sep 2024 15:56:44 +0900 Subject: [PATCH 125/229] =?UTF-8?q?fix=20:=20fat=20model=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/models.py b/accounts/models.py index 2d4045a..c75067e 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -50,7 +50,7 @@ def get_or_create_user(self, email): user = User.objects.create(username=email, password="") send_welcome_email( email, - (user.username), + user.username, ) return user From 93e683cb473c56b53dc8028beb7abe42ba78d93c Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 25 Sep 2024 17:11:26 +0900 Subject: [PATCH 126/229] fix : merge with delveop --- accounts/tests.py | 8 ++++++-- requirements.txt | 21 +++++++++++++++++++++ todos/tests/test_firebase_alarm.py | 11 ++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/accounts/tests.py b/accounts/tests.py index d5dcf12..6392091 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -3,6 +3,7 @@ import pytest from django.contrib.auth import get_user_model from django.urls import reverse +from fcm_django.models import FCMDevice from rest_framework import status from rest_framework.test import ( APIClient, @@ -10,7 +11,6 @@ force_authenticate, ) -from accounts.models import Device from accounts.views import UserRetrieveView User = get_user_model() @@ -47,12 +47,14 @@ def test_update_user_is_subscribed( assert response.data["is_subscribed"] +@pytest.mark.django_db def test_google_login(invalid_token): client = APIClient() response = client.post(reverse("google_login"), data=invalid_token) assert response.status_code == 400 +@pytest.mark.django_db def test_google_login_missing_device_token(): client = APIClient() response = client.post(reverse("google_login")) @@ -90,7 +92,9 @@ def test_google_login_new_user( assert user is not None # Check that the device was created - device = Device.objects.get(user_id=user.id, token="mock_device_token") + device = FCMDevice.objects.get( + user_id=user.id, registration_id="mock_device_token" + ) assert device is not None # Check that the email was sent diff --git a/requirements.txt b/requirements.txt index d7fad68..81df256 100644 --- a/requirements.txt +++ b/requirements.txt @@ -89,3 +89,24 @@ wheel==0.43.0 zope.event==5.0 zope.interface==7.0.1 resend==2.4.0 +cachecontrol==0.14.0 +fcm_django==2.2.1 +firebase-admin==6.5.0 +google-api-core==2.20.0 +google-api-python-client==2.147.0 +google-auth-httplib2==0.2.0 +google-cloud-core==2.4.1 +google-cloud-firestore==2.18.0 +google-cloud-storage==2.18.2 +google-crc32c==1.6.0 +google-resumable-media==2.7.2 +grpcio==1.66.1 +grpcio-status==1.66.1 +httplib2==0.22.0 +proto-plus==1.24.0 +protobuf==5.28.2 +swapper==1.4.0 +django_crontab==0.7.1 +mock==3.14.0 +anyio==4.2.0 +Faker==27.0.0 diff --git a/todos/tests/test_firebase_alarm.py b/todos/tests/test_firebase_alarm.py index ca8d918..4d768e3 100644 --- a/todos/tests/test_firebase_alarm.py +++ b/todos/tests/test_firebase_alarm.py @@ -1,10 +1,15 @@ import unittest from unittest.mock import patch + from django.test import TestCase -from todos.firebase_messaging import send_push_notification, PUSH_NOTIFICATION_SUCCESS -class FCMNotificationTest(TestCase): +from todos.firebase_messaging import ( + PUSH_NOTIFICATION_SUCCESS, + send_push_notification, +) + +class FCMNotificationTest(TestCase): @patch("todos.firebase_messaging.send_push_notification") def test_send_push_notification(self, mock_send_fcm): mock_send_fcm.return_value = PUSH_NOTIFICATION_SUCCESS @@ -14,5 +19,5 @@ def test_send_push_notification(self, mock_send_fcm): self.assertEqual(response, PUSH_NOTIFICATION_SUCCESS) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 4bca8b74bf9e4919585cfd7fa8023d123551b167 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 25 Sep 2024 17:33:09 +0900 Subject: [PATCH 127/229] =?UTF-8?q?fix=20:=20requirements.txt=20delete=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 81df256..43dc618 100644 --- a/requirements.txt +++ b/requirements.txt @@ -107,6 +107,4 @@ proto-plus==1.24.0 protobuf==5.28.2 swapper==1.4.0 django_crontab==0.7.1 -mock==3.14.0 -anyio==4.2.0 Faker==27.0.0 From 114fbf29522b2b2edcf2418f28947f839a6da5cc Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 25 Sep 2024 17:38:30 +0900 Subject: [PATCH 128/229] feat : add is_premium attribute in User model --- accounts/migrations/0014_user_is_premium.py | 18 ++++++++++++++++++ accounts/models.py | 1 + accounts/serializers.py | 1 + accounts/tests.py | 18 +++++++++++++++++- accounts/views.py | 7 +++++-- 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 accounts/migrations/0014_user_is_premium.py diff --git a/accounts/migrations/0014_user_is_premium.py b/accounts/migrations/0014_user_is_premium.py new file mode 100644 index 0000000..9b97f85 --- /dev/null +++ b/accounts/migrations/0014_user_is_premium.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-09-25 08:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0013_user_is_subscribed'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_premium', + field=models.BooleanField(default=False), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index c75067e..ac717e4 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -41,6 +41,7 @@ class SocialProvider(models.TextChoices): default=SocialProvider.GOOGLE, ) is_subscribed = models.BooleanField(default=False) + is_premium = models.BooleanField(default=False) @classmethod def get_or_create_user(self, email): diff --git a/accounts/serializers.py b/accounts/serializers.py index c1a6c77..da64040 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -43,4 +43,5 @@ class Meta: "username", "social_provider", "is_subscribed", + "is_premium", ] diff --git a/accounts/tests.py b/accounts/tests.py index 6392091..d3cf302 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -31,12 +31,14 @@ def test_user_info(create_user): "username": create_user.username, "social_provider": "GOOGLE", "is_subscribed": False, + "is_premium": False, } @pytest.mark.django_db def test_update_user_is_subscribed( - create_user, authenticated_client, content, order, color + create_user, + authenticated_client, ): url = reverse("user") # URL name for the categoryView patch method data = { @@ -47,6 +49,20 @@ def test_update_user_is_subscribed( assert response.data["is_subscribed"] +@pytest.mark.django_db +def test_update_user_is_premium( + create_user, + authenticated_client, +): + url = reverse("user") # URL name for the categoryView patch method + data = { + "is_premium": True, + } + response = authenticated_client.patch(url, data, format="json") + assert response.status_code == 200 + assert response.data["is_premium"] + + @pytest.mark.django_db def test_google_login(invalid_token): client = APIClient() diff --git a/accounts/views.py b/accounts/views.py index 3f300a0..851b77a 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -65,10 +65,13 @@ def get(self, request): def patch(self, request): """ - 입력 : is_subscribe (Boolean) + 입력 : is_subscribe (Boolean), is_premium (Boolean) """ user = User.objects.get(username=request.user.username) - user.is_subscribed = request.data.get("is_subscribed") + if request.data.get("is_premium"): + user.is_premium = request.data.get("is_premium") + if request.data.get("is_subscribed"): + user.is_subscribed = request.data.get("is_subscribed") user.save() serializer = UserSerializer(user) return Response(serializer.data, status=status.HTTP_200_OK) From 4b77a418d210cad01b995033d322f0f235d5a1af Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 25 Sep 2024 18:20:45 +0900 Subject: [PATCH 129/229] feat: Add UserLastUsage Model --- todos/migrations/0011_userlastusage.py | 24 ++++++++++++++++++++++++ todos/models.py | 6 ++++++ todos/serializers.py | 8 +++++++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 todos/migrations/0011_userlastusage.py diff --git a/todos/migrations/0011_userlastusage.py b/todos/migrations/0011_userlastusage.py new file mode 100644 index 0000000..96a0baf --- /dev/null +++ b/todos/migrations/0011_userlastusage.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.6 on 2024-09-25 09:20 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0010_alter_subtodo_todo_alter_todo_category_id'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserLastUsage', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('last_used_at', models.DateTimeField(null=True)), + ('user_id', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/todos/models.py b/todos/models.py index 901298c..f25ee0c 100644 --- a/todos/models.py +++ b/todos/models.py @@ -147,3 +147,9 @@ class Category(TimeStamp): order = models.CharField(max_length=255, null=True) objects = TodosManager() + + +class UserLastUsage(models.Model): + id = models.AutoField(primary_key=True) + user_id = models.ForeignKey(User, on_delete=models.PROTECT) + last_used_at = models.DateTimeField(null=True) diff --git a/todos/serializers.py b/todos/serializers.py index 527d930..565b17c 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -7,7 +7,7 @@ from accounts.models import User from todos.utils import validate_lexo_order -from .models import Category, SubTodo, Todo +from .models import Category, SubTodo, Todo, UserLastUsage class PatchOrderSerializer(serializers.Serializer): @@ -258,3 +258,9 @@ def update(self, instance, validated_data): instance.updated_at = timezone.now() instance.save() return instance + + +class UserLastUsageSerializer(serializers.ModelSerializer): + class Meta: + model = UserLastUsage + fields = ["user_id", "last_used_at"] From bbe1cbb3c5adf6526c6a82a60f791d3fd9771b92 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Thu, 26 Sep 2024 16:19:44 +0900 Subject: [PATCH 130/229] =?UTF-8?q?fix:=20middleware=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/authentication.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/accounts/authentication.py b/accounts/authentication.py index d1d9f14..92d0033 100644 --- a/accounts/authentication.py +++ b/accounts/authentication.py @@ -7,8 +7,12 @@ class CustomJWTAuthentication(JWTAuthentication): def authenticate(self, request): - raw_token = self.get_raw_token(self.get_header(request)) + header = self.get_header(request) + if header is None: + return None + + raw_token = self.get_raw_token(header) if raw_token is None: return None From 449c5fe8afde9809de444b7bb37856f36b6d797d Mon Sep 17 00:00:00 2001 From: earthyoung Date: Wed, 25 Sep 2024 17:52:45 +0900 Subject: [PATCH 131/229] =?UTF-8?q?fix:=20pytest=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=97=90=EC=84=9C=20fcm=20middleware=20=EA=B1=B4=EB=84=88?= =?UTF-8?q?=EB=9B=B0=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conftest.py | 7 ++++++ onestep_be/setting/test.py | 3 ++- onestep_be/settings.py | 11 ++++----- requirements.txt | 1 + todos/firebase_messaging.py | 7 ++---- todos/middleware.py | 46 +++++++++++++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 todos/middleware.py diff --git a/conftest.py b/conftest.py index def88ff..dc78a03 100644 --- a/conftest.py +++ b/conftest.py @@ -14,6 +14,13 @@ fake = Faker() +@pytest.fixture(autouse=True) +def skip_fcm_middleware(): + from django.conf import settings + + settings.MIDDLEWARE = settings.TEST_MIDDLEWARE + + @pytest.fixture(scope="module") def invalid_token(): response = {"token": "token", "deviceToken": "device_token"} diff --git a/onestep_be/setting/test.py b/onestep_be/setting/test.py index fc04bf6..4d11be8 100644 --- a/onestep_be/setting/test.py +++ b/onestep_be/setting/test.py @@ -4,4 +4,5 @@ SKIP_AUTHENTICATION = True if SKIP_AUTHENTICATION: - MIDDLEWARE.insert(0, "accounts.middleware.SkipAuthMiddleware") \ No newline at end of file + MIDDLEWARE.insert(0, "accounts.middleware.SkipAuthMiddleware") + MIDDLEWARE.remove("todos.middleware.FCMAlarmMiddleware") \ No newline at end of file diff --git a/onestep_be/settings.py b/onestep_be/settings.py index c883ddd..bef4fd0 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -59,11 +59,7 @@ "todos", "feedback", "rest_framework_simplejwt", - "allauth", - "allauth.account", - "allauth.socialaccount", - "allauth.socialaccount.providers.google", - "fcm_django", + 'fcm_django', "django_crontab", ] @@ -76,10 +72,13 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "allauth.account.middleware.AccountMiddleware", + # "allauth.account.middleware.AccountMiddleware", "djangorestframework_camel_case.middleware.CamelCaseMiddleWare", + "todos.middleware.FCMAlarmMiddleware", ] +TEST_MIDDLEWARE = [m for m in MIDDLEWARE if m != "todos.middleware.FCMAlarmMiddleware"] + CRONJOBS = [ ("0 8 * * *", "todos.jobs.send_morning_alarm"), ("0 14 * * *", "todos.jobs.send_afternoon_alarm"), diff --git a/requirements.txt b/requirements.txt index 43dc618..8b4720d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,6 +27,7 @@ djangorestframework-camel-case==1.4.2 djangorestframework-simplejwt==5.3.1 docutils==0.16 drf-yasg==1.21.7 +Faker==27.0.0 Flask==3.0.3 Flask-Cors==4.0.1 Flask-Login==0.6.3 diff --git a/todos/firebase_messaging.py b/todos/firebase_messaging.py index 5318619..30eb1a4 100644 --- a/todos/firebase_messaging.py +++ b/todos/firebase_messaging.py @@ -7,10 +7,6 @@ from fcm_django.models import FCMDevice -firebase_info = eval(settings.SECRETS.get("FIREBASE")) -cred = credentials.Certificate(firebase_info) -firebase_admin.initialize_app(cred) - @dataclass class PushNotificationStatus: @@ -32,6 +28,7 @@ def send_push_notification(token, title, body): ), ) ) - except Exception: + except Exception as e: + print("e", e) return PUSH_NOTIFICATION_ERROR return PUSH_NOTIFICATION_SUCCESS \ No newline at end of file diff --git a/todos/middleware.py b/todos/middleware.py new file mode 100644 index 0000000..8b33277 --- /dev/null +++ b/todos/middleware.py @@ -0,0 +1,46 @@ +from fcm_django.models import FCMDevice +import firebase_admin +import os +from firebase_admin import credentials, messaging +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onestep_be.settings') +from django.conf import settings +from fcm_django.models import FCMDevice +from todos.firebase_messaging import send_push_notification + + +FCM_ALARM_PATH_TODO = "/todos/todo" +FCM_ALARM_PATH_SUBTODO = "/todos/subtodo" +FCM_ALARM_PATH_CATEGORY = "/todos/category" + +FCM_ALARM_PATHS = [FCM_ALARM_PATH_TODO, FCM_ALARM_PATH_SUBTODO, FCM_ALARM_PATH_CATEGORY] +FCM_ALARM_METHODS = [ + "POST", "PATCH", "DELETE" +] + + +class FCMAlarmMiddleware: + + def __init__(self, get_response): + self.get_response = get_response + + def startswith_fcm_alarm_paths(self, path): + for p in FCM_ALARM_PATHS: + if path.startswith(p): + return True + return False + + def __call__(self, request): + + if request.method in FCM_ALARM_METHODS and self.startswith_fcm_alarm_paths(request.path): + fcm_token = request.auth.token + other_device = FCMDevice.objects.filter(user=request.user).exclude(registration_id=fcm_token) + + if other_device.exists(): + device_id = other_device.first().registration_id + + if request.path.startsWith(FCM_ALARM_PATH_TODO): + send_push_notification(device_id, "Todo", "") + elif request.path.startsWith(FCM_ALARM_PATH_SUBTODO): + send_push_notification(device_id, "Subtodo", "") + elif request.path.startsWith(FCM_ALARM_PATH_CATEGORY): + send_push_notification(device_id, "Category", "") From 7f8745e170cf8b2fd9641ea785a1e519627a74e7 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Thu, 26 Sep 2024 23:29:44 +0900 Subject: [PATCH 132/229] =?UTF-8?q?feat:=20Todo=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conftest.py | 7 ------- onestep_be/settings.py | 4 ---- todos/firebase_messaging.py | 23 +++++++++++++++++++++-- todos/views.py | 18 ++++++++++++++++++ 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/conftest.py b/conftest.py index dc78a03..def88ff 100644 --- a/conftest.py +++ b/conftest.py @@ -14,13 +14,6 @@ fake = Faker() -@pytest.fixture(autouse=True) -def skip_fcm_middleware(): - from django.conf import settings - - settings.MIDDLEWARE = settings.TEST_MIDDLEWARE - - @pytest.fixture(scope="module") def invalid_token(): response = {"token": "token", "deviceToken": "device_token"} diff --git a/onestep_be/settings.py b/onestep_be/settings.py index bef4fd0..bf88369 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -72,13 +72,9 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - # "allauth.account.middleware.AccountMiddleware", "djangorestframework_camel_case.middleware.CamelCaseMiddleWare", - "todos.middleware.FCMAlarmMiddleware", ] -TEST_MIDDLEWARE = [m for m in MIDDLEWARE if m != "todos.middleware.FCMAlarmMiddleware"] - CRONJOBS = [ ("0 8 * * *", "todos.jobs.send_morning_alarm"), ("0 14 * * *", "todos.jobs.send_afternoon_alarm"), diff --git a/todos/firebase_messaging.py b/todos/firebase_messaging.py index 30eb1a4..3cc792b 100644 --- a/todos/firebase_messaging.py +++ b/todos/firebase_messaging.py @@ -5,8 +5,11 @@ from django.conf import settings from dataclasses import dataclass from fcm_django.models import FCMDevice +from django.contrib.auth import get_user_model +User = get_user_model() + @dataclass class PushNotificationStatus: @@ -17,6 +20,23 @@ class PushNotificationStatus: PUSH_NOTIFICATION_ERROR = PushNotificationStatus("error") +def send_push_notification_device(token, target_user, title, body): + target_device = FCMDevice.objects.filter(user=target_user).exclude(registration_id=token) + if target_device.exists(): + target_device = target_device.first() + try: + target_device.send_message( + messaging.Message( + notification=messaging.Notification( + title=title, + body=body, + ), + ) + ) + except Exception: + pass + + def send_push_notification(token, title, body): device = FCMDevice.objects.filter(registration_id=token).first() try: @@ -28,7 +48,6 @@ def send_push_notification(token, title, body): ), ) ) - except Exception as e: - print("e", e) + except Exception: return PUSH_NOTIFICATION_ERROR return PUSH_NOTIFICATION_SUCCESS \ No newline at end of file diff --git a/todos/views.py b/todos/views.py index 8e43194..9adf7c0 100644 --- a/todos/views.py +++ b/todos/views.py @@ -21,6 +21,15 @@ SwaggerSubTodoPatchSerializer, SwaggerTodoPatchSerializer, ) +from todos.firebase_messaging import send_push_notification_device + + +TODO_FCM_MESSAGE_TITLE = "Todo" +TODO_FCM_MESSAGE_BODY = "Todo가 변경되었습니다." +SUBTODO_FCM_MESSAGE_TITLE = "SubTodo" +SUBTODO_FCM_MESSAGE_BODY = "SubTodo가 변경되었습니다." +CATEGORY_FCM_MESSAGE_TITLE = "Category" +CATEGORY_FCM_MESSAGE_BODY = "Category가 변경되었습니다." class TodoView(APIView): @@ -55,6 +64,7 @@ def post(self, request): serializer = TodoSerializer(context={"request": request}, data=data) if serializer.is_valid(raise_exception=True): serializer.save() + send_push_notification_device(request.auth.token, request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST @@ -163,6 +173,7 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() + send_push_notification_device(request.auth.token, request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_200_OK) return Response( {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST @@ -205,6 +216,7 @@ def delete(self, request): {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) + send_push_notification_device(request.auth.token, request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) return Response( {"todo_id": todo.id, "message": "Todo deleted successfully"}, status=status.HTTP_200_OK, @@ -235,6 +247,7 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() + send_push_notification_device(request.auth.token, request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST @@ -313,6 +326,7 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() + send_push_notification_device(request.auth.token, request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_200_OK) return Response( @@ -353,6 +367,7 @@ def delete(self, request): {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) + send_push_notification_device(request.auth.token, request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) return Response( { "subtodo_id": sub_todo.id, @@ -386,6 +401,7 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() + send_push_notification_device(request.auth.token, request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST @@ -439,6 +455,7 @@ def patch(self, request): ) if serializer.is_valid(raise_exception=True): serializer.save() + send_push_notification_device(request.auth.token, request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_200_OK) return Response( @@ -505,6 +522,7 @@ def delete(self, request): try: category = Category.objects.get_with_id(id=category_id) Category.objects.delete_instance(category) + send_push_notification_device(request.auth.token, request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) return Response( { "category_id": category.id, From 0e5011ef876210f02a70ec6d2fa122bc2af5b76a Mon Sep 17 00:00:00 2001 From: earthyoung Date: Fri, 27 Sep 2024 14:31:36 +0900 Subject: [PATCH 133/229] =?UTF-8?q?fix:=20rebase=20conflict=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/authentication.py | 1 - todos/views.py | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/accounts/authentication.py b/accounts/authentication.py index 92d0033..6e780ce 100644 --- a/accounts/authentication.py +++ b/accounts/authentication.py @@ -7,7 +7,6 @@ class CustomJWTAuthentication(JWTAuthentication): def authenticate(self, request): - header = self.get_header(request) if header is None: return None diff --git a/todos/views.py b/todos/views.py index 9adf7c0..3a05929 100644 --- a/todos/views.py +++ b/todos/views.py @@ -64,7 +64,7 @@ def post(self, request): serializer = TodoSerializer(context={"request": request}, data=data) if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device(request.auth.token, request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST @@ -173,7 +173,7 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device(request.auth.token, request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_200_OK) return Response( {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST @@ -216,7 +216,7 @@ def delete(self, request): {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - send_push_notification_device(request.auth.token, request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, TODO_FCM_MESSAGE_TITLE, TODO_FCM_MESSAGE_BODY) return Response( {"todo_id": todo.id, "message": "Todo deleted successfully"}, status=status.HTTP_200_OK, @@ -247,7 +247,7 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device(request.auth.token, request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST @@ -326,7 +326,7 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device(request.auth.token, request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_200_OK) return Response( @@ -367,7 +367,7 @@ def delete(self, request): {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - send_push_notification_device(request.auth.token, request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, SUBTODO_FCM_MESSAGE_TITLE, SUBTODO_FCM_MESSAGE_BODY) return Response( { "subtodo_id": sub_todo.id, @@ -401,7 +401,7 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device(request.auth.token, request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response( {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST @@ -455,7 +455,7 @@ def patch(self, request): ) if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device(request.auth.token, request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) return Response(serializer.data, status=status.HTTP_200_OK) return Response( @@ -522,7 +522,7 @@ def delete(self, request): try: category = Category.objects.get_with_id(id=category_id) Category.objects.delete_instance(category) - send_push_notification_device(request.auth.token, request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) + send_push_notification_device(request.auth.get("device"), request.user, CATEGORY_FCM_MESSAGE_TITLE, CATEGORY_FCM_MESSAGE_BODY) return Response( { "category_id": category.id, From 60e775b582d42fc1f6e4f22c976a239b39cfb92f Mon Sep 17 00:00:00 2001 From: earthyoung Date: Fri, 27 Sep 2024 20:18:16 +0900 Subject: [PATCH 134/229] =?UTF-8?q?fix:=20RecommendTodo=20view=20=EB=B9=84?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todos/views.py b/todos/views.py index 8e43194..5112df8 100644 --- a/todos/views.py +++ b/todos/views.py @@ -580,7 +580,7 @@ class RecommendSubTodo(APIView): operation_summary="Recommend subtodo", responses={200: SubTodoSerializer}, ) - def get(self, request): + async def get(self, request): """ - 이 함수는 sub todo를 추천하는 함수입니다. - 입력 : todo_id, recommend_category From dcbadaca0d929aefff5ab851e821b69df6291e94 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 28 Sep 2024 19:08:31 +0900 Subject: [PATCH 135/229] feat: recommend api check rate limit --- todos/models.py | 27 ++++++++++++++++++++++++++- todos/serializers.py | 8 +------- todos/views.py | 16 +++++++++++++--- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/todos/models.py b/todos/models.py index f25ee0c..fc9f56a 100644 --- a/todos/models.py +++ b/todos/models.py @@ -152,4 +152,29 @@ class Category(TimeStamp): class UserLastUsage(models.Model): id = models.AutoField(primary_key=True) user_id = models.ForeignKey(User, on_delete=models.PROTECT) - last_used_at = models.DateTimeField(null=True) + last_used_at = models.DateTimeField(null=False) + + @classmethod + def check_rate_limit(cls, user_id: int, RATE_LIMIT_SECONDS: int): + try: + user = User.objects.get(id=user_id) + user_last_usage, created = cls.objects.get_or_create( + user_id=user, defaults={"last_used_at": timezone.now()} + ) + + if not created: + now = timezone.now() + if ( + user_last_usage.last_used_at + + timezone.timedelta(seconds=RATE_LIMIT_SECONDS) + > now + ): + return False, "Rate limit exceeded" + else: + user_last_usage.last_used_at = now + user_last_usage.save(update_fields=["last_used_at"]) + return True, "Updated" + else: + return True, "Created" + except Exception as e: + return False, f"An error occurred: {str(e)}" diff --git a/todos/serializers.py b/todos/serializers.py index 565b17c..527d930 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -7,7 +7,7 @@ from accounts.models import User from todos.utils import validate_lexo_order -from .models import Category, SubTodo, Todo, UserLastUsage +from .models import Category, SubTodo, Todo class PatchOrderSerializer(serializers.Serializer): @@ -258,9 +258,3 @@ def update(self, instance, validated_data): instance.updated_at = timezone.now() instance.save() return instance - - -class UserLastUsageSerializer(serializers.ModelSerializer): - class Meta: - model = UserLastUsage - fields = ["user_id", "last_used_at"] diff --git a/todos/views.py b/todos/views.py index 8e43194..ed86e04 100644 --- a/todos/views.py +++ b/todos/views.py @@ -4,12 +4,12 @@ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from onestep_be.settings import client -from todos.models import Category, SubTodo, Todo +from todos.models import Category, SubTodo, Todo, UserLastUsage from todos.serializers import ( CategorySerializer, GetTodoSerializer, @@ -22,6 +22,8 @@ SwaggerTodoPatchSerializer, ) +RATE_LIMIT_SECONDS = 10 + class TodoView(APIView): permission_classes = [IsAuthenticated] @@ -564,7 +566,7 @@ def get(self, request): class RecommendSubTodo(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [AllowAny] @swagger_auto_schema( tags=["RecommendSubTodo"], @@ -588,6 +590,14 @@ def get(self, request): - 커스텀의 경우 사용자의 이전 기록들을 바탕으로 추천합니다. - 추천할 때의 subtodo 는 약 1시간의 작업으로 openAI 의 api를 통해 추천합니다. """ # noqa: E501 + user_id = request.data.get("user_id") + flag, message = UserLastUsage.check_rate_limit( + user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS + ) + if flag is False: + return Response( + {"error": message}, status=status.HTTP_429_TOO_MANY_REQUESTS + ) todo_id = request.GET.get("todo_id") try: todo = Todo.objects.get_with_id(id=todo_id) From d4c0e8a36fc4fcab824cc7d21bb8aa2220d0a1fc Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 28 Sep 2024 19:48:00 +0900 Subject: [PATCH 136/229] feat: add test code about recommend check rate limit --- conftest.py | 19 ++++++++++++ .../tests/test_recommend_check_rate_limit.py | 31 +++++++++++++++++++ todos/urls.py | 2 +- todos/views.py | 2 +- 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 todos/tests/test_recommend_check_rate_limit.py diff --git a/conftest.py b/conftest.py index def88ff..8ec1fea 100644 --- a/conftest.py +++ b/conftest.py @@ -2,6 +2,7 @@ # This file is used to define fixtures that can be used in multiple test files import random +from unittest.mock import Mock import pytest from faker import Faker @@ -112,3 +113,21 @@ def category(): ("feedback", "일반 피드백"), ] return random.choice(CATEGORY_CHOICES)[0] + + +@pytest.fixture +def llm(): + mock_response = Mock() + mock_response.choices = [ + Mock( + message=Mock( + content=( + '{"id": 1, "content": "subtask", "start_date": "2024-09-01", ' # noqa + '"end_date": "2024-09-24", "category_id": 1, "order": 1, ' + '"is_completed": false, "children": []}' + ) + ) + ) + ] + + return mock_response diff --git a/todos/tests/test_recommend_check_rate_limit.py b/todos/tests/test_recommend_check_rate_limit.py new file mode 100644 index 0000000..3f86a54 --- /dev/null +++ b/todos/tests/test_recommend_check_rate_limit.py @@ -0,0 +1,31 @@ +import time +from unittest.mock import patch + +from django.urls import reverse +from rest_framework import status + + +@patch("todos.views.client.chat.completions.create") +def test_rate_limit_exceeded(mock_llm, authenticated_client, create_todo, llm): + mock_llm.return_value = llm + url = reverse("recommend") + response = authenticated_client.get(url, {"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_200_OK + + response = authenticated_client.get(url, {"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS + assert response.data["error"] == "Rate limit exceeded" + + +@patch("todos.views.client.chat.completions.create") +def test_rate_limit_passed(mock_llm, authenticated_client, create_todo, llm): + url = reverse("recommend") + mock_llm.return_value = llm + + response = authenticated_client.get(url, {"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_200_OK + + time.sleep(10) + + response = authenticated_client.get(url, {"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_200_OK diff --git a/todos/urls.py b/todos/urls.py index 65fdb65..cb9b4ce 100644 --- a/todos/urls.py +++ b/todos/urls.py @@ -14,5 +14,5 @@ path("sub/", SubTodoView.as_view(), name="subtodos"), path("category/", CategoryView.as_view(), name="category"), path("inbox/", InboxView.as_view(), name="inbox"), - path("recommend/", RecommendSubTodo.as_view(), name="Recommend"), + path("recommend/", RecommendSubTodo.as_view(), name="recommend"), ] diff --git a/todos/views.py b/todos/views.py index ed86e04..5c62e62 100644 --- a/todos/views.py +++ b/todos/views.py @@ -590,7 +590,7 @@ def get(self, request): - 커스텀의 경우 사용자의 이전 기록들을 바탕으로 추천합니다. - 추천할 때의 subtodo 는 약 1시간의 작업으로 openAI 의 api를 통해 추천합니다. """ # noqa: E501 - user_id = request.data.get("user_id") + user_id = request.user.id flag, message = UserLastUsage.check_rate_limit( user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS ) From 35d8ec7ba54986a4b82e53cc41b8a981c148c6a0 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 28 Sep 2024 19:58:42 +0900 Subject: [PATCH 137/229] =?UTF-8?q?feat=20:=20premium=20user=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=ED=92=80=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/models.py | 7 ++++++- todos/tests/test_recommend_check_rate_limit.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/todos/models.py b/todos/models.py index fc9f56a..fd87ff0 100644 --- a/todos/models.py +++ b/todos/models.py @@ -161,7 +161,10 @@ def check_rate_limit(cls, user_id: int, RATE_LIMIT_SECONDS: int): user_last_usage, created = cls.objects.get_or_create( user_id=user, defaults={"last_used_at": timezone.now()} ) - + if user.is_premium: + user_last_usage.last_used_at = timezone.now() + user_last_usage.save(update_fields=["last_used_at"]) + return True, "Premium user" if not created: now = timezone.now() if ( @@ -176,5 +179,7 @@ def check_rate_limit(cls, user_id: int, RATE_LIMIT_SECONDS: int): return True, "Updated" else: return True, "Created" + except User.DoesNotExist: + return False, "User does not exist" except Exception as e: return False, f"An error occurred: {str(e)}" diff --git a/todos/tests/test_recommend_check_rate_limit.py b/todos/tests/test_recommend_check_rate_limit.py index 3f86a54..fda3bc9 100644 --- a/todos/tests/test_recommend_check_rate_limit.py +++ b/todos/tests/test_recommend_check_rate_limit.py @@ -29,3 +29,20 @@ def test_rate_limit_passed(mock_llm, authenticated_client, create_todo, llm): response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK + + +@patch("todos.views.client.chat.completions.create") +def test_rate_limit_premium( + mock_llm, authenticated_client, create_user, create_todo, llm +): + create_user.is_premium = True + create_user.save() + + url = reverse("recommend") + mock_llm.return_value = llm + + response = authenticated_client.get(url, {"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_200_OK + + response = authenticated_client.get(url, {"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_200_OK From 09d7c9a63c2f7682bea18039e548d13d159b9ac0 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 28 Sep 2024 20:13:56 +0900 Subject: [PATCH 138/229] fix : settings --- onestep_be/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index c883ddd..505c45f 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -22,6 +22,7 @@ from accounts.aws import get_secret +CSRF_TRUSTED_ORIGINS = ["https://*.stepby.one"] # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent From ce27f746cc387459da466c3b31fa0f78892091f5 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Sat, 28 Sep 2024 20:45:26 +0900 Subject: [PATCH 139/229] =?UTF-8?q?fix:=20gunicorn=20wsgi=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20->=20uvicorn=20asgi=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.dev | 3 ++- Dockerfile.prod | 3 ++- Dockerfile.test | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index d977c6c..7af9dcb 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -15,5 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +# CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.dev"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod index 0b7586c..01457ee 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -15,5 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +# CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.prod"] diff --git a/Dockerfile.test b/Dockerfile.test index 90d5082..f28016f 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -15,5 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +# CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.test"] From 5e29dee7909d579cf0953f4752395fae6273335a Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 30 Sep 2024 18:17:51 +0900 Subject: [PATCH 140/229] feat : add sentry log --- accounts/models.py | 38 ++++++++++++++++++++++++--------- accounts/views.py | 52 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index c75067e..be5a997 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,3 +1,4 @@ +import sentry_sdk from django.contrib.auth.models import AbstractUser from django.contrib.auth.validators import UnicodeUsernameValidator from django.db import models @@ -47,11 +48,24 @@ def get_or_create_user(self, email): try: user = User.objects.get(username=email) except User.DoesNotExist: + sentry_sdk.capture_message( + "User does not exist. Creating new user" + ) user = User.objects.create(username=email, password="") send_welcome_email( email, user.username, ) + sentry_sdk.capture_message("Sent welcome email", level="info") + except Exception as e: + sentry_sdk.capture_exception(e) + sentry_sdk.capture_message( + "Failed to get or create user", level="error" + ) + raise e + + sentry_sdk.set_user({"id": user.id}) + sentry_sdk.capture_message("Get User", level="info") return user @@ -85,14 +99,18 @@ def send_email_to_all_users(self): deleted_at__isnull=True, is_staff=False ).values_list("username", flat=True) subject = f"OneStep's New Patch Note: {self.title}" - - # HTML 파일 내용 읽기 - with open(self.html_file.path, "r") as f: - message = f.read() - - send_email( - to_email_address=list(user_email_list), - subject=subject, - message=message, + sentry_sdk.capture_message( + f"Patch note title : {self.title}, Sending email to users" ) - return user_email_list + try: + # HTML 파일 내용 읽기 + with open(self.html_file.path, "r") as f: + message = f.read() + send_email( + to_email_address=list(user_email_list), + subject=subject, + message=message, + ) + return user_email_list + except Exception as e: + sentry_sdk.capture_exception(e) diff --git a/accounts/views.py b/accounts/views.py index 3f300a0..f7edb62 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,4 @@ +import sentry_sdk from django.conf import settings from django.contrib.auth import get_user_model from fcm_django.models import FCMDevice @@ -27,6 +28,7 @@ def post(self, request): token = request.data.get("token") device_token = request.data.get("device_token") if not device_token or not token: + sentry_sdk.capture_exception(LoginException()) raise LoginException() try: idinfo = id_token.verify_oauth2_token( @@ -35,7 +37,6 @@ def post(self, request): if "accounts.google.com" in idinfo["iss"]: email = idinfo["email"] user = User.get_or_create_user(email) - FCMDevice.objects.get_or_create( user=user, registration_id=device_token ) @@ -48,6 +49,7 @@ def post(self, request): status=status.HTTP_200_OK, ) except Exception as e: + sentry_sdk.capture_exception(e) return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) @@ -59,24 +61,48 @@ class UserRetrieveView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - user = User.objects.get(username=request.user.username) - serializer = UserSerializer(user) - return Response(serializer.data) + try: + user = User.objects.get(username=request.user.username) + serializer = UserSerializer(user) + return Response(serializer.data, status=status.HTTP_200_OK) + except User.DoesNotExist: + sentry_sdk.capture_exception(User.DoesNotExist()) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": "User not found"}, status=status.HTTP_400_BAD_REQUEST + ) def patch(self, request): """ 입력 : is_subscribe (Boolean) """ - user = User.objects.get(username=request.user.username) - user.is_subscribed = request.data.get("is_subscribed") - user.save() - serializer = UserSerializer(user) - return Response(serializer.data, status=status.HTTP_200_OK) + try: + user = request.user + user.is_subscribed = request.data.get("is_subscribed") + user.save() + serializer = UserSerializer(user) + return Response(serializer.data, status=status.HTTP_200_OK) + except User.DoesNotExist: + sentry_sdk.capture_exception(User.DoesNotExist()) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": "User not found"}, status=status.HTTP_400_BAD_REQUEST + ) class AndroidClientView(APIView): def get(self, request): - ANDROID_CLIENT_ID = settings.SECRETS.get("ANDROID_CLIENT_ID") - return Response( - {"android_client_id": ANDROID_CLIENT_ID}, status=status.HTTP_200_OK - ) + try: + ANDROID_CLIENT_ID = settings.SECRETS.get("ANDROID_CLIENT_ID") + return Response( + {"android_client_id": ANDROID_CLIENT_ID}, + status=status.HTTP_200_OK, + ) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": "Android client id not found"}, + status=status.HTTP_400_BAD_REQUEST, + ) From 12e0d3805c6be1d721c5b955da787e5b8694d171 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 30 Sep 2024 21:07:00 +0900 Subject: [PATCH 141/229] feat : add sentry log in todos view --- accounts/views.py | 35 ++-- todos/tests/test_category_post.py | 15 -- todos/tests/test_todo_post.py | 18 --- todos/utils.py | 19 +++ todos/views.py | 261 +++++++++++++++++++----------- 5 files changed, 214 insertions(+), 134 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index f7edb62..864dab1 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -62,16 +62,27 @@ class UserRetrieveView(APIView): def get(self, request): try: + sentry_sdk.set_user( + { + "id": request.user.id, + "username": request.user.username, + } + ) user = User.objects.get(username=request.user.username) serializer = UserSerializer(user) return Response(serializer.data, status=status.HTTP_200_OK) - except User.DoesNotExist: - sentry_sdk.capture_exception(User.DoesNotExist()) + except User.DoesNotExist as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": "User not found"}, status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: sentry_sdk.capture_exception(e) - return Response( - {"error": "User not found"}, status=status.HTTP_400_BAD_REQUEST - ) + return Response( + {"error": "An unexpected error occurred"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) def patch(self, request): """ @@ -83,13 +94,17 @@ def patch(self, request): user.save() serializer = UserSerializer(user) return Response(serializer.data, status=status.HTTP_200_OK) - except User.DoesNotExist: - sentry_sdk.capture_exception(User.DoesNotExist()) + except User.DoesNotExist as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": "User not found"}, status=status.HTTP_404_NOT_FOUND + ) except Exception as e: sentry_sdk.capture_exception(e) - return Response( - {"error": "User not found"}, status=status.HTTP_400_BAD_REQUEST - ) + return Response( + {"error": "An unexpected error occurred"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) class AndroidClientView(APIView): diff --git a/todos/tests/test_category_post.py b/todos/tests/test_category_post.py index 4a3e991..8c04f21 100644 --- a/todos/tests/test_category_post.py +++ b/todos/tests/test_category_post.py @@ -48,18 +48,3 @@ def test_create_category_invalid_order( } response = authenticated_client.post(url, data, format="json") assert response.status_code == 400 - - -@pytest.mark.django_db -def test_create_category_invalid_user_id( - authenticated_client, content, order, color -): - url = reverse("category") - data = { - "user_id": 999, - "title": content, - "color": color, - "order": order(0), - } - response = authenticated_client.post(url, data, format="json") - assert response.status_code == 400 diff --git a/todos/tests/test_todo_post.py b/todos/tests/test_todo_post.py index 2704644..91a5e85 100644 --- a/todos/tests/test_todo_post.py +++ b/todos/tests/test_todo_post.py @@ -58,7 +58,6 @@ def test_create_todo_invalid_order( } response = authenticated_client.post(url, data, format="json") assert response.status_code == 400 - assert response.data["non_field_errors"][0] == "Order is invalid" @pytest.mark.django_db @@ -109,20 +108,3 @@ def test_create_todo_invalid_category_id( } response = authenticated_client.post(url, data, format="json") assert response.status_code == 400 - - -@pytest.mark.django_db -def test_create_todo_invalid_user_id( - authenticated_client, create_category, date, content, order -): - url = reverse("todos") - data = { - "user_id": 999, - "start_date": date + timedelta(days=1), - "end_date": date + timedelta(days=2), - "content": content, - "category_id": create_category.id, - "order": order(0), - } - response = authenticated_client.post(url, data, format="json") - assert response.status_code == 400 diff --git a/todos/utils.py b/todos/utils.py index b3a390c..6184809 100644 --- a/todos/utils.py +++ b/todos/utils.py @@ -1,3 +1,5 @@ +import sentry_sdk + from todos.lexorank import LexoRank @@ -22,3 +24,20 @@ def validate_lexo_order(prev, next, updated): ): return False return True + + +def set_sentry_user(user): + sentry_sdk.set_user( + { + "id": user.id, + "username": user.username, + } + ) + + +def sentry_validation_error(where: str, error, user_id): + sentry_sdk.capture_message( + "Validation Error in" + where, + level="error", + extra={"error": error, "user_id": user_id}, + ) diff --git a/todos/views.py b/todos/views.py index 8e43194..7c56781 100644 --- a/todos/views.py +++ b/todos/views.py @@ -1,6 +1,7 @@ # todos/views.py import json +import sentry_sdk from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status @@ -21,6 +22,7 @@ SwaggerSubTodoPatchSerializer, SwaggerTodoPatchSerializer, ) +from todos.utils import sentry_validation_error, set_sentry_user class TodoView(APIView): @@ -36,29 +38,42 @@ class TodoView(APIView): def post(self, request): """ - 이 함수는 todo를 생성하는 함수입니다. - - 입력 : user_id, start_date, deadline, content, category, parent_id + - 입력 : start_date, end_date, content, category, parent_id - content 는 암호화 되어야 합니다. - - deadline 은 항상 start_date 와 같은 날이거나 그 이후여야합니다 - category_id 는 category에 존재해야합니다. - content는 1자 이상 50자 이하여야합니다. - - user_id 는 user 테이블에 존재해야합니다. - - parent_id는 todo 테이블에 이미 존재해야합니다. - - parent_id가 없는 경우 null로 처리합니다. - - parent_id는 자기 자신을 참조할 수 없습니다. 구현해야할 내용 - order 순서 정리 - 암호화 """ - data = request.data - # category_id validation - serializer = TodoSerializer(context={"request": request}, data=data) - if serializer.is_valid(raise_exception=True): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response( - {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST - ) + try: + data = request.data.copy() + data["user_id"] = request.user.id + + set_sentry_user(request.user) + serializer = TodoSerializer( + context={"request": request}, data=data + ) + if serializer.is_valid(raise_exception=True): + serializer.save() + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + else: + sentry_validation_error( + "TodoCreate", serializer.errors, request.user.id + ) + return Response( + {"error": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": str(e)}, + status=status.HTTP_400_BAD_REQUEST, + ) @swagger_auto_schema( tags=["Todo"], @@ -93,20 +108,15 @@ def post(self, request): def get(self, request): """ - 이 함수는 daily todo list를 불러오는 함수입니다. - - 입력 : user_id(필수), start_date, end_date + - 입력 : start_date, end_date - start_date와 end_date가 없는 경우 user_id에 해당하는 모든 todo를 불러옵니다. - start_date와 end_date가 있는 경우 user_id에 해당하는 todo 중 start_date와 end_date 사이에 있는 todo를 불러옵니다. - order 의 순서로 정렬합니다. """ # noqa: E501 start_date = request.GET.get("start_date") end_date = request.GET.get("end_date") - user_id = request.GET.get("user_id") - - if user_id is None: - return Response( - {"error": "user_id must be provided"}, - status=status.HTTP_400_BAD_REQUEST, - ) + user_id = request.user.id + set_sentry_user(request.user) try: if ( start_date is not None and end_date is not None @@ -114,11 +124,15 @@ def get(self, request): todos = Todo.objects.get_daily_with_date( user_id=user_id, start_date=start_date, end_date=end_date ) - else: # start_date and end_date are None + sentry_sdk.capture_message("Get inbox todos", level="info") + else: todos = Todo.objects.get_with_user_id( user_id=user_id ).order_by("order") - except Todo.DoesNotExist: + + sentry_sdk.capture_message("Get daily todos", level="info") + except Todo.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "Todo not found"}, status=status.HTTP_404_NOT_FOUND ) @@ -146,7 +160,8 @@ def patch(self, request): todo_id = request.data.get("todo_id") try: todo = Todo.objects.get(id=todo_id, deleted_at__isnull=True) - except Todo.DoesNotExist: + except Todo.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "Todo not found"}, status=status.HTTP_404_NOT_FOUND ) @@ -164,9 +179,14 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) - return Response( - {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST - ) + else: + sentry_validation_error( + "TodoPatch", serializer.errors, request.user.id + ) + return Response( + {"error": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) @swagger_auto_schema( tags=["Todo"], @@ -196,20 +216,21 @@ def delete(self, request): subtodos = SubTodo.objects.get_subtodos(todo.id) SubTodo.objects.delete_many(subtodos) Todo.objects.delete_instance(todo) - except Todo.DoesNotExist: return Response( - {"error": "Todo not found"}, status=status.HTTP_404_NOT_FOUND + {"todo_id": todo.id, "message": "Todo deleted successfully"}, + status=status.HTTP_200_OK, + ) + except Todo.DoesNotExist as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": "Todo not found"}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: + sentry_sdk.capture_exception(e) return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - return Response( - {"todo_id": todo.id, "message": "Todo deleted successfully"}, - status=status.HTTP_200_OK, - ) - class SubTodoView(APIView): permission_classes = [IsAuthenticated] @@ -235,10 +256,16 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() + sentry_sdk.capture_message("SubTodo created", level="info") return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response( - {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST - ) + else: + sentry_validation_error( + "SubTodoCreate", serializer.errors, request.user.id + ) + return Response( + {"error": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) @swagger_auto_schema( tags=["SubTodo"], @@ -263,14 +290,19 @@ def get(self, request): todo_id = request.GET.get("todo_id") try: sub_todos = SubTodo.objects.get_subtodos(todo_id=todo_id) - except SubTodo.DoesNotExist: + serializer = SubTodoSerializer(sub_todos, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except SubTodo.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "SubTodo not found"}, status=status.HTTP_404_NOT_FOUND, ) - - serializer = SubTodoSerializer(sub_todos, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) @swagger_auto_schema( tags=["SubTodo"], @@ -313,11 +345,16 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() + sentry_sdk.capture_message("SubTodo updated", level="info") return Response(serializer.data, status=status.HTTP_200_OK) - - return Response( - {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST - ) + else: + sentry_validation_error( + "SubTodoPatch", serializer.errors, request.user.id + ) + return Response( + {"error": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) @swagger_auto_schema( tags=["SubTodo"], @@ -343,24 +380,26 @@ def delete(self, request): try: sub_todo = SubTodo.objects.get_with_id(id=subtodo_id) SubTodo.objects.delete_instance(sub_todo) - except SubTodo.DoesNotExist: + sentry_sdk.capture_message("SubTodo deleted", level="info") + return Response( + { + "subtodo_id": sub_todo.id, + "message": "SubTodo deleted successfully", + }, + status=status.HTTP_200_OK, + ) + except SubTodo.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "SubTodo not found"}, status=status.HTTP_404_NOT_FOUND, ) except Exception as e: + sentry_sdk.capture_exception(e) return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - return Response( - { - "subtodo_id": sub_todo.id, - "message": "SubTodo deleted successfully", - }, - status=status.HTTP_200_OK, - ) - class CategoryView(APIView): permission_classes = [IsAuthenticated] @@ -374,11 +413,12 @@ class CategoryView(APIView): def post(self, request): """ - 이 함수는 category를 생성하는 함수입니다. - - 입력 : user_id, title, color + - 입력 : title, color - title은 1자 이상 50자 이하여야합니다. - color는 7자여야합니다. """ - data = request.data + data = request.data.copy() + data["user_id"] = request.user.id serializer = CategorySerializer( context={"request": request}, data=data @@ -387,9 +427,14 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response( - {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST - ) + else: + sentry_validation_error( + "CategoryCreate", serializer.errors, request.user.id + ) + return Response( + {"error": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) @swagger_auto_schema( tags=["Category"], @@ -420,11 +465,17 @@ def patch(self, request): category = Category.objects.get( id=category_id, deleted_at__isnull=True ) - except Category.DoesNotExist: + except Category.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "Category not found"}, status=status.HTTP_404_NOT_FOUND, ) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) if "order" in request.data: nested_order = request.data.get("order") @@ -439,11 +490,16 @@ def patch(self, request): ) if serializer.is_valid(raise_exception=True): serializer.save() + sentry_sdk.capture_message("Category updated", level="info") return Response(serializer.data, status=status.HTTP_200_OK) - - return Response( - {"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST - ) + else: + sentry_validation_error( + "CategoryPatch", serializer.errors, request.user.id + ) + return Response( + {"error": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) @swagger_auto_schema( tags=["Category"], @@ -462,24 +518,25 @@ def patch(self, request): def get(self, request): """ - 이 함수는 category list를 불러오는 함수입니다. - - 입력 : user_id(필수) + - 입력 : 없음 - user_id에 해당하는 category list를 불러옵니다. """ - user_id = request.GET.get("user_id") - if user_id is None: - return Response( - {"error": "user_id must be provided"}, - status=status.HTTP_400_BAD_REQUEST, - ) try: + user_id = request.user.id categories = Category.objects.get_with_user_id(user_id=user_id) - except Category.DoesNotExist: + serializer = CategorySerializer(categories, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Category.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "Category not found"}, status=status.HTTP_404_NOT_FOUND, ) - serializer = CategorySerializer(categories, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) @swagger_auto_schema( tags=["Category"], @@ -501,8 +558,16 @@ def delete(self, request): - category_id에 해당하는 category의 deleted_at 필드를 현재 시간으로 업데이트합니다. - deleted_at 필드가 null이 아닌 경우 이미 삭제된 category입니다. """ # noqa: E501 - category_id = request.data.get("category_id") try: + category_id = request.data.get("category_id") + if category_id is None: + sentry_sdk.capture_message( + "Category_id not provided", level="error" + ) + return Response( + {"error": "category_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) category = Category.objects.get_with_id(id=category_id) Category.objects.delete_instance(category) return Response( @@ -512,12 +577,14 @@ def delete(self, request): }, status=status.HTTP_200_OK, ) - except Category.DoesNotExist: + except Category.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "Category not found"}, status=status.HTTP_404_NOT_FOUND, ) except Exception as e: + sentry_sdk.capture_exception(e) return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) @@ -543,24 +610,32 @@ class InboxView(APIView): def get(self, request): """ - 이 함수는 daily todo list를 불러오는 함수입니다. - - 입력 : user_id(필수) + - 입력 : 없음 - order 의 순서로 정렬합니다. """ - user_id = request.GET.get("user_id") - - if user_id is None: - return Response( - {"error": "user_id must be provided"}, - status=status.HTTP_400_BAD_REQUEST, - ) try: + user_id = request.user.id + if user_id is None: + sentry_sdk.capture_message( + "User_id not provided", level="error" + ) + return Response( + {"error": "user_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) todos = Todo.objects.get_inbox(user_id=user_id) - except Todo.DoesNotExist: + serializer = GetTodoSerializer(todos, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Todo.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "Inbox is Empty"}, status=status.HTTP_404_NOT_FOUND ) - serializer = GetTodoSerializer(todos, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) class RecommendSubTodo(APIView): @@ -624,15 +699,19 @@ def get(self, request): response_format={"type": "json_object"}, ) - except Todo.DoesNotExist: + except Todo.DoesNotExist as e: + sentry_sdk.capture_exception(e) return Response( {"error": "Todo not found"}, status=status.HTTP_404_NOT_FOUND ) except Exception as e: + sentry_sdk.capture_exception(e) return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - + sentry_sdk.capture_message( + "SubTodo recommended", level="info", extra={"todo_id": todo.id} + ) return Response( json.loads(completion.choices[0].message.content), status=status.HTTP_200_OK, From 6b2ebce9e87051bccc92715566dea4ba29226f95 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 30 Sep 2024 21:20:04 +0900 Subject: [PATCH 142/229] =?UTF-8?q?feat=20:=20sentry=20user=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20in=20todos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/views.py | 124 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 21 deletions(-) diff --git a/todos/views.py b/todos/views.py index 7c56781..db12389 100644 --- a/todos/views.py +++ b/todos/views.py @@ -117,6 +117,12 @@ def get(self, request): end_date = request.GET.get("end_date") user_id = request.user.id set_sentry_user(request.user) + if user_id is None: + sentry_sdk.capture_message("User_id not provided", level="error") + return Response( + {"error": "user_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) try: if ( start_date is not None and end_date is not None @@ -157,7 +163,14 @@ def patch(self, request): "updated_order" : "0|asdf:" } """ # noqa: E501 + set_sentry_user(request.user) todo_id = request.data.get("todo_id") + if todo_id is None: + sentry_sdk.capture_message("Todo_id not provided", level="error") + return Response( + {"error": "todo_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) try: todo = Todo.objects.get(id=todo_id, deleted_at__isnull=True) except Todo.DoesNotExist as e: @@ -209,8 +222,14 @@ def delete(self, request): - deleted_at 필드가 null이 아닌 경우 이미 삭제된 todo입니다. - 해당 todo 에 속한 subtodo 도 전부 delete 를 해야함 """ # noqa: E501 + set_sentry_user(request.user) todo_id = request.data.get("todo_id") - + if todo_id is None: + sentry_sdk.capture_message("Todo_id not provided", level="error") + return Response( + {"error": "todo_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) try: todo = Todo.objects.get_with_id(id=todo_id) subtodos = SubTodo.objects.get_subtodos(todo.id) @@ -249,6 +268,7 @@ def post(self, request): - content 는 암호화 되어야 합니다(// 미정) - date 는 parent의 start_date와 end_date의 사이여야 합니다. """ + set_sentry_user(request.user) data = request.data serializer = SubTodoSerializer( context={"request": request}, data=data, many=True @@ -287,7 +307,14 @@ def get(self, request): - 입력 : todo_id - parent_id에 해당하는 sub todo list를 불러옵니다. """ + set_sentry_user(request.user) todo_id = request.GET.get("todo_id") + if todo_id is None: + sentry_sdk.capture_message("Todo_id not provided", level="error") + return Response( + {"error": "todo_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) try: sub_todos = SubTodo.objects.get_subtodos(todo_id=todo_id) serializer = SubTodoSerializer(sub_todos, many=True) @@ -322,7 +349,16 @@ def patch(self, request): "updated_order" : "0|asdf:" } """ + set_sentry_user(request.user) subtodo_id = request.data.get("subtodo_id") + if subtodo_id is None: + sentry_sdk.capture_message( + "SubTodo_id not provided", level="error" + ) + return Response( + {"error": "subtodo_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) try: sub_todo = SubTodo.objects.get( id=subtodo_id, deleted_at__isnull=True @@ -376,7 +412,16 @@ def delete(self, request): - subtodo_id에 해당하는 sub todo의 deleted_at 필드를 현재 시간으로 업데이트합니다. - deleted_at 필드가 null이 아닌 경우 이미 삭제된 sub todo입니다. """ # noqa: E501 + set_sentry_user(request.user) subtodo_id = request.data.get("subtodo_id") + if subtodo_id is None: + sentry_sdk.capture_message( + "SubTodo_id not provided", level="error" + ) + return Response( + {"error": "subtodo_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) try: sub_todo = SubTodo.objects.get_with_id(id=subtodo_id) SubTodo.objects.delete_instance(sub_todo) @@ -417,22 +462,32 @@ def post(self, request): - title은 1자 이상 50자 이하여야합니다. - color는 7자여야합니다. """ - data = request.data.copy() - data["user_id"] = request.user.id - - serializer = CategorySerializer( - context={"request": request}, data=data - ) + set_sentry_user(request.user) + try: + data = request.data.copy() + data["user_id"] = request.user.id - if serializer.is_valid(raise_exception=True): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - sentry_validation_error( - "CategoryCreate", serializer.errors, request.user.id + serializer = CategorySerializer( + context={"request": request}, data=data ) + + if serializer.is_valid(raise_exception=True): + serializer.save() + return Response( + serializer.data, status=status.HTTP_201_CREATED + ) + else: + sentry_validation_error( + "CategoryCreate", serializer.errors, request.user.id + ) + return Response( + {"error": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + sentry_sdk.capture_exception(e) return Response( - {"error": serializer.errors}, + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST, ) @@ -454,7 +509,16 @@ def patch(self, request): "updated_order" : "0|asdf:" } """ + set_sentry_user(request.user) category_id = request.data.get("category_id") + if category_id is None: + sentry_sdk.capture_message( + "Category_id not provided", level="error" + ) + return Response( + {"error": "category_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) if "user_id" in request.data: return Response( {"error": "user_id cannot be updated"}, @@ -521,8 +585,17 @@ def get(self, request): - 입력 : 없음 - user_id에 해당하는 category list를 불러옵니다. """ + set_sentry_user(request.user) try: user_id = request.user.id + if user_id is None: + sentry_sdk.capture_message( + "User_id not provided", level="error" + ) + return Response( + {"error": "user_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) categories = Category.objects.get_with_user_id(user_id=user_id) serializer = CategorySerializer(categories, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @@ -559,6 +632,7 @@ def delete(self, request): - deleted_at 필드가 null이 아닌 경우 이미 삭제된 category입니다. """ # noqa: E501 try: + set_sentry_user(request.user) category_id = request.data.get("category_id") if category_id is None: sentry_sdk.capture_message( @@ -614,6 +688,7 @@ def get(self, request): - order 의 순서로 정렬합니다. """ try: + set_sentry_user(request.user) user_id = request.user.id if user_id is None: sentry_sdk.capture_message( @@ -663,7 +738,14 @@ def get(self, request): - 커스텀의 경우 사용자의 이전 기록들을 바탕으로 추천합니다. - 추천할 때의 subtodo 는 약 1시간의 작업으로 openAI 의 api를 통해 추천합니다. """ # noqa: E501 + set_sentry_user(request.user) todo_id = request.GET.get("todo_id") + if todo_id is None: + sentry_sdk.capture_message("Todo_id not provided", level="error") + return Response( + {"error": "todo_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) try: todo = Todo.objects.get_with_id(id=todo_id) completion = client.chat.completions.create( @@ -698,6 +780,13 @@ def get(self, request): ], response_format={"type": "json_object"}, ) + sentry_sdk.capture_message( + "SubTodo recommended", level="info", extra={"todo_id": todo.id} + ) + return Response( + json.loads(completion.choices[0].message.content), + status=status.HTTP_200_OK, + ) except Todo.DoesNotExist as e: sentry_sdk.capture_exception(e) @@ -709,10 +798,3 @@ def get(self, request): return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - sentry_sdk.capture_message( - "SubTodo recommended", level="info", extra={"todo_id": todo.id} - ) - return Response( - json.loads(completion.choices[0].message.content), - status=status.HTTP_200_OK, - ) From 2af3264fe228a3e6df136f6395cbbef91897e781 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 3 Oct 2024 16:34:30 +0900 Subject: [PATCH 143/229] =?UTF-8?q?feat=20:=20model=20=EC=97=90=20due=5Fti?= =?UTF-8?q?me=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20todo=EC=9D=98=20start=5Fd?= =?UTF-8?q?ate,=20end=5Fdate=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/admin.py | 16 ++++++-- ...do_date_remove_todo_start_date_and_more.py | 39 +++++++++++++++++++ todos/models.py | 5 ++- 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 todos/migrations/0012_rename_end_date_todo_date_remove_todo_start_date_and_more.py diff --git a/todos/admin.py b/todos/admin.py index 2ec3fc2..93ced30 100644 --- a/todos/admin.py +++ b/todos/admin.py @@ -12,7 +12,8 @@ class TodoAdmin(admin.ModelAdmin): "user_id", "category_id", "order", - "start_date", + "due_time", + "date", "is_completed", ] fieldsets = [ @@ -25,20 +26,28 @@ class TodoAdmin(admin.ModelAdmin): "user_id", "category_id", "order", + "due_time", "is_completed", ] }, ), ( "Date information", - {"fields": ["start_date", "end_date"], "classes": ["collapse"]}, + {"fields": ["date"], "classes": ["collapse"]}, ), ] class SubTodoAdmin(admin.ModelAdmin): readonly_fields = ["id"] - list_display = ["id", "content", "todo", "order", "is_completed"] + list_display = [ + "id", + "content", + "todo", + "due_time", + "order", + "is_completed", + ] fieldsets = [ ( None, @@ -47,6 +56,7 @@ class SubTodoAdmin(admin.ModelAdmin): "id", "content", "todo", + "due_time", "order", "is_completed", ] diff --git a/todos/migrations/0012_rename_end_date_todo_date_remove_todo_start_date_and_more.py b/todos/migrations/0012_rename_end_date_todo_date_remove_todo_start_date_and_more.py new file mode 100644 index 0000000..99ef01e --- /dev/null +++ b/todos/migrations/0012_rename_end_date_todo_date_remove_todo_start_date_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0.6 on 2024-10-03 07:33 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0011_userlastusage'), + ] + + operations = [ + migrations.RenameField( + model_name='todo', + old_name='end_date', + new_name='date', + ), + migrations.RemoveField( + model_name='todo', + name='start_date', + ), + migrations.AddField( + model_name='subtodo', + name='due_time', + field=models.TimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='todo', + name='due_time', + field=models.TimeField(null=True), + ), + migrations.AlterField( + model_name='userlastusage', + name='last_used_at', + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/todos/models.py b/todos/models.py index fd87ff0..d3e5c42 100644 --- a/todos/models.py +++ b/todos/models.py @@ -111,8 +111,8 @@ class Todo(TimeStamp): id = models.AutoField(primary_key=True) content = models.CharField(max_length=255) category_id = models.ForeignKey("Category", on_delete=models.CASCADE) - start_date = models.DateField(null=True) - end_date = models.DateField(null=True) + due_time = models.TimeField(null=True) + date = models.DateField(null=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE) order = models.CharField(max_length=255) is_completed = models.BooleanField(default=False) @@ -129,6 +129,7 @@ class SubTodo(TimeStamp): todo = models.ForeignKey( "Todo", on_delete=models.CASCADE, related_name="subtodos" ) + due_time = models.TimeField(null=True, blank=True) date = models.DateField(null=True) order = models.CharField(max_length=255, null=True) is_completed = models.BooleanField(default=False) From d849863206b82fed633f5aba0ba75f5e98e76757 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sun, 6 Oct 2024 18:29:26 +0900 Subject: [PATCH 144/229] fix : models --- conftest.py | 50 +++++++++++++++++++++++++----------- onestep_be/settings.py | 2 +- todos/models.py | 21 +++------------ todos/tests/test_todo_get.py | 23 +++++------------ 4 files changed, 46 insertions(+), 50 deletions(-) diff --git a/conftest.py b/conftest.py index 8ec1fea..541cccb 100644 --- a/conftest.py +++ b/conftest.py @@ -39,38 +39,58 @@ def authenticated_client(create_user): @pytest.fixture -def create_category(db, create_user): +def create_category( + db, create_user, title="Test Category", color="#FFFFFF", order="0|hzzzzz:" +): category = Category.objects.create( user_id=create_user, - title="Test Category", - color="#FFFFFF", - order="0|hzzzzz:", + title=title, + color=color, + order=order, ) return category @pytest.fixture -def create_todo(db, create_user, create_category): +def create_todo( + db, + create_user, + create_category, + date="2024-08-01", + due_time=None, + content="Test Todo", + order="0|hzzzzz:", + is_completed=False, +): todo = Todo.objects.create( user_id=create_user, - start_date="2024-08-01", - end_date="2024-08-30", + date=date, + due_time=due_time, category_id=create_category, - content="Test Todo", - order="0|hzzzzz:", - is_completed=False, + content=content, + order=order, + is_completed=is_completed, ) return todo @pytest.fixture -def create_subtodo(db, create_user, create_todo): +def create_subtodo( + db, + create_todo, + content="Test SubTodo", + date="2024-08-01", + due_time=None, + order="0|hzzzzz:", + is_completed=False, +): subtodo = SubTodo.objects.create( - content="Test SubTodo", - date="2024-08-01", + content=content, + date=date, + due_time=due_time, todo=create_todo, - order="0|hzzzzz:", - is_completed=False, + order=order, + is_completed=is_completed, ) return subtodo diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 84117b7..79093b7 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -60,7 +60,7 @@ "todos", "feedback", "rest_framework_simplejwt", - 'fcm_django', + "fcm_django", "django_crontab", ] diff --git a/todos/models.py b/todos/models.py index d3e5c42..1bd355b 100644 --- a/todos/models.py +++ b/todos/models.py @@ -41,10 +41,7 @@ def get_inbox(self, user_id): ), ) ) - .filter( - Q(end_date__isnull=True, start_date__isnull=True) - | Q(subtodos_count__gt=0) - ) + .filter(Q(date__isnull=True) | Q(subtodos_count__gt=0)) .prefetch_related( Prefetch( "subtodos", @@ -59,18 +56,8 @@ def get_inbox(self, user_id): def get_daily_with_date(self, user_id, start_date, end_date): return ( Todo.objects.filter(user_id=user_id, deleted_at__isnull=True) - .filter( - ( - Q(start_date__isnull=True) - | Q(start_date__lte=end_date, start_date__gte=start_date) - ) - | ( - Q(end_date__isnull=True) - | Q(end_date__lte=end_date, end_date__gte=start_date) - ) - | (Q(start_date__lte=start_date, end_date__gte=end_date)) - ) - .exclude(start_date__isnull=True, end_date__isnull=True) + .filter(Q(date__gte=start_date, date__lte=end_date)) + .exclude(date__isnull=True) .order_by("order") .prefetch_related( Prefetch( @@ -85,7 +72,7 @@ def get_daily_with_date(self, user_id, start_date, end_date): def get_daily(self, user_id): return ( Todo.objects.filter(user_id=user_id, deleted_at__isnull=True) - .filter(Q(end_date__isnull=False) | Q(start_date__isnull=False)) + .filter(date__isnull=False) .order_by("order") .prefetch_related( Prefetch( diff --git a/todos/tests/test_todo_get.py b/todos/tests/test_todo_get.py index 46f4a7a..0dbdd28 100644 --- a/todos/tests/test_todo_get.py +++ b/todos/tests/test_todo_get.py @@ -19,25 +19,14 @@ @pytest.mark.django_db def test_get_todos( - create_user, create_category, authenticated_client, date, content, order + create_user, + create_todo, + authenticated_client, + order, ): url = reverse("todos") - Todo.objects.create( - user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), - content=content, - category_id=create_category, - order=order(0), - ) - Todo.objects.create( - user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), - content=content, - category_id=create_category, - order=order(1), - ) + create_todo() + create_todo(order="0|j00000") response = authenticated_client.get( url, {"user_id": create_user.id}, format="json" ) From be58c09520dc18b044751302d849fd9f04cc5fdb Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sun, 6 Oct 2024 18:29:54 +0900 Subject: [PATCH 145/229] fix : csrf_trusted_origins --- onestep_be/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 84117b7..981fbc0 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -22,7 +22,7 @@ from accounts.aws import get_secret -CSRF_TRUSTED_ORIGINS = ["https://*.stepby.one"] +CSRF_TRUSTED_ORIGINS = ["https://*.stepby.one", "http://stepby.one"] # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -60,7 +60,7 @@ "todos", "feedback", "rest_framework_simplejwt", - 'fcm_django', + "fcm_django", "django_crontab", ] From ec81b61e687cd03d63cefc7661060bea142507ac Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 7 Oct 2024 18:06:02 +0900 Subject: [PATCH 146/229] fix :CSRF ORIGIN --- onestep_be/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 981fbc0..6dca40f 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -22,7 +22,7 @@ from accounts.aws import get_secret -CSRF_TRUSTED_ORIGINS = ["https://*.stepby.one", "http://stepby.one"] +CSRF_TRUSTED_ORIGINS = ["https://*.stepby.one", "https://stepby.one"] # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent From 1a72880c4d676dbb275ef695bfa689a9349197ea Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 8 Oct 2024 13:44:31 +0900 Subject: [PATCH 147/229] =?UTF-8?q?fix=20:=20id=EB=A1=9C=20=EC=B0=BE?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=20exception=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/models.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/todos/models.py b/todos/models.py index fd87ff0..2f4cff0 100644 --- a/todos/models.py +++ b/todos/models.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models import Count, Prefetch, Q from django.utils import timezone @@ -18,16 +19,27 @@ def delete_many(self, instances): return instances def get_queryset(self): - return super().get_queryset().filter(deleted_at__isnull=True) + super().get_queryset().filter(deleted_at__isnull=True) def get_with_id(self, id): - return self.get_queryset().filter(id=id).first() + instance = self.get_queryset().filter(id=id).first() + if instance is None: + raise ObjectDoesNotExist(f"No object found with id {id}") + return instance def get_with_user_id(self, user_id): - return self.get_queryset().filter(user_id=user_id).order_by("order") + instance = ( + self.get_queryset().filter(user_id=user_id).order_by("order") + ) + if instance is None: + raise ObjectDoesNotExist(f"No object found with user_id {user_id}") + return instance def get_subtodos(self, todo_id): - return self.get_queryset().filter(todo=todo_id).order_by("order") + instance = self.get_queryset().filter(todo=todo_id).order_by("order") + if instance is None: + raise ObjectDoesNotExist(f"No object found with todo_id {todo_id}") + return instance def get_inbox(self, user_id): return ( From fdefb4f304b08077b6bf374919220095b46140c4 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 8 Oct 2024 16:50:02 +0900 Subject: [PATCH 148/229] fix : model subtodos.todo -> subtodos.todo_id --- conftest.py | 5 ++ todos/admin.py | 4 +- ...do_date_remove_todo_start_date_and_more.py | 2 +- .../0013_rename_todo_subtodo_todo_id.py | 18 ++++++ todos/models.py | 2 +- todos/serializers.py | 26 ++++----- todos/tests/test_todo_post.py | 56 +++++-------------- 7 files changed, 51 insertions(+), 62 deletions(-) create mode 100644 todos/migrations/0013_rename_todo_subtodo_todo_id.py diff --git a/conftest.py b/conftest.py index 541cccb..73b4e49 100644 --- a/conftest.py +++ b/conftest.py @@ -135,6 +135,11 @@ def category(): return random.choice(CATEGORY_CHOICES)[0] +@pytest.fixture +def due_time(): + return fake.time(pattern="%H:%M:%S") + + @pytest.fixture def llm(): mock_response = Mock() diff --git a/todos/admin.py b/todos/admin.py index 93ced30..43cfa9e 100644 --- a/todos/admin.py +++ b/todos/admin.py @@ -43,7 +43,7 @@ class SubTodoAdmin(admin.ModelAdmin): list_display = [ "id", "content", - "todo", + "todo_id", "due_time", "order", "is_completed", @@ -55,7 +55,7 @@ class SubTodoAdmin(admin.ModelAdmin): "fields": [ "id", "content", - "todo", + "todo_id", "due_time", "order", "is_completed", diff --git a/todos/migrations/0012_rename_end_date_todo_date_remove_todo_start_date_and_more.py b/todos/migrations/0012_rename_end_date_todo_date_remove_todo_start_date_and_more.py index 99ef01e..038bd11 100644 --- a/todos/migrations/0012_rename_end_date_todo_date_remove_todo_start_date_and_more.py +++ b/todos/migrations/0012_rename_end_date_todo_date_remove_todo_start_date_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-10-03 07:33 +# Generated by Django 5.0.6 on 2024-10-08 06:51 import django.utils.timezone from django.db import migrations, models diff --git a/todos/migrations/0013_rename_todo_subtodo_todo_id.py b/todos/migrations/0013_rename_todo_subtodo_todo_id.py new file mode 100644 index 0000000..b1b2e00 --- /dev/null +++ b/todos/migrations/0013_rename_todo_subtodo_todo_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-10-08 07:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0012_rename_end_date_todo_date_remove_todo_start_date_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='subtodo', + old_name='todo', + new_name='todo_id', + ), + ] diff --git a/todos/models.py b/todos/models.py index 1bd355b..2ca9ce8 100644 --- a/todos/models.py +++ b/todos/models.py @@ -113,7 +113,7 @@ def __str__(self): class SubTodo(TimeStamp): id = models.AutoField(primary_key=True) content = models.CharField(max_length=255) - todo = models.ForeignKey( + todo_id = models.ForeignKey( "Todo", on_delete=models.CASCADE, related_name="subtodos" ) due_time = models.TimeField(null=True, blank=True) diff --git a/todos/serializers.py b/todos/serializers.py index 527d930..a69ea06 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -79,6 +79,7 @@ class SubTodoSerializer(serializers.ModelSerializer): queryset=Todo.objects.all(), required=True ) date = serializers.DateField(required=False, allow_null=True) + due_time = serializers.TimeField(required=False, allow_null=True) order = serializers.CharField(max_length=255) is_completed = serializers.BooleanField(default=False) patch_order = PatchOrderSerializer(required=False) @@ -149,8 +150,8 @@ class Meta: "id", "content", "category_id", - "start_date", - "end_date", + "date", + "due_time", "user_id", "order", "is_completed", @@ -166,8 +167,8 @@ class TodoSerializer(serializers.ModelSerializer): user_id = serializers.PrimaryKeyRelatedField( queryset=User.objects.all(), required=True ) - start_date = serializers.DateField(allow_null=True, required=False) - end_date = serializers.DateField(allow_null=True, required=False) + date = serializers.DateField(allow_null=True, required=False) + due_time = serializers.TimeField(allow_null=True, required=False) order = serializers.CharField(max_length=255) is_completed = serializers.BooleanField(default=False, required=False) patch_order = PatchOrderSerializer(required=False) @@ -178,8 +179,8 @@ class Meta: "id", "content", "category_id", - "start_date", - "end_date", + "date", + "due_time", "user_id", "order", "is_completed", @@ -210,13 +211,6 @@ def validate_patch_order(self, data): def validate(self, data): request = self.context["request"] - start_date = data.get("start_date") - end_date = data.get("end_date") - - if start_date and end_date and start_date > end_date: - raise serializers.ValidationError( - "Start date should be less than end date" - ) if request.method == "PATCH": if not any( @@ -224,14 +218,14 @@ def validate(self, data): for field in [ "content", "category_id", - "start_date", - "end_date", + "date", + "due_time", "is_completed", "order", ] ): raise serializers.ValidationError( - "At least one of content, category_id, start_date, end_date, is_completed must be provided" # noqa : E501 + "At least one of content, category_id, date, due_time, is_completed must be provided" # noqa : E501 ) if data.get("user_id"): raise serializers.ValidationError("User cannot be updated") diff --git a/todos/tests/test_todo_post.py b/todos/tests/test_todo_post.py index 91a5e85..b248376 100644 --- a/todos/tests/test_todo_post.py +++ b/todos/tests/test_todo_post.py @@ -19,13 +19,18 @@ @pytest.mark.django_db def test_create_todo_success( - authenticated_client, create_category, create_user, date, content, order + authenticated_client, + create_category, + create_user, + date, + content, + order, ): url = reverse("todos") data = { "user_id": create_user.id, - "start_date": date, - "end_date": date + timedelta(days=1), + "date": date, + "due_time": None, "content": content, "category_id": create_category.id, "order": order(0), @@ -42,16 +47,16 @@ def test_create_todo_invalid_order( url = reverse("todos") Todo.objects.create( user_id=create_user, - start_date=date, - end_date=date + timedelta(days=1), + date=date, + due_time=None, content=content, category_id=create_category, order=order(1), ) data = { "user_id": create_user.id, - "start_date": date + timedelta(days=2), - "end_date": date + timedelta(days=3), + "date": date + timedelta(days=2), + "due_time": None, "content": content, "category_id": create_category.id, "order": order(0), @@ -60,39 +65,6 @@ def test_create_todo_invalid_order( assert response.status_code == 400 -@pytest.mark.django_db -def test_create_todo_invalid_start_date( - create_user, create_category, authenticated_client, date, content, order -): - url = reverse("todos") - data = { - "user_id": create_user.id, - "start_date": date + timedelta(days=2), - "end_date": date + timedelta(days=1), - "content": content, - "category_id": create_category.id, - "order": order(0), - } - response = authenticated_client.post(url, data, format="json") - assert response.status_code == 400 - - -@pytest.mark.django_db -def test_create_todo_valid_start_date( - create_user, create_category, authenticated_client, date, content, order -): - url = reverse("todos") - data = { - "user_id": create_user.id, - "start_date": date + timedelta(days=2), - "content": content, - "category_id": create_category.id, - "order": order(0), - } - response = authenticated_client.post(url, data, format="json") - assert response.status_code == 201 - - @pytest.mark.django_db def test_create_todo_invalid_category_id( create_user, authenticated_client, date, content, order @@ -100,8 +72,8 @@ def test_create_todo_invalid_category_id( url = reverse("todos") data = { "user_id": create_user.id, - "start_date": date + timedelta(days=1), - "end_date": date + timedelta(days=2), + "date": date, + "due_time": None, "content": content, "category_id": 999, "order": order(0), From 0a12c122dc4ed4335c1cd9bbb5c8b7cfde3bc7ef Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 8 Oct 2024 19:25:59 +0900 Subject: [PATCH 149/229] =?UTF-8?q?feat=20:=20testcode=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/tests/test_todo_patch.py | 73 +++++++++++----------------------- 1 file changed, 24 insertions(+), 49 deletions(-) diff --git a/todos/tests/test_todo_patch.py b/todos/tests/test_todo_patch.py index 4c31924..337aa92 100644 --- a/todos/tests/test_todo_patch.py +++ b/todos/tests/test_todo_patch.py @@ -19,12 +19,18 @@ @pytest.mark.django_db def test_update_todo_success( - authenticated_client, create_category, create_user, date, content, order + authenticated_client, + create_category, + create_user, + date, + content, + order, + due_time, ): todo = Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), + date=date, + due_time=None, content=content, category_id=create_category, order=order(0), @@ -33,13 +39,13 @@ def test_update_todo_success( data = { "todo_id": todo.id, "content": "Updated Todo", - "start_date": date + timedelta(days=1), - "end_date": date + timedelta(days=3), + "date": date + timedelta(days=1), + "due_time": due_time, } response = authenticated_client.patch(url, data, format="json") assert response.status_code == 200 assert response.data["content"] == "Updated Todo" - assert response.data["end_date"] == str(data["end_date"]) + assert response.data["date"] == str(data["date"]) @pytest.mark.django_db @@ -48,16 +54,16 @@ def test_update_todo_success_order( ): todo = Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), + date=date, + due_time=None, content=content, category_id=create_category, order=order(0), ) todo2 = Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), + date=date, + due_time=None, content=content, category_id=create_category, order=order(1), @@ -82,17 +88,15 @@ def test_update_todo_invalid_order( ): todo = Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), - content=content, + date=date, + due_time=None, category_id=create_category, order=order(0), ) todo2 = Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), - content=content, + date=date, + due_time=None, category_id=create_category, order=order(1), ) @@ -100,8 +104,6 @@ def test_update_todo_invalid_order( data = { "todo_id": todo.id, "content": "Updated Todo", - "start_date": date + timedelta(days=1), - "end_date": date + timedelta(days=3), "order": { "prev_id": None, "next_id": todo2.id, @@ -112,37 +114,14 @@ def test_update_todo_invalid_order( assert response.status_code == 400 -@pytest.mark.django_db -def test_update_todo_invalid_start_date( - authenticated_client, create_category, create_user, date, content, order -): - todo = Todo.objects.create( - user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), - content=content, - category_id=create_category, - order=order(0), - ) - url = reverse("todos") - data = { - "todo_id": todo.id, - "content": "Updated Todo", - "start_date": date + timedelta(days=2), - "end_date": date + timedelta(days=1), - } - response = authenticated_client.patch(url, data, format="json") - assert response.status_code == 400 - - @pytest.mark.django_db def test_update_todo_invalid_category_id( authenticated_client, create_category, create_user, date, content, order ): todo = Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), + date=date, + due_time=None, content=content, category_id=create_category, order=order(0), @@ -151,8 +130,6 @@ def test_update_todo_invalid_category_id( data = { "todo_id": todo.id, "content": "Updated Todo", - "start_date": date + timedelta(days=1), - "end_date": date + timedelta(days=3), "category_id": 999, } response = authenticated_client.patch(url, data, format="json") @@ -165,8 +142,8 @@ def test_update_todo_invalid_user_id( ): todo = Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), + date=date, + due_time=None, content=content, category_id=create_category, order=order(0), @@ -175,8 +152,6 @@ def test_update_todo_invalid_user_id( data = { "todo_id": todo.id, "content": "Updated Todo", - "start_date": date + timedelta(days=1), - "end_date": date + timedelta(days=3), "user_id": 999, } response = authenticated_client.patch(url, data, format="json") From 540f191fa4e654dd74312609eb6180f80def3627 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 11 Oct 2024 15:21:52 +0900 Subject: [PATCH 150/229] =?UTF-8?q?fix=20:=20RankedModel=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- onestep_be/settings.py | 1 + requirements.txt | 1 + todos/admin.py | 8 ++--- ...rank_rename_order_subtodo_rank_and_more.py | 28 ++++++++++++++++++ ...tegory_rank_alter_subtodo_rank_and_more.py | 29 +++++++++++++++++++ todos/models.py | 24 +++++++-------- 6 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 todos/migrations/0014_rename_order_category_rank_rename_order_subtodo_rank_and_more.py create mode 100644 todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 79093b7..f3ac213 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -62,6 +62,7 @@ "rest_framework_simplejwt", "fcm_django", "django_crontab", + "django_lexorank", ] MIDDLEWARE = [ diff --git a/requirements.txt b/requirements.txt index 8b4720d..22977ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -109,3 +109,4 @@ protobuf==5.28.2 swapper==1.4.0 django_crontab==0.7.1 Faker==27.0.0 +django-lexorank==0.1.3 diff --git a/todos/admin.py b/todos/admin.py index 43cfa9e..88ca544 100644 --- a/todos/admin.py +++ b/todos/admin.py @@ -11,7 +11,6 @@ class TodoAdmin(admin.ModelAdmin): "content", "user_id", "category_id", - "order", "due_time", "date", "is_completed", @@ -25,7 +24,6 @@ class TodoAdmin(admin.ModelAdmin): "content", "user_id", "category_id", - "order", "due_time", "is_completed", ] @@ -45,7 +43,6 @@ class SubTodoAdmin(admin.ModelAdmin): "content", "todo_id", "due_time", - "order", "is_completed", ] fieldsets = [ @@ -57,7 +54,6 @@ class SubTodoAdmin(admin.ModelAdmin): "content", "todo_id", "due_time", - "order", "is_completed", ] }, @@ -71,11 +67,11 @@ class SubTodoAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin): readonly_fields = ["id"] - list_display = ["id", "title", "user_id", "color", "order"] + list_display = ["id", "title", "user_id", "color"] fieldsets = [ ( None, - {"fields": ["id", "title", "user_id", "color", "order"]}, + {"fields": ["id", "title", "user_id", "color"]}, ), ] diff --git a/todos/migrations/0014_rename_order_category_rank_rename_order_subtodo_rank_and_more.py b/todos/migrations/0014_rename_order_category_rank_rename_order_subtodo_rank_and_more.py new file mode 100644 index 0000000..ace734d --- /dev/null +++ b/todos/migrations/0014_rename_order_category_rank_rename_order_subtodo_rank_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.16 on 2024-10-11 06:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0013_rename_todo_subtodo_todo_id'), + ] + + operations = [ + migrations.RenameField( + model_name='category', + old_name='order', + new_name='rank', + ), + migrations.RenameField( + model_name='subtodo', + old_name='order', + new_name='rank', + ), + migrations.RenameField( + model_name='todo', + old_name='order', + new_name='rank', + ), + ] diff --git a/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py b/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py new file mode 100644 index 0000000..4c8daf4 --- /dev/null +++ b/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.16 on 2024-10-11 06:18 + +from django.db import migrations +import django_lexorank.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0014_rename_order_category_rank_rename_order_subtodo_rank_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='category', + name='rank', + field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), + ), + migrations.AlterField( + model_name='subtodo', + name='rank', + field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), + ), + migrations.AlterField( + model_name='todo', + name='rank', + field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), + ), + ] diff --git a/todos/models.py b/todos/models.py index 2ca9ce8..db9a02c 100644 --- a/todos/models.py +++ b/todos/models.py @@ -1,6 +1,7 @@ from django.db import models from django.db.models import Count, Prefetch, Q from django.utils import timezone +from django_lexorank.models import RankedModel from accounts.models import User @@ -24,10 +25,10 @@ def get_with_id(self, id): return self.get_queryset().filter(id=id).first() def get_with_user_id(self, user_id): - return self.get_queryset().filter(user_id=user_id).order_by("order") + return self.get_queryset().filter(user_id=user_id).order_by("rank") def get_subtodos(self, todo_id): - return self.get_queryset().filter(todo=todo_id).order_by("order") + return self.get_queryset().filter(todo=todo_id).order_by("rank") def get_inbox(self, user_id): return ( @@ -47,10 +48,10 @@ def get_inbox(self, user_id): "subtodos", queryset=SubTodo.objects.filter( deleted_at__isnull=True, date__isnull=True - ).order_by("order"), + ).order_by("rank"), ) ) - .order_by("order") + .order_by("rank") ) def get_daily_with_date(self, user_id, start_date, end_date): @@ -58,13 +59,13 @@ def get_daily_with_date(self, user_id, start_date, end_date): Todo.objects.filter(user_id=user_id, deleted_at__isnull=True) .filter(Q(date__gte=start_date, date__lte=end_date)) .exclude(date__isnull=True) - .order_by("order") + .order_by("rank") .prefetch_related( Prefetch( "subtodos", queryset=SubTodo.objects.filter( deleted_at__isnull=True, date__isnull=False - ).order_by("order"), + ).order_by("rank"), ) ) ) @@ -79,7 +80,7 @@ def get_daily(self, user_id): "subtodos", queryset=SubTodo.objects.filter( deleted_at__isnull=True, date__isnull=False - ).order_by("order"), + ).order_by("rank"), ) ) ) @@ -94,14 +95,13 @@ class Meta: abstract = True -class Todo(TimeStamp): +class Todo(TimeStamp, RankedModel): id = models.AutoField(primary_key=True) content = models.CharField(max_length=255) category_id = models.ForeignKey("Category", on_delete=models.CASCADE) due_time = models.TimeField(null=True) date = models.DateField(null=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE) - order = models.CharField(max_length=255) is_completed = models.BooleanField(default=False) objects = TodosManager() @@ -110,7 +110,7 @@ def __str__(self): return self.content -class SubTodo(TimeStamp): +class SubTodo(TimeStamp, RankedModel): id = models.AutoField(primary_key=True) content = models.CharField(max_length=255) todo_id = models.ForeignKey( @@ -118,7 +118,6 @@ class SubTodo(TimeStamp): ) due_time = models.TimeField(null=True, blank=True) date = models.DateField(null=True) - order = models.CharField(max_length=255, null=True) is_completed = models.BooleanField(default=False) objects = TodosManager() @@ -127,12 +126,11 @@ def __str__(self): return self.content -class Category(TimeStamp): +class Category(TimeStamp, RankedModel): id = models.AutoField(primary_key=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=1) color = models.CharField(max_length=7) title = models.CharField(max_length=100, null=True) - order = models.CharField(max_length=255, null=True) objects = TodosManager() From 7310734514db725fdc78b2082b86523a31412481 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 11 Oct 2024 16:40:05 +0900 Subject: [PATCH 151/229] fix : serailizer, model manager --- todos/serializers.py | 106 ++++++------------------------------------- 1 file changed, 13 insertions(+), 93 deletions(-) diff --git a/todos/serializers.py b/todos/serializers.py index a69ea06..7709d94 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -5,19 +5,17 @@ from rest_framework import serializers from accounts.models import User -from todos.utils import validate_lexo_order from .models import Category, SubTodo, Todo -class PatchOrderSerializer(serializers.Serializer): +class PatchRankSerializer(serializers.Serializer): prev_id = serializers.PrimaryKeyRelatedField( queryset=Todo.objects.all(), required=False, allow_null=True ) next_id = serializers.PrimaryKeyRelatedField( queryset=Todo.objects.all(), required=False, allow_null=True ) - updated_order = serializers.CharField(max_length=255, required=True) class CategorySerializer(serializers.ModelSerializer): @@ -43,33 +41,13 @@ def validate(self, data): request = self.context["request"] if request.method == "PATCH": if not any( - data.get(field) for field in ["color", "title", "order"] + data.get(field) for field in ["color", "title", "rank"] ): raise serializers.ValidationError( - "At least one of color, title, order must be provided" + "At least one of color, title, rank must be provided" ) if data.get("user_id"): raise serializers.ValidationError("User cannot be updated") - return data - - elif request.method == "POST": - user_id = data.get("user_id") - last_category = ( - Category.objects.filter( - user_id=user_id, deleted_at__isnull=True - ) - .order_by("-order") - .first() - ) - if last_category is not None: - last_order = last_category.order - if ( - validate_lexo_order( - prev=last_order, next=None, updated=data["order"] - ) - is False - ): - raise serializers.ValidationError("Order is invalid") return data @@ -79,10 +57,9 @@ class SubTodoSerializer(serializers.ModelSerializer): queryset=Todo.objects.all(), required=True ) date = serializers.DateField(required=False, allow_null=True) - due_time = serializers.TimeField(required=False, allow_null=True) - order = serializers.CharField(max_length=255) + rank = serializers.CharField(max_length=255) is_completed = serializers.BooleanField(default=False) - patch_order = PatchOrderSerializer(required=False) + patch_rank = PatchRankSerializer(required=False) class Meta: model = SubTodo @@ -95,49 +72,18 @@ def validate_todo(self, data): raise serializers.ValidationError("Todo does not exist") return data - def validate_patch_order(self, data): - request = self.context["request"] - if request.method == "PATCH": - updated_order = data.get("updated_order") - prev = data.get("prev_id").order if data.get("prev_id") else None - next = data.get("next_id").order if data.get("next_id") else None - if not validate_lexo_order( - prev=prev, next=next, updated=updated_order - ): - raise serializers.ValidationError("Order is invalid") - return data - def validate(self, data): request = self.context["request"] if request.method == "PATCH": if not any( data.get(field) - for field in ["content", "date", "is_completed", "order"] + for field in ["content", "date", "is_completed", "rank"] ): raise serializers.ValidationError( "At least one of content, date, \ - is_completed, order must be provided" + is_completed, rank must be provided" ) return data - - elif request.method == "POST": - todo_id = data.get("todo").id - last_subtodo = ( - SubTodo.objects.filter( - todo_id=todo_id, deleted_at__isnull=True - ) - .order_by("-order") - .first() - ) - if last_subtodo is not None: - last_order = last_subtodo.order - if ( - validate_lexo_order( - prev=last_order, next=None, updated=data["order"] - ) - is False - ): - raise serializers.ValidationError("Order is invalid") return data @@ -153,7 +99,7 @@ class Meta: "date", "due_time", "user_id", - "order", + "rank", "is_completed", "children", ] @@ -169,9 +115,9 @@ class TodoSerializer(serializers.ModelSerializer): ) date = serializers.DateField(allow_null=True, required=False) due_time = serializers.TimeField(allow_null=True, required=False) - order = serializers.CharField(max_length=255) + rank = serializers.CharField(max_length=255) is_completed = serializers.BooleanField(default=False, required=False) - patch_order = PatchOrderSerializer(required=False) + patch_rank = PatchRankSerializer(required=False) class Meta: model = Todo @@ -182,9 +128,9 @@ class Meta: "date", "due_time", "user_id", - "order", + "rank", "is_completed", - "patch_order", + "patch_rank", ] def validate_category_id(self, data): @@ -197,18 +143,6 @@ def validate_user_id(self, data): raise serializers.ValidationError("User does not exist") return data - def validate_patch_order(self, data): - request = self.context["request"] - if request.method == "PATCH": - updated_order = data.get("updated_order") - prev = data.get("prev_id").order if data.get("prev_id") else None - next = data.get("next_id").order if data.get("next_id") else None - if not validate_lexo_order( - prev=prev, next=next, updated=updated_order - ): - raise serializers.ValidationError("Order is invalid") - return data - def validate(self, data): request = self.context["request"] @@ -219,28 +153,14 @@ def validate(self, data): "content", "category_id", "date", - "due_time", "is_completed", - "order", ] ): raise serializers.ValidationError( - "At least one of content, category_id, date, due_time, is_completed must be provided" # noqa : E501 + "At least one of content, category_id, date, is_completed must be provided" # noqa : E501 ) if data.get("user_id"): raise serializers.ValidationError("User cannot be updated") - elif request.method == "POST": - user_id = data.get("user_id") - last_todo = ( - Todo.objects.filter(user_id=user_id, deleted_at__isnull=True) - .order_by("-order") - .first() - ) - if last_todo and not validate_lexo_order( - prev=last_todo.order, next=None, updated=data["order"] - ): - raise serializers.ValidationError("Order is invalid") - return data def update(self, instance, validated_data): From 0a40aa6c47f50b65b02f1a492a5f6201e601b711 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 11 Oct 2024 16:53:12 +0900 Subject: [PATCH 152/229] fix : sentry sample_rate --- onestep_be/settings.py | 21 ++------------------ todos/views.py | 44 ++++++++++-------------------------------- 2 files changed, 12 insertions(+), 53 deletions(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index f3ac213..4d82a8e 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -248,14 +248,9 @@ sentry_sdk.init( dsn="https://9425334e0e90c405218fa9613cea9a03@o4507736964136960.ingest.us.sentry.io/4507763025117184", - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - traces_sample_rate=1.0, + traces_sample_rate=0.1, release=PROJECT_VERSION, - # Set profiles_sample_rate to 1.0 to profile 100% - # of sampled transactions. - # We recommend adjusting this value in production. - profiles_sample_rate=1.0, + profiles_sample_rate=0.1, integrations=[ DjangoIntegration( transaction_style="url", @@ -270,17 +265,5 @@ ], ) -sentry_sdk.metrics.incr(key="api_calls", value=1) - -sentry_sdk.metrics.distribution( - key="processing_time", - value=0.002, - unit="second", -) -sentry_sdk.metrics.gauge( - key="cpu_usage", - value=94, - unit="percent", -) resend.api_key = SECRETS.get("RESEND") diff --git a/todos/views.py b/todos/views.py index ccb9a4e..fe859b1 100644 --- a/todos/views.py +++ b/todos/views.py @@ -148,13 +148,10 @@ def get(self, request): todos = Todo.objects.get_daily_with_date( user_id=user_id, start_date=start_date, end_date=end_date ) - sentry_sdk.capture_message("Get inbox todos", level="info") else: todos = Todo.objects.get_with_user_id( user_id=user_id ).order_by("order") - - sentry_sdk.capture_message("Get daily todos", level="info") except Todo.DoesNotExist as e: sentry_sdk.capture_exception(e) return Response( @@ -174,11 +171,10 @@ def patch(self, request): - 이 함수는 todo를 수정하는 함수입니다. - 입력 : todo_id, 수정 내용 - 수정 내용은 content, category, start_date, end_date 중 하나 이상이어야 합니다. - - order 의 경우 아래와 같이 제시된다. - "order" : { + - rank 의 경우 아래와 같이 제시된다. + "rank" : { "prev_id" : 1, "next_id" : 3, - "updated_order" : "0|asdf:" } """ # noqa: E501 set_sentry_user(request.user) @@ -196,10 +192,7 @@ def patch(self, request): return Response( {"error": "Todo not found"}, status=status.HTTP_404_NOT_FOUND ) - if "order" in request.data: - nested_order = request.data.get("order") - request.data["order"] = nested_order.get("updated_order") - request.data["patch_order"] = nested_order + # rank 의 경우 업데이트 처리 필요 serializer = TodoSerializer( context={"request": request}, instance=todo, @@ -293,7 +286,7 @@ class SubTodoView(APIView): def post(self, request): """ - 이 함수는 sub todo를 생성하는 함수입니다. - - 입력 : todo, date, content, order + - 입력 : todo, date, content - subtodo 는 리스트에 여러 객체가 들어간 형태를 가집니다. - content 는 암호화 되어야 합니다(// 미정) - date 는 parent의 start_date와 end_date의 사이여야 합니다. @@ -307,8 +300,6 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - sentry_sdk.capture_message("SubTodo created", level="info") - send_push_notification_device( request.auth.get("device"), request.user, @@ -380,12 +371,11 @@ def patch(self, request): """ - 이 함수는 sub todo를 수정하는 함수입니다. - 입력 : subtodo_id, 수정 내용 - - 수정 내용은 content, date, parent_id 중 하나 이상이어야 합니다. - - order 의 경우 아래와 같이 수신됨 - "order" : { + - 수정 내용은 content, date, parent_id, rank 중 하나 이상이어야 합니다. + - rank 의 경우 아래와 같이 수신됨 + "rank" : { "prev_id" : 1, "next_id" : 3, - "updated_order" : "0|asdf:" } """ set_sentry_user(request.user) @@ -407,10 +397,7 @@ def patch(self, request): {"error": "SubTodo not found"}, status=status.HTTP_404_NOT_FOUND, ) - if "order" in request.data: - nested_order = request.data.get("order") - request.data["order"] = nested_order.get("updated_order") - request.data["patch_order"] = nested_order + # rank 관련 로직 필요 serializer = SubTodoSerializer( context={"request": request}, instance=sub_todo, @@ -421,8 +408,6 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - sentry_sdk.capture_message("SubTodo updated", level="info") - send_push_notification_device( request.auth.get("device"), request.user, @@ -473,7 +458,6 @@ def delete(self, request): try: sub_todo = SubTodo.objects.get_with_id(id=subtodo_id) SubTodo.objects.delete_instance(sub_todo) - sentry_sdk.capture_message("SubTodo deleted", level="info") send_push_notification_device( request.auth.get("device"), @@ -564,11 +548,10 @@ def patch(self, request): - 이 함수는 category를 수정하는 함수입니다. - 입력 : category_id, 수정 내용 - 수정 내용은 title, color 중 하나 이상이어야 합니다. - - order 의 경우 아래와 같이 수신됨 - "order" : { + - rank 의 경우 아래와 같이 수신됨 + "rank" : { "prev_id" : 1, "next_id" : 3, - "updated_order" : "0|asdf:" } """ set_sentry_user(request.user) @@ -602,12 +585,6 @@ def patch(self, request): return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - - if "order" in request.data: - nested_order = request.data.get("order") - request.data["order"] = nested_order.get("updated_order") - request.data["patch_order"] = nested_order - serializer = CategorySerializer( context={"request": request}, instance=category, @@ -617,7 +594,6 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - sentry_sdk.capture_message("Category updated", level="info") send_push_notification_device( request.auth.get("device"), request.user, From 93aac84b2463002c767af505ee54c22ef4485dcb Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 11 Oct 2024 17:25:54 +0900 Subject: [PATCH 153/229] fix: testcode_todopost --- todos/tests/test_todo_post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todos/tests/test_todo_post.py b/todos/tests/test_todo_post.py index b248376..5ec7776 100644 --- a/todos/tests/test_todo_post.py +++ b/todos/tests/test_todo_post.py @@ -33,7 +33,7 @@ def test_create_todo_success( "due_time": None, "content": content, "category_id": create_category.id, - "order": order(0), + "rank": order(0), } response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 From d483453b91b82a515000dcd76f85f3b593283f9a Mon Sep 17 00:00:00 2001 From: earthyoung Date: Fri, 11 Oct 2024 19:17:23 +0900 Subject: [PATCH 154/229] =?UTF-8?q?fix:=20mysql=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- onestep_be/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 79093b7..aab44cc 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -19,6 +19,7 @@ import sentry_sdk from openai import OpenAI from sentry_sdk.integrations.django import DjangoIntegration +import pymysql from accounts.aws import get_secret @@ -28,6 +29,8 @@ SKIP_AUTHENTICATION = False +pymysql.install_as_MySQLdb() + # Load all secret variables stored in AWS secret manager SECRETS = eval(get_secret()) From e38660654ee573b7ef73d2719e0bfcd15c980a77 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 11 Oct 2024 20:06:39 +0900 Subject: [PATCH 155/229] fix : category testcode --- todos/tests/test_category_post.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/todos/tests/test_category_post.py b/todos/tests/test_category_post.py index 8c04f21..137ffca 100644 --- a/todos/tests/test_category_post.py +++ b/todos/tests/test_category_post.py @@ -1,8 +1,6 @@ import pytest from django.urls import reverse -from todos.models import Category - """ ====================================== # Todo Post checklist # @@ -22,29 +20,7 @@ def test_create_category_success( "user_id": create_user.id, "title": content, "color": color, - "order": order(0), } response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 assert "id" in response.data - - -@pytest.mark.django_db -def test_create_category_invalid_order( - create_user, authenticated_client, content, order, color -): - url = reverse("category") - Category.objects.create( - user_id=create_user, - title=content, - color=color, - order=order(0), - ) - data = { - "user_id": create_user.id, - "title": content, - "color": color, - "order": order(0), - } - response = authenticated_client.post(url, data, format="json") - assert response.status_code == 400 From 13c1c2d04fd1c4d1d954b264328d00d9e65525d5 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 14:53:11 +0900 Subject: [PATCH 156/229] fix : complete test all category --- todos/models.py | 4 + todos/tests/test_category_delete.py | 5 +- todos/tests/test_category_get.py | 23 ++---- todos/tests/test_category_patch.py | 6 +- todos/views.py | 111 ++++++++++++++-------------- 5 files changed, 72 insertions(+), 77 deletions(-) diff --git a/todos/models.py b/todos/models.py index db9a02c..4798197 100644 --- a/todos/models.py +++ b/todos/models.py @@ -1,6 +1,7 @@ from django.db import models from django.db.models import Count, Prefetch, Q from django.utils import timezone +from django_lexorank.fields import RankField from django_lexorank.models import RankedModel from accounts.models import User @@ -103,6 +104,7 @@ class Todo(TimeStamp, RankedModel): date = models.DateField(null=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE) is_completed = models.BooleanField(default=False) + rank = RankField(insert_to_bottom=True) objects = TodosManager() @@ -119,6 +121,7 @@ class SubTodo(TimeStamp, RankedModel): due_time = models.TimeField(null=True, blank=True) date = models.DateField(null=True) is_completed = models.BooleanField(default=False) + rank = RankField(insert_to_bottom=True) objects = TodosManager() @@ -131,6 +134,7 @@ class Category(TimeStamp, RankedModel): user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=1) color = models.CharField(max_length=7) title = models.CharField(max_length=100, null=True) + rank = RankField(insert_to_bottom=True) objects = TodosManager() diff --git a/todos/tests/test_category_delete.py b/todos/tests/test_category_delete.py index 1535417..f1b611a 100644 --- a/todos/tests/test_category_delete.py +++ b/todos/tests/test_category_delete.py @@ -14,13 +14,12 @@ @pytest.mark.django_db def test_delete_category_success( - create_user, authenticated_client, content, order, color + create_user, authenticated_client, content, color ): category = Category.objects.create( user_id=create_user, title=content, color=color, - order=order(0), ) url = reverse("category") data = {"category_id": category.id} @@ -37,7 +36,7 @@ def test_delete_category_success( @pytest.mark.django_db def test_delete_category_invalid_id( - authenticated_client, content, order, color + authenticated_client, ): url = reverse("category") data = {"category_id": 999} diff --git a/todos/tests/test_category_get.py b/todos/tests/test_category_get.py index 808edec..8763b6e 100644 --- a/todos/tests/test_category_get.py +++ b/todos/tests/test_category_get.py @@ -13,21 +13,17 @@ @pytest.mark.django_db -def test_get_category( - create_user, authenticated_client, content, order, color -): +def test_get_category(create_user, authenticated_client, content, color): url = reverse("category") Category.objects.create( user_id=create_user, title=content, color=color, - order=order(0), ) Category.objects.create( user_id=create_user, title=content, color=color, - order=order(1), ) response = authenticated_client.get( url, {"user_id": create_user.id}, format="json" @@ -38,31 +34,28 @@ def test_get_category( @pytest.mark.django_db def test_get_category_ordering( - create_user, authenticated_client, content, order, color + create_user, authenticated_client, content, color ): url = reverse("category") Category.objects.create( user_id=create_user, - title=content, + title="1", color=color, - order=order(2), ) Category.objects.create( user_id=create_user, - title=content, + title="2", color=color, - order=order(1), ) Category.objects.create( user_id=create_user, - title=content, + title="3", color=color, - order=order(0), ) response = authenticated_client.get( url, {"user_id": create_user.id}, format="json" ) assert response.status_code == 200 - assert response.data[0]["order"] == order(0) - assert response.data[1]["order"] == order(1) - assert response.data[2]["order"] == order(2) + assert response.data[0]["title"] == "1" + assert response.data[1]["title"] == "2" + assert response.data[2]["title"] == "3" diff --git a/todos/tests/test_category_patch.py b/todos/tests/test_category_patch.py index 60f8b79..e861399 100644 --- a/todos/tests/test_category_patch.py +++ b/todos/tests/test_category_patch.py @@ -16,13 +16,12 @@ @pytest.mark.django_db def test_update_category_success( - create_user, authenticated_client, content, order, color + create_user, authenticated_client, content, color ): category = Category.objects.create( user_id=create_user, title=content, color=color, - order=order(0), ) url = reverse("category") # URL name for the categoryView patch method data = { @@ -36,13 +35,12 @@ def test_update_category_success( @pytest.mark.django_db def test_update_category_invalid_user_id( - create_user, authenticated_client, content, order, color + create_user, authenticated_client, content, color ): category = Category.objects.create( user_id=create_user, title=content, color=color, - order=order(0), ) url = reverse("category") data = {"category_id": category.id, "user_id": 999} diff --git a/todos/views.py b/todos/views.py index fe859b1..1e3cd2e 100644 --- a/todos/views.py +++ b/todos/views.py @@ -10,7 +10,8 @@ from rest_framework.views import APIView from onestep_be.settings import client -from todos.firebase_messaging import send_push_notification_device + +# from todos.firebase_messaging import send_push_notification_device from todos.models import Category, SubTodo, Todo, UserLastUsage from todos.serializers import ( CategorySerializer, @@ -69,12 +70,12 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( - request.auth.get("device"), - request.user, - TODO_FCM_MESSAGE_TITLE, - TODO_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # TODO_FCM_MESSAGE_TITLE, + # TODO_FCM_MESSAGE_BODY, + # ) return Response( serializer.data, status=status.HTTP_201_CREATED ) @@ -202,12 +203,12 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( - request.auth.get("device"), - request.user, - TODO_FCM_MESSAGE_TITLE, - TODO_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # TODO_FCM_MESSAGE_TITLE, + # TODO_FCM_MESSAGE_BODY, + # ) return Response(serializer.data, status=status.HTTP_200_OK) else: sentry_validation_error( @@ -252,12 +253,12 @@ def delete(self, request): subtodos = SubTodo.objects.get_subtodos(todo.id) SubTodo.objects.delete_many(subtodos) Todo.objects.delete_instance(todo) - send_push_notification_device( - request.auth.get("device"), - request.user, - TODO_FCM_MESSAGE_TITLE, - TODO_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # TODO_FCM_MESSAGE_TITLE, + # TODO_FCM_MESSAGE_BODY, + # ) return Response( {"todo_id": todo.id, "message": "Todo deleted successfully"}, status=status.HTTP_200_OK, @@ -300,12 +301,12 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( - request.auth.get("device"), - request.user, - SUBTODO_FCM_MESSAGE_TITLE, - SUBTODO_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # SUBTODO_FCM_MESSAGE_TITLE, + # SUBTODO_FCM_MESSAGE_BODY, + # ) return Response(serializer.data, status=status.HTTP_201_CREATED) else: @@ -408,12 +409,12 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( - request.auth.get("device"), - request.user, - SUBTODO_FCM_MESSAGE_TITLE, - SUBTODO_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # SUBTODO_FCM_MESSAGE_TITLE, + # SUBTODO_FCM_MESSAGE_BODY, + # ) return Response(serializer.data, status=status.HTTP_200_OK) else: @@ -459,12 +460,12 @@ def delete(self, request): sub_todo = SubTodo.objects.get_with_id(id=subtodo_id) SubTodo.objects.delete_instance(sub_todo) - send_push_notification_device( - request.auth.get("device"), - request.user, - SUBTODO_FCM_MESSAGE_TITLE, - SUBTODO_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # SUBTODO_FCM_MESSAGE_TITLE, + # SUBTODO_FCM_MESSAGE_BODY, + # ) return Response( { "subtodo_id": sub_todo.id, @@ -512,12 +513,12 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( - request.auth.get("device"), - request.user, - CATEGORY_FCM_MESSAGE_TITLE, - CATEGORY_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # CATEGORY_FCM_MESSAGE_TITLE, + # CATEGORY_FCM_MESSAGE_BODY, + # ) return Response( serializer.data, status=status.HTTP_201_CREATED @@ -594,12 +595,12 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( - request.auth.get("device"), - request.user, - CATEGORY_FCM_MESSAGE_TITLE, - CATEGORY_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # CATEGORY_FCM_MESSAGE_TITLE, + # CATEGORY_FCM_MESSAGE_BODY, + # ) return Response(serializer.data, status=status.HTTP_200_OK) else: @@ -690,12 +691,12 @@ def delete(self, request): ) category = Category.objects.get_with_id(id=category_id) Category.objects.delete_instance(category) - send_push_notification_device( - request.auth.get("device"), - request.user, - CATEGORY_FCM_MESSAGE_TITLE, - CATEGORY_FCM_MESSAGE_BODY, - ) + # send_push_notification_device( + # request.auth.get("device"), + # request.user, + # CATEGORY_FCM_MESSAGE_TITLE, + # CATEGORY_FCM_MESSAGE_BODY, + # ) return Response( { "category_id": category.id, From 3ae0aba5ad5dae14aade8f4e4410ae14843e23b3 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 15:26:34 +0900 Subject: [PATCH 157/229] fix : compelte all test subtodos --- conftest.py | 10 +++--- todos/models.py | 2 +- todos/serializers.py | 7 ++-- todos/tests/test_subtodo_delete.py | 7 ++-- todos/tests/test_subtodo_get.py | 42 +++++++++------------- todos/tests/test_subtodo_patch.py | 25 ++++++------- todos/tests/test_subtodo_post.py | 39 +++------------------ todos/tests/test_todo_patch.py | 56 +++++++----------------------- todos/tests/test_todo_post.py | 34 +----------------- 9 files changed, 56 insertions(+), 166 deletions(-) diff --git a/conftest.py b/conftest.py index 73b4e49..a84f73d 100644 --- a/conftest.py +++ b/conftest.py @@ -40,13 +40,15 @@ def authenticated_client(create_user): @pytest.fixture def create_category( - db, create_user, title="Test Category", color="#FFFFFF", order="0|hzzzzz:" + db, + create_user, + title="Test Category", + color="#FFFFFF", ): category = Category.objects.create( user_id=create_user, title=title, color=color, - order=order, ) return category @@ -59,7 +61,6 @@ def create_todo( date="2024-08-01", due_time=None, content="Test Todo", - order="0|hzzzzz:", is_completed=False, ): todo = Todo.objects.create( @@ -68,7 +69,6 @@ def create_todo( due_time=due_time, category_id=create_category, content=content, - order=order, is_completed=is_completed, ) return todo @@ -81,7 +81,6 @@ def create_subtodo( content="Test SubTodo", date="2024-08-01", due_time=None, - order="0|hzzzzz:", is_completed=False, ): subtodo = SubTodo.objects.create( @@ -89,7 +88,6 @@ def create_subtodo( date=date, due_time=due_time, todo=create_todo, - order=order, is_completed=is_completed, ) return subtodo diff --git a/todos/models.py b/todos/models.py index 4798197..e5e61f7 100644 --- a/todos/models.py +++ b/todos/models.py @@ -29,7 +29,7 @@ def get_with_user_id(self, user_id): return self.get_queryset().filter(user_id=user_id).order_by("rank") def get_subtodos(self, todo_id): - return self.get_queryset().filter(todo=todo_id).order_by("rank") + return self.get_queryset().filter(todo_id=todo_id).order_by("rank") def get_inbox(self, user_id): return ( diff --git a/todos/serializers.py b/todos/serializers.py index 7709d94..999414e 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -53,11 +53,11 @@ def validate(self, data): class SubTodoSerializer(serializers.ModelSerializer): content = serializers.CharField(max_length=255) - todo = serializers.PrimaryKeyRelatedField( + todo_id = serializers.PrimaryKeyRelatedField( queryset=Todo.objects.all(), required=True ) date = serializers.DateField(required=False, allow_null=True) - rank = serializers.CharField(max_length=255) + rank = serializers.CharField(max_length=255, required=False) is_completed = serializers.BooleanField(default=False) patch_rank = PatchRankSerializer(required=False) @@ -115,7 +115,7 @@ class TodoSerializer(serializers.ModelSerializer): ) date = serializers.DateField(allow_null=True, required=False) due_time = serializers.TimeField(allow_null=True, required=False) - rank = serializers.CharField(max_length=255) + rank = serializers.CharField(max_length=255, required=False) is_completed = serializers.BooleanField(default=False, required=False) patch_rank = PatchRankSerializer(required=False) @@ -154,6 +154,7 @@ def validate(self, data): "category_id", "date", "is_completed", + "rank", ] ): raise serializers.ValidationError( diff --git a/todos/tests/test_subtodo_delete.py b/todos/tests/test_subtodo_delete.py index 61688a8..a027c90 100644 --- a/todos/tests/test_subtodo_delete.py +++ b/todos/tests/test_subtodo_delete.py @@ -14,13 +14,12 @@ @pytest.mark.django_db def test_delete_subtodo_success( - create_todo, authenticated_client, content, date, order + create_todo, authenticated_client, content, date ): subtodo = SubTodo.objects.create( content=content, date=date, - todo=create_todo, - order=order(0), + todo_id=create_todo, is_completed=False, ) url = reverse("subtodos") @@ -37,7 +36,7 @@ def test_delete_subtodo_success( @pytest.mark.django_db -def test_delete_subtodo_invalid_id(authenticated_client, content, date, order): +def test_delete_subtodo_invalid_id(authenticated_client): url = reverse("subtodos") data = { "subtodo_id": 999 # Invalid subtodo id diff --git a/todos/tests/test_subtodo_get.py b/todos/tests/test_subtodo_get.py index 34dcc9a..e219207 100644 --- a/todos/tests/test_subtodo_get.py +++ b/todos/tests/test_subtodo_get.py @@ -14,20 +14,18 @@ @pytest.mark.django_db -def test_get_subtodos(create_todo, authenticated_client, content, date, order): +def test_get_subtodos(create_todo, authenticated_client, content, date): url = reverse("subtodos") SubTodo.objects.create( content=content, date=date, - todo=create_todo, - order=order(0), + todo_id=create_todo, is_completed=False, ) SubTodo.objects.create( content=content, date=date, - todo=create_todo, - order=order(1), + todo_id=create_todo, is_completed=False, ) response = authenticated_client.get( @@ -39,63 +37,57 @@ def test_get_subtodos(create_todo, authenticated_client, content, date, order): @pytest.mark.django_db def test_get_subtodos_ordering( - create_todo, authenticated_client, content, date, order + create_todo, authenticated_client, content, date ): url = reverse("subtodos") SubTodo.objects.create( - content=content, + content="1", date=date, - todo=create_todo, - order=order(2), + todo_id=create_todo, is_completed=False, ) SubTodo.objects.create( - content=content, + content="2", date=date, - todo=create_todo, - order=order(1), + todo_id=create_todo, is_completed=False, ) SubTodo.objects.create( - content=content, + content="3", date=date, - todo=create_todo, - order=order(0), + todo_id=create_todo, is_completed=False, ) response = authenticated_client.get( url, {"todo_id": create_todo.id}, format="json" ) assert response.status_code == 200 - assert response.data[0]["order"] == order(0) - assert response.data[1]["order"] == order(1) - assert response.data[2]["order"] == order(2) + assert response.data[0]["content"] == "1" + assert response.data[1]["content"] == "2" + assert response.data[2]["content"] == "3" @pytest.mark.django_db def test_get_subtodos_between_dates( - create_todo, authenticated_client, content, date, order + create_todo, authenticated_client, content ): url = reverse("subtodos") SubTodo.objects.create( content=content, date="2024-08-02", - todo=create_todo, - order=order(0), + todo_id=create_todo, is_completed=False, ) SubTodo.objects.create( content=content, date="2024-08-04", - todo=create_todo, - order=order(1), + todo_id=create_todo, is_completed=False, ) SubTodo.objects.create( content=content, date="2024-08-06", - todo=create_todo, - order=order(2), + todo_id=create_todo, is_completed=False, ) response = authenticated_client.get( diff --git a/todos/tests/test_subtodo_patch.py b/todos/tests/test_subtodo_patch.py index 3eac9e3..011661f 100644 --- a/todos/tests/test_subtodo_patch.py +++ b/todos/tests/test_subtodo_patch.py @@ -17,13 +17,12 @@ @pytest.mark.django_db def test_update_subtodo_success( - create_todo, authenticated_client, content, date, order + create_todo, authenticated_client, content, date ): subtodo = SubTodo.objects.create( content=content, date=date, - todo=create_todo, - order=order(0), + todo_id=create_todo, is_completed=False, ) url = reverse("subtodos") # URL name for the SubTodoView patch method @@ -39,21 +38,19 @@ def test_update_subtodo_success( @pytest.mark.django_db -def test_update_subtodo_invalid_order( - create_todo, authenticated_client, content, date, order +def test_update_subtodo_order( + create_todo, authenticated_client, content, date ): subtodo = SubTodo.objects.create( content=content, date=date, - todo=create_todo, - order=order(0), + todo_id=create_todo, is_completed=False, ) subtodo2 = SubTodo.objects.create( content=content, date=date, - todo=create_todo, - order=order(1), + todo_id=create_todo, is_completed=False, ) url = reverse("subtodos") @@ -61,10 +58,9 @@ def test_update_subtodo_invalid_order( "subtodo_id": subtodo.id, "content": "Updated SubTodo", "date": "2024-08-03", - "order": { + "rank": { "prev_id": None, "next_id": subtodo2.id, - "updated_order": order(2), }, } response = authenticated_client.patch(url, data, format="json") @@ -73,13 +69,12 @@ def test_update_subtodo_invalid_order( @pytest.mark.django_db def test_update_subtodo_invalid_todo_id( - create_todo, authenticated_client, content, date, order + create_todo, authenticated_client, content, date ): subtodo = SubTodo.objects.create( content=content, date=date, - todo=create_todo, - order=order(0), + todo_id=create_todo, is_completed=False, ) url = reverse("subtodos") @@ -87,7 +82,7 @@ def test_update_subtodo_invalid_todo_id( "subtodo_id": subtodo.id, "content": "Updated SubTodo", "date": "2024-08-03", - "todo": 999, # Invalid todo id + "todo_id": 999, # Invalid todo id } response = authenticated_client.patch(url, data, format="json") assert response.status_code == 400 diff --git a/todos/tests/test_subtodo_post.py b/todos/tests/test_subtodo_post.py index 538ce1d..5d53996 100644 --- a/todos/tests/test_subtodo_post.py +++ b/todos/tests/test_subtodo_post.py @@ -1,8 +1,6 @@ import pytest from django.urls import reverse -from todos.models import SubTodo - """ ====================================== # SubTodo Post checklist # @@ -17,15 +15,14 @@ @pytest.mark.django_db def test_create_subtodo_success( - create_todo, authenticated_client, content, date, order + create_todo, authenticated_client, content, date ): url = reverse("subtodos") data = [ { "content": content, "date": date, - "todo": create_todo.id, - "order": order(0), + "todo_id": create_todo.id, "is_completed": False, } ] @@ -36,41 +33,13 @@ def test_create_subtodo_success( @pytest.mark.django_db -def test_create_subtodo_invalid_order( - create_todo, authenticated_client, content, date, order -): - SubTodo.objects.create( - content=content, - date=date, - todo=create_todo, - order=order(0), - is_completed=False, - ) - url = reverse("subtodos") - data = [ - { - "content": content, - "date": date, - "todo": create_todo.id, - "order": order(0), - "is_completed": False, - } - ] - response = authenticated_client.post(url, data, format="json") - assert response.status_code == 400 - - -@pytest.mark.django_db -def test_create_subtodo_invalid_todo_id( - authenticated_client, content, date, order -): +def test_create_subtodo_invalid_todo_id(authenticated_client, content, date): url = reverse("subtodos") data = [ { "content": content, "date": date, - "todo": 999, # Invalid todo id - "order": order(0), + "todo_id": 999, # Invalid todo id "is_completed": False, } ] diff --git a/todos/tests/test_todo_patch.py b/todos/tests/test_todo_patch.py index 337aa92..2585030 100644 --- a/todos/tests/test_todo_patch.py +++ b/todos/tests/test_todo_patch.py @@ -24,7 +24,6 @@ def test_update_todo_success( create_user, date, content, - order, due_time, ): todo = Todo.objects.create( @@ -33,7 +32,6 @@ def test_update_todo_success( due_time=None, content=content, category_id=create_category, - order=order(0), ) url = reverse("todos") data = { @@ -50,7 +48,7 @@ def test_update_todo_success( @pytest.mark.django_db def test_update_todo_success_order( - create_user, create_category, authenticated_client, date, content, order + create_user, create_category, authenticated_client, date, content ): todo = Todo.objects.create( user_id=create_user, @@ -58,7 +56,6 @@ def test_update_todo_success_order( due_time=None, content=content, category_id=create_category, - order=order(0), ) todo2 = Todo.objects.create( user_id=create_user, @@ -66,57 +63,26 @@ def test_update_todo_success_order( due_time=None, content=content, category_id=create_category, - order=order(1), ) url = reverse("todos") data = { "todo_id": todo.id, - "order": { + "rank": { "prev_id": todo2.id, "next_id": None, - "updated_order": order(2), }, } response = authenticated_client.patch(url, data, format="json") assert response.status_code == 200 - assert response.data["order"] == order(2) - - -@pytest.mark.django_db -def test_update_todo_invalid_order( - authenticated_client, create_category, create_user, date, content, order -): - todo = Todo.objects.create( - user_id=create_user, - date=date, - due_time=None, - category_id=create_category, - order=order(0), - ) - todo2 = Todo.objects.create( - user_id=create_user, - date=date, - due_time=None, - category_id=create_category, - order=order(1), - ) - url = reverse("todos") - data = { - "todo_id": todo.id, - "content": "Updated Todo", - "order": { - "prev_id": None, - "next_id": todo2.id, - "updated_order": order(2), - }, - } - response = authenticated_client.patch(url, data, format="json") - assert response.status_code == 400 @pytest.mark.django_db def test_update_todo_invalid_category_id( - authenticated_client, create_category, create_user, date, content, order + authenticated_client, + create_category, + create_user, + date, + content, ): todo = Todo.objects.create( user_id=create_user, @@ -124,7 +90,6 @@ def test_update_todo_invalid_category_id( due_time=None, content=content, category_id=create_category, - order=order(0), ) url = reverse("todos") data = { @@ -138,7 +103,11 @@ def test_update_todo_invalid_category_id( @pytest.mark.django_db def test_update_todo_invalid_user_id( - authenticated_client, create_category, create_user, date, content, order + authenticated_client, + create_category, + create_user, + date, + content, ): todo = Todo.objects.create( user_id=create_user, @@ -146,7 +115,6 @@ def test_update_todo_invalid_user_id( due_time=None, content=content, category_id=create_category, - order=order(0), ) url = reverse("todos") data = { diff --git a/todos/tests/test_todo_post.py b/todos/tests/test_todo_post.py index 5ec7776..4db5afd 100644 --- a/todos/tests/test_todo_post.py +++ b/todos/tests/test_todo_post.py @@ -1,10 +1,6 @@ -from datetime import timedelta - import pytest from django.urls import reverse -from todos.models import Todo - """ ====================================== # SubTodo Post checklist # @@ -24,7 +20,6 @@ def test_create_todo_success( create_user, date, content, - order, ): url = reverse("todos") data = { @@ -33,41 +28,15 @@ def test_create_todo_success( "due_time": None, "content": content, "category_id": create_category.id, - "rank": order(0), } response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 assert "id" in response.data -@pytest.mark.django_db -def test_create_todo_invalid_order( - create_user, create_category, authenticated_client, date, content, order -): - url = reverse("todos") - Todo.objects.create( - user_id=create_user, - date=date, - due_time=None, - content=content, - category_id=create_category, - order=order(1), - ) - data = { - "user_id": create_user.id, - "date": date + timedelta(days=2), - "due_time": None, - "content": content, - "category_id": create_category.id, - "order": order(0), - } - response = authenticated_client.post(url, data, format="json") - assert response.status_code == 400 - - @pytest.mark.django_db def test_create_todo_invalid_category_id( - create_user, authenticated_client, date, content, order + create_user, authenticated_client, date, content ): url = reverse("todos") data = { @@ -76,7 +45,6 @@ def test_create_todo_invalid_category_id( "due_time": None, "content": content, "category_id": 999, - "order": order(0), } response = authenticated_client.post(url, data, format="json") assert response.status_code == 400 From ab74d1c65b26c82bc90ece5211a1d7fa9939fa0c Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 15:33:12 +0900 Subject: [PATCH 158/229] fix: complete all test todos --- todos/tests/test_todo_delete.py | 11 ++-- todos/tests/test_todo_get.py | 112 +++++--------------------------- todos/views.py | 2 +- 3 files changed, 22 insertions(+), 103 deletions(-) diff --git a/todos/tests/test_todo_delete.py b/todos/tests/test_todo_delete.py index 2ee377d..a6c97d3 100644 --- a/todos/tests/test_todo_delete.py +++ b/todos/tests/test_todo_delete.py @@ -1,5 +1,3 @@ -from datetime import timedelta - import pytest from django.urls import reverse @@ -16,15 +14,14 @@ @pytest.mark.django_db def test_delete_todo_success( - authenticated_client, create_category, create_user, date, content, order + authenticated_client, create_category, create_user, date, content ): todo = Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), + date=date, + due_time=None, content=content, category_id=create_category, - order=order(0), ) url = reverse("todos") data = {"todo_id": todo.id} @@ -40,7 +37,7 @@ def test_delete_todo_success( @pytest.mark.django_db -def test_delete_todo_invalid_id(authenticated_client, order): +def test_delete_todo_invalid_id(authenticated_client): url = reverse("todos") data = {"todo_id": 999} response = authenticated_client.delete(url, data, format="json") diff --git a/todos/tests/test_todo_get.py b/todos/tests/test_todo_get.py index 0dbdd28..6cb89da 100644 --- a/todos/tests/test_todo_get.py +++ b/todos/tests/test_todo_get.py @@ -20,138 +20,60 @@ @pytest.mark.django_db def test_get_todos( create_user, - create_todo, + date, + content, + create_category, authenticated_client, - order, -): - url = reverse("todos") - create_todo() - create_todo(order="0|j00000") - response = authenticated_client.get( - url, {"user_id": create_user.id}, format="json" - ) - assert response.status_code == 200 - assert len(response.data) == 2 - - -@pytest.mark.django_db -def test_get_todos_ordering( - create_user, create_category, authenticated_client, date, content, order ): url = reverse("todos") Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), + date=date, + due_time=None, content=content, category_id=create_category, - order=order(2), ) Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), + date=date, + due_time=None, content=content, category_id=create_category, - order=order(1), - ) - Todo.objects.create( - user_id=create_user, - start_date=date + timedelta(days=1), - end_date=date + timedelta(days=2), - content=content, - category_id=create_category, - order=order(0), ) response = authenticated_client.get( url, {"user_id": create_user.id}, format="json" ) assert response.status_code == 200 - - assert response.data[0]["order"] == order(0) - assert response.data[1]["order"] == order(1) - assert response.data[2]["order"] == order(2) - - -@pytest.mark.django_db -def test_get_todos_between_dates( - create_user, create_category, authenticated_client, date, content, order -): - url = reverse("todos") - Todo.objects.create( - user_id=create_user, - start_date=date + timedelta(days=2), - end_date=date + timedelta(days=4), - content=content, - category_id=create_category, - order=order(0), - ) - Todo.objects.create( - user_id=create_user, - start_date=date + timedelta(days=4), - end_date=date + timedelta(days=6), - content=content, - category_id=create_category, - order=order(1), - ) - Todo.objects.create( - user_id=create_user, - start_date=date + timedelta(days=6), - end_date=date + timedelta(days=8), - content=content, - category_id=create_category, - order=order(2), - ) - response = authenticated_client.get( - url, - { - "user_id": create_user.id, - "start_date": date + timedelta(days=3), - "end_date": date + timedelta(days=6), - }, - format="json", - ) - assert response.status_code == 200 - assert len(response.data) == 3 + assert len(response.data) == 2 @pytest.mark.django_db -def test_get_todos_between_dates2( - create_user, create_category, authenticated_client, date, content, order +def test_get_todos_ordering( + create_user, create_category, authenticated_client, date, content ): url = reverse("todos") Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=2), - end_date=date + timedelta(days=4), + date=date, + due_time=None, content=content, category_id=create_category, - order=order(0), ) Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=4), - end_date=date + timedelta(days=6), + date=date + timedelta(days=1), + due_time=None, content=content, category_id=create_category, - order=order(1), ) Todo.objects.create( user_id=create_user, - start_date=date + timedelta(days=7), - end_date=date + timedelta(days=8), + date=date, + due_time=None, content=content, category_id=create_category, - order=order(2), ) response = authenticated_client.get( - url, - { - "user_id": create_user.id, - "start_date": date + timedelta(days=3), - "end_date": date + timedelta(days=6), - }, - format="json", + url, {"user_id": create_user.id}, format="json" ) assert response.status_code == 200 - assert len(response.data) == 2 diff --git a/todos/views.py b/todos/views.py index 1e3cd2e..5b66748 100644 --- a/todos/views.py +++ b/todos/views.py @@ -152,7 +152,7 @@ def get(self, request): else: todos = Todo.objects.get_with_user_id( user_id=user_id - ).order_by("order") + ).order_by("rank") except Todo.DoesNotExist as e: sentry_sdk.capture_exception(e) return Response( From 70b946922fec2d6176d4bd5873b20e8c8af3b8c5 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 16:38:49 +0900 Subject: [PATCH 159/229] fix : update todos.rank --- todos/models.py | 11 +++++ todos/serializers.py | 10 ++++- todos/tests/test_todo_patch.py | 75 +++++++++++++++++++++++++++++++++- todos/views.py | 1 - 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/todos/models.py b/todos/models.py index e5e61f7..267fe5d 100644 --- a/todos/models.py +++ b/todos/models.py @@ -19,6 +19,17 @@ def delete_many(self, instances): instance.save() return instances + def update_rank(self, instance, prev_id, next_id): + if prev_id is None and next_id is None: + return instance + elif prev_id is None: # Move to the top + instance.place_on_top() + elif next_id is None: # Move to the bottom + instance.place_on_bottom() + else: # Move to after prev_id + instance.place_after(prev_id) + return instance + def get_queryset(self): return super().get_queryset().filter(deleted_at__isnull=True) diff --git a/todos/serializers.py b/todos/serializers.py index 999414e..2311e1b 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -154,7 +154,7 @@ def validate(self, data): "category_id", "date", "is_completed", - "rank", + "patch_rank", ] ): raise serializers.ValidationError( @@ -167,7 +167,13 @@ def validate(self, data): def update(self, instance, validated_data): # Update the fields as usual for attr, value in validated_data.items(): - setattr(instance, attr, value) + if attr == "patch_rank": + # If the rank field is provided, update the rank field + Todo.objects.update_rank( + instance, value.get("prev_id"), value.get("next_id") + ) + else: + setattr(instance, attr, value) # Set the updated_at field to the current time instance.updated_at = timezone.now() diff --git a/todos/tests/test_todo_patch.py b/todos/tests/test_todo_patch.py index 2585030..74b985e 100644 --- a/todos/tests/test_todo_patch.py +++ b/todos/tests/test_todo_patch.py @@ -47,7 +47,7 @@ def test_update_todo_success( @pytest.mark.django_db -def test_update_todo_success_order( +def test_update_todo_success_top_order( create_user, create_category, authenticated_client, date, content ): todo = Todo.objects.create( @@ -67,13 +67,84 @@ def test_update_todo_success_order( url = reverse("todos") data = { "todo_id": todo.id, - "rank": { + "patch_rank": { "prev_id": todo2.id, "next_id": None, }, } response = authenticated_client.patch(url, data, format="json") assert response.status_code == 200 + assert response.data["rank"] > todo2.rank + + +@pytest.mark.django_db +def test_update_todo_success_bottom_order( + create_user, create_category, authenticated_client, date, content +): + todo = Todo.objects.create( + user_id=create_user, + date=date, + due_time=None, + content=content, + category_id=create_category, + ) + todo2 = Todo.objects.create( + user_id=create_user, + date=date, + due_time=None, + content=content, + category_id=create_category, + ) + url = reverse("todos") + data = { + "todo_id": todo2.id, + "patch_rank": { + "prev_id": None, + "next_id": todo.id, + }, + } + response = authenticated_client.patch(url, data, format="json") + assert response.status_code == 200 + assert response.data["rank"] < todo.rank + + +@pytest.mark.django_db +def test_update_todo_success_between_order( + create_user, create_category, authenticated_client, date, content +): + todo = Todo.objects.create( + user_id=create_user, + date=date, + due_time=None, + content=content, + category_id=create_category, + ) + todo2 = Todo.objects.create( + user_id=create_user, + date=date, + due_time=None, + content=content, + category_id=create_category, + ) + todo3 = Todo.objects.create( + user_id=create_user, + date=date, + due_time=None, + content=content, + category_id=create_category, + ) + url = reverse("todos") + data = { + "todo_id": todo.id, + "patch_rank": { + "prev_id": todo2.id, + "next_id": todo3.id, + }, + } + response = authenticated_client.patch(url, data, format="json") + assert response.status_code == 200 + assert response.data["rank"] > todo2.rank + assert response.data["rank"] < todo3.rank @pytest.mark.django_db diff --git a/todos/views.py b/todos/views.py index 5b66748..160dccb 100644 --- a/todos/views.py +++ b/todos/views.py @@ -193,7 +193,6 @@ def patch(self, request): return Response( {"error": "Todo not found"}, status=status.HTTP_404_NOT_FOUND ) - # rank 의 경우 업데이트 처리 필요 serializer = TodoSerializer( context={"request": request}, instance=todo, From 0ac5e7513c034f60adcdfdf8456344548d908594 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 17:12:42 +0900 Subject: [PATCH 160/229] =?UTF-8?q?fix=20:=20todos=20patch=20lexorank=20?= =?UTF-8?q?=EB=A1=9C=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/serializers.py | 14 +++++- todos/tests/test_subtodo_patch.py | 83 +++++++++++++++++++++++++++++-- todos/tests/test_todo_patch.py | 4 +- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/todos/serializers.py b/todos/serializers.py index 2311e1b..535463d 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -77,7 +77,7 @@ def validate(self, data): if request.method == "PATCH": if not any( data.get(field) - for field in ["content", "date", "is_completed", "rank"] + for field in ["content", "date", "is_completed", "patch_rank"] ): raise serializers.ValidationError( "At least one of content, date, \ @@ -86,6 +86,18 @@ def validate(self, data): return data return data + def update(self, instance, validated_data): + for attr, value in validated_data.items(): + if attr == "patch_rank": + SubTodo.objects.update_rank( + instance, value.get("prev_id"), value.get("next_id") + ) + else: + setattr(instance, attr, value) + instance.updated_at = timezone.now() + instance.save() + return instance + class GetTodoSerializer(serializers.ModelSerializer): children = SubTodoSerializer(many=True, read_only=True, source="subtodos") diff --git a/todos/tests/test_subtodo_patch.py b/todos/tests/test_subtodo_patch.py index 011661f..0ae6fb2 100644 --- a/todos/tests/test_subtodo_patch.py +++ b/todos/tests/test_subtodo_patch.py @@ -38,7 +38,7 @@ def test_update_subtodo_success( @pytest.mark.django_db -def test_update_subtodo_order( +def test_update_subtodo_success_top_rank( create_todo, authenticated_client, content, date ): subtodo = SubTodo.objects.create( @@ -55,16 +55,89 @@ def test_update_subtodo_order( ) url = reverse("subtodos") data = { - "subtodo_id": subtodo.id, + "subtodo_id": subtodo2.id, "content": "Updated SubTodo", "date": "2024-08-03", - "rank": { + "patch_rank": { "prev_id": None, - "next_id": subtodo2.id, + "next_id": subtodo.id, }, } response = authenticated_client.patch(url, data, format="json") - assert response.status_code == 400 + assert response.status_code == 200 + assert response.data["content"] == "Updated SubTodo" + assert response.data["rank"] < subtodo.rank + + +@pytest.mark.django_db +def test_update_subtodo_success_bottom_rank( + create_todo, authenticated_client, content, date +): + subtodo = SubTodo.objects.create( + content=content, + date=date, + todo_id=create_todo, + is_completed=False, + ) + subtodo2 = SubTodo.objects.create( + content=content, + date=date, + todo_id=create_todo, + is_completed=False, + ) + url = reverse("subtodos") + data = { + "subtodo_id": subtodo.id, + "content": "Updated SubTodo", + "date": "2024-08-03", + "patch_rank": { + "prev_id": subtodo2.id, + "next_id": None, + }, + } + response = authenticated_client.patch(url, data, format="json") + assert response.status_code == 200 + assert response.data["content"] == "Updated SubTodo" + assert response.data["rank"] > subtodo2.rank + + +@pytest.mark.django_db +def test_update_subtodo_success_between_rank( + create_todo, authenticated_client, content, date +): + subtodo = SubTodo.objects.create( + content=content, + date=date, + todo_id=create_todo, + is_completed=False, + ) + subtodo2 = SubTodo.objects.create( + content=content, + date=date, + todo_id=create_todo, + is_completed=False, + ) + subtodo3 = SubTodo.objects.create( + content=content, + date=date, + todo_id=create_todo, + is_completed=False, + ) + url = reverse("subtodos") + data = { + "subtodo_id": subtodo.id, + "content": "Updated SubTodo", + "date": "2024-08-03", + "patch_rank": { + "prev_id": subtodo2.id, + "next_id": subtodo3.id, + }, + } + response = authenticated_client.patch(url, data, format="json") + assert response.status_code == 200 + assert response.data["content"] == "Updated SubTodo" + assert response.data["rank"] < subtodo3.rank + assert response.data["rank"] > subtodo2.rank @pytest.mark.django_db diff --git a/todos/tests/test_todo_patch.py b/todos/tests/test_todo_patch.py index 74b985e..ebe9270 100644 --- a/todos/tests/test_todo_patch.py +++ b/todos/tests/test_todo_patch.py @@ -47,7 +47,7 @@ def test_update_todo_success( @pytest.mark.django_db -def test_update_todo_success_top_order( +def test_update_todo_success_bottom_order( create_user, create_category, authenticated_client, date, content ): todo = Todo.objects.create( @@ -78,7 +78,7 @@ def test_update_todo_success_top_order( @pytest.mark.django_db -def test_update_todo_success_bottom_order( +def test_update_todo_success_top_order( create_user, create_category, authenticated_client, date, content ): todo = Todo.objects.create( From dbddd558bb51cd46c755b688c0458baab8759a88 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 17:30:29 +0900 Subject: [PATCH 161/229] =?UTF-8?q?fix=20:=20subtodos.patch=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/models.py | 3 ++- todos/serializers.py | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/todos/models.py b/todos/models.py index 267fe5d..3c590e6 100644 --- a/todos/models.py +++ b/todos/models.py @@ -27,7 +27,8 @@ def update_rank(self, instance, prev_id, next_id): elif next_id is None: # Move to the bottom instance.place_on_bottom() else: # Move to after prev_id - instance.place_after(prev_id) + prev_instance = self.get_queryset().get(id=prev_id) + instance.place_after(prev_instance) return instance def get_queryset(self): diff --git a/todos/serializers.py b/todos/serializers.py index 535463d..2d4bf5a 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -10,12 +10,8 @@ class PatchRankSerializer(serializers.Serializer): - prev_id = serializers.PrimaryKeyRelatedField( - queryset=Todo.objects.all(), required=False, allow_null=True - ) - next_id = serializers.PrimaryKeyRelatedField( - queryset=Todo.objects.all(), required=False, allow_null=True - ) + prev_id = serializers.IntegerField(required=False, allow_null=True) + next_id = serializers.IntegerField(required=False, allow_null=True) class CategorySerializer(serializers.ModelSerializer): From 47ef1545d3d969d4b02ce7093f23e4e800ba932f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 17:36:06 +0900 Subject: [PATCH 162/229] fix : category models color char -> smallint --- todos/migrations/0016_alter_category_color.py | 19 +++++++++++++++++++ todos/models.py | 5 ++++- todos/serializers.py | 8 -------- 3 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 todos/migrations/0016_alter_category_color.py diff --git a/todos/migrations/0016_alter_category_color.py b/todos/migrations/0016_alter_category_color.py new file mode 100644 index 0000000..e9f96be --- /dev/null +++ b/todos/migrations/0016_alter_category_color.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.16 on 2024-10-12 08:35 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0015_alter_category_rank_alter_subtodo_rank_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='category', + name='color', + field=models.SmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(8)]), + ), + ] diff --git a/todos/models.py b/todos/models.py index 3c590e6..c53be08 100644 --- a/todos/models.py +++ b/todos/models.py @@ -1,3 +1,4 @@ +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Count, Prefetch, Q from django.utils import timezone @@ -144,7 +145,9 @@ def __str__(self): class Category(TimeStamp, RankedModel): id = models.AutoField(primary_key=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=1) - color = models.CharField(max_length=7) + color = models.SmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(8)] + ) title = models.CharField(max_length=100, null=True) rank = RankField(insert_to_bottom=True) diff --git a/todos/serializers.py b/todos/serializers.py index 2d4bf5a..2858b14 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -1,5 +1,4 @@ # todos/serializers.py -import re import django.utils.timezone as timezone from rest_framework import serializers @@ -26,13 +25,6 @@ def validate_user_id(self, data): raise serializers.ValidationError("User does not exist") return data - def validate_color(self, data): - hex_color_pattern = r"^#([A-Fa-f0-9]{6})$" - match = re.match(hex_color_pattern, data) - if bool(match) is False: - raise serializers.ValidationError("Color code is invalid") - return data - def validate(self, data): request = self.context["request"] if request.method == "PATCH": From 6f174ab906cd6a8b7e461e153da4ec1e541dbe4d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 17:53:45 +0900 Subject: [PATCH 163/229] fix : model: Category color 0~8 --- conftest.py | 2 +- todos/migrations/0016_alter_category_color.py | 2 +- todos/models.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index a84f73d..3c673e9 100644 --- a/conftest.py +++ b/conftest.py @@ -115,7 +115,7 @@ def get_order(index): @pytest.fixture def color(): - return fake.color() + return fake.random_int(min=0, max=8) @pytest.fixture diff --git a/todos/migrations/0016_alter_category_color.py b/todos/migrations/0016_alter_category_color.py index e9f96be..6904c48 100644 --- a/todos/migrations/0016_alter_category_color.py +++ b/todos/migrations/0016_alter_category_color.py @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='category', name='color', - field=models.SmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(8)]), + field=models.SmallIntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(8)]), ), ] diff --git a/todos/models.py b/todos/models.py index c53be08..ee2e37d 100644 --- a/todos/models.py +++ b/todos/models.py @@ -146,7 +146,7 @@ class Category(TimeStamp, RankedModel): id = models.AutoField(primary_key=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=1) color = models.SmallIntegerField( - validators=[MinValueValidator(1), MaxValueValidator(8)] + validators=[MinValueValidator(0), MaxValueValidator(8)] ) title = models.CharField(max_length=100, null=True) rank = RankField(insert_to_bottom=True) From e8eea1d709e352f0374068210d5350c92d9e799a Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 18:07:34 +0900 Subject: [PATCH 164/229] fix : conftest llm -> recommend_result --- conftest.py | 6 +++--- todos/tests/test_recommend_check_rate_limit.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/conftest.py b/conftest.py index 3c673e9..adc8806 100644 --- a/conftest.py +++ b/conftest.py @@ -43,7 +43,7 @@ def create_category( db, create_user, title="Test Category", - color="#FFFFFF", + color=1, ): category = Category.objects.create( user_id=create_user, @@ -139,14 +139,14 @@ def due_time(): @pytest.fixture -def llm(): +def recommend_result(): mock_response = Mock() mock_response.choices = [ Mock( message=Mock( content=( '{"id": 1, "content": "subtask", "start_date": "2024-09-01", ' # noqa - '"end_date": "2024-09-24", "category_id": 1, "order": 1, ' + '"end_date": "2024-09-24", "category_id": 1 ' '"is_completed": false, "children": []}' ) ) diff --git a/todos/tests/test_recommend_check_rate_limit.py b/todos/tests/test_recommend_check_rate_limit.py index fda3bc9..0227268 100644 --- a/todos/tests/test_recommend_check_rate_limit.py +++ b/todos/tests/test_recommend_check_rate_limit.py @@ -6,8 +6,10 @@ @patch("todos.views.client.chat.completions.create") -def test_rate_limit_exceeded(mock_llm, authenticated_client, create_todo, llm): - mock_llm.return_value = llm +def test_rate_limit_exceeded( + mock_llm, authenticated_client, create_todo, recommend_result +): + mock_llm.return_value = recommend_result url = reverse("recommend") response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK @@ -18,9 +20,11 @@ def test_rate_limit_exceeded(mock_llm, authenticated_client, create_todo, llm): @patch("todos.views.client.chat.completions.create") -def test_rate_limit_passed(mock_llm, authenticated_client, create_todo, llm): +def test_rate_limit_passed( + mock_llm, authenticated_client, create_todo, recommend_result +): url = reverse("recommend") - mock_llm.return_value = llm + mock_llm.return_value = recommend_result response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK @@ -33,13 +37,13 @@ def test_rate_limit_passed(mock_llm, authenticated_client, create_todo, llm): @patch("todos.views.client.chat.completions.create") def test_rate_limit_premium( - mock_llm, authenticated_client, create_user, create_todo, llm + mock_llm, authenticated_client, create_user, create_todo, recommend_result ): create_user.is_premium = True create_user.save() url = reverse("recommend") - mock_llm.return_value = llm + mock_llm.return_value = recommend_result response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK From d5fc116b3929310bcee6353982b431668ee203ad Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 18:11:46 +0900 Subject: [PATCH 165/229] =?UTF-8?q?fix=20:=20fcm=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/views.py | 111 ++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/todos/views.py b/todos/views.py index 160dccb..2ea0891 100644 --- a/todos/views.py +++ b/todos/views.py @@ -10,8 +10,7 @@ from rest_framework.views import APIView from onestep_be.settings import client - -# from todos.firebase_messaging import send_push_notification_device +from todos.firebase_messaging import send_push_notification_device from todos.models import Category, SubTodo, Todo, UserLastUsage from todos.serializers import ( CategorySerializer, @@ -70,12 +69,12 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # TODO_FCM_MESSAGE_TITLE, - # TODO_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + TODO_FCM_MESSAGE_TITLE, + TODO_FCM_MESSAGE_BODY, + ) return Response( serializer.data, status=status.HTTP_201_CREATED ) @@ -202,12 +201,12 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # TODO_FCM_MESSAGE_TITLE, - # TODO_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + TODO_FCM_MESSAGE_TITLE, + TODO_FCM_MESSAGE_BODY, + ) return Response(serializer.data, status=status.HTTP_200_OK) else: sentry_validation_error( @@ -252,12 +251,12 @@ def delete(self, request): subtodos = SubTodo.objects.get_subtodos(todo.id) SubTodo.objects.delete_many(subtodos) Todo.objects.delete_instance(todo) - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # TODO_FCM_MESSAGE_TITLE, - # TODO_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + TODO_FCM_MESSAGE_TITLE, + TODO_FCM_MESSAGE_BODY, + ) return Response( {"todo_id": todo.id, "message": "Todo deleted successfully"}, status=status.HTTP_200_OK, @@ -300,12 +299,12 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # SUBTODO_FCM_MESSAGE_TITLE, - # SUBTODO_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + SUBTODO_FCM_MESSAGE_TITLE, + SUBTODO_FCM_MESSAGE_BODY, + ) return Response(serializer.data, status=status.HTTP_201_CREATED) else: @@ -408,12 +407,12 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # SUBTODO_FCM_MESSAGE_TITLE, - # SUBTODO_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + SUBTODO_FCM_MESSAGE_TITLE, + SUBTODO_FCM_MESSAGE_BODY, + ) return Response(serializer.data, status=status.HTTP_200_OK) else: @@ -459,12 +458,12 @@ def delete(self, request): sub_todo = SubTodo.objects.get_with_id(id=subtodo_id) SubTodo.objects.delete_instance(sub_todo) - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # SUBTODO_FCM_MESSAGE_TITLE, - # SUBTODO_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + SUBTODO_FCM_MESSAGE_TITLE, + SUBTODO_FCM_MESSAGE_BODY, + ) return Response( { "subtodo_id": sub_todo.id, @@ -512,12 +511,12 @@ def post(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # CATEGORY_FCM_MESSAGE_TITLE, - # CATEGORY_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + CATEGORY_FCM_MESSAGE_TITLE, + CATEGORY_FCM_MESSAGE_BODY, + ) return Response( serializer.data, status=status.HTTP_201_CREATED @@ -594,12 +593,12 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # CATEGORY_FCM_MESSAGE_TITLE, - # CATEGORY_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + CATEGORY_FCM_MESSAGE_TITLE, + CATEGORY_FCM_MESSAGE_BODY, + ) return Response(serializer.data, status=status.HTTP_200_OK) else: @@ -690,12 +689,12 @@ def delete(self, request): ) category = Category.objects.get_with_id(id=category_id) Category.objects.delete_instance(category) - # send_push_notification_device( - # request.auth.get("device"), - # request.user, - # CATEGORY_FCM_MESSAGE_TITLE, - # CATEGORY_FCM_MESSAGE_BODY, - # ) + send_push_notification_device( + request.auth.get("device"), + request.user, + CATEGORY_FCM_MESSAGE_TITLE, + CATEGORY_FCM_MESSAGE_BODY, + ) return Response( { "category_id": category.id, From e3753965e1a13c86bfb337078900006d4f48e9bd Mon Sep 17 00:00:00 2001 From: earthyoung Date: Sat, 12 Oct 2024 19:06:06 +0900 Subject: [PATCH 166/229] =?UTF-8?q?feat:=20pytest=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=9C=20?= =?UTF-8?q?send=5Fpush=5Fnotification=5Fdevice=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=9A=B0=ED=9A=8C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conftest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/conftest.py b/conftest.py index 73b4e49..627b3d7 100644 --- a/conftest.py +++ b/conftest.py @@ -156,3 +156,9 @@ def llm(): ] return mock_response + + +# FCM 알림 함수를 기본적으로 disable하는 fixture +@pytest.fixture(autouse=True) +def patch_send_push_notification_device(monkeypatch): + monkeypatch.setattr('todos.firebase_messaging.send_push_notification_device', lambda: None) From c51105cb1104a9f030473d77a26ed70209ae912b Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 19:20:11 +0900 Subject: [PATCH 167/229] fix : patch_rank serializer --- todos/serializers.py | 4 ++-- todos/tests/test_todo_patch.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/todos/serializers.py b/todos/serializers.py index 2858b14..7641b69 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -9,8 +9,8 @@ class PatchRankSerializer(serializers.Serializer): - prev_id = serializers.IntegerField(required=False, allow_null=True) - next_id = serializers.IntegerField(required=False, allow_null=True) + prev_id = serializers.IntegerField(allow_null=True) + next_id = serializers.IntegerField(allow_null=True) class CategorySerializer(serializers.ModelSerializer): diff --git a/todos/tests/test_todo_patch.py b/todos/tests/test_todo_patch.py index ebe9270..fca295b 100644 --- a/todos/tests/test_todo_patch.py +++ b/todos/tests/test_todo_patch.py @@ -108,6 +108,31 @@ def test_update_todo_success_top_order( assert response.data["rank"] < todo.rank +@pytest.mark.django_db +def test_update_todo_success_None_order( + create_user, create_category, authenticated_client, date, content +): + todo = Todo.objects.create( + user_id=create_user, + date=date, + due_time=None, + content=content, + category_id=create_category, + ) + before_rank = todo.rank + url = reverse("todos") + data = { + "todo_id": todo.id, + "patch_rank": { + "prev_id": None, + "next_id": None, + }, + } + response = authenticated_client.patch(url, data, format="json") + assert response.status_code == 200 + assert response.data["rank"] == before_rank + + @pytest.mark.django_db def test_update_todo_success_between_order( create_user, create_category, authenticated_client, date, content From 5009d1294acd38ddfc2c85761b44ff7d5df600d4 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 19:31:14 +0900 Subject: [PATCH 168/229] fix: requirement django version 5.0.6 -> 4.2.16 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 22977ce..170b1e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ coreschema==0.0.4 cryptography==42.0.8 distro==1.9.0 dj-rest-auth==6.0.0 -Django==5.0.6 +Django==4.2.16 django-allauth==0.63.3 django-cors-headers==4.4.0 django-db-connection-pool==1.2.5 From 2e1d9f6f23f9da23963e9e61d18a50f55e2fcf94 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 12 Oct 2024 20:30:39 +0900 Subject: [PATCH 169/229] =?UTF-8?q?fix=20:=20swagger=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/models.py | 2 +- todos/serializers.py | 2 +- todos/swagger_serializers.py | 87 +++++++++++++++++++++++++++++------- todos/views.py | 48 +++++++------------- 4 files changed, 91 insertions(+), 48 deletions(-) diff --git a/todos/models.py b/todos/models.py index 8ce150e..33c9f6c 100644 --- a/todos/models.py +++ b/todos/models.py @@ -34,7 +34,7 @@ def update_rank(self, instance, prev_id, next_id): return instance def get_queryset(self): - super().get_queryset().filter(deleted_at__isnull=True) + return super().get_queryset().filter(deleted_at__isnull=True) def get_with_id(self, id): instance = self.get_queryset().filter(id=id).first() diff --git a/todos/serializers.py b/todos/serializers.py index 7641b69..630cd5d 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -111,7 +111,7 @@ class TodoSerializer(serializers.ModelSerializer): queryset=Category.objects.all(), required=True ) user_id = serializers.PrimaryKeyRelatedField( - queryset=User.objects.all(), required=True + queryset=User.objects.all(), required=False ) date = serializers.DateField(allow_null=True, required=False) due_time = serializers.TimeField(allow_null=True, required=False) diff --git a/todos/swagger_serializers.py b/todos/swagger_serializers.py index 036932a..d76c6fe 100644 --- a/todos/swagger_serializers.py +++ b/todos/swagger_serializers.py @@ -4,21 +4,77 @@ from .models import Category, SubTodo, Todo -class SwaggerOrderserializer(serializers.ModelSerializer): +class SwaggerTodoSerializer(serializers.ModelSerializer): + content = serializers.CharField(max_length=255) + category_id = serializers.PrimaryKeyRelatedField( + queryset=Category.objects.all(), required=True + ) + date = serializers.DateField(allow_null=True, required=False) + due_time = serializers.TimeField(allow_null=True, required=False) + is_completed = serializers.BooleanField(default=False, required=False) + + class Meta: + model = Todo + fields = [ + "id", + "content", + "category_id", + "date", + "due_time", + "user_id", + "is_completed", + "rank", + ] + read_only_fields = ["user_id", "rank", "id"] + + +class SwaggerSubTodoSerializer(serializers.ModelSerializer): + content = serializers.CharField(max_length=255) + todo_id = serializers.PrimaryKeyRelatedField( + queryset=Todo.objects.all(), required=True + ) + date = serializers.DateField(required=False, allow_null=True) + due_time = serializers.TimeField(required=False, allow_null=True) + is_completed = serializers.BooleanField(default=False) + + class Meta: + model = Todo + fields = [ + "id", + "todo_id", + "content", + "date", + "due_time", + "is_completed", + "rank", + ] + read_only_fields = ["rank", "id"] + + +class SwaggerCategorySerializer(serializers.ModelSerializer): + color = serializers.IntegerField(min_value=0, max_value=8) + title = serializers.CharField(max_length=100) + + class Meta: + model = Category + fields = ["id", "user_id", "color", "title", "rank"] + read_only_fields = ["rank", "id", "user_id"] + + +class SwaggerRankSerializer(serializers.ModelSerializer): prev_id = serializers.IntegerField() next_id = serializers.IntegerField() - updated_order = serializers.CharField(max_length=255) class Meta: model = Todo - fields = ["prev_id", "next_id", "updated_order"] + fields = ["prev_id", "next_id"] class SwaggerCategoryPatchSerializer(serializers.ModelSerializer): category_id = serializers.IntegerField() - color = serializers.CharField(max_length=7, required=False) + color = serializers.IntegerField(min_value=0, max_value=8) title = serializers.CharField(max_length=100, required=False) - order = SwaggerOrderserializer(required=False) + rank = SwaggerRankSerializer(required=False) class Meta: model = Category @@ -31,9 +87,9 @@ class SwaggerTodoPatchSerializer(serializers.ModelSerializer): category_id = serializers.PrimaryKeyRelatedField( queryset=Category.objects.all(), required=False ) - start_date = serializers.DateField(allow_null=True, required=False) - end_date = serializers.DateField(allow_null=True, required=False) - order = SwaggerOrderserializer(required=False) + date = serializers.DateField(allow_null=True, required=False) + due_time = serializers.TimeField(allow_null=True, required=False) + rank = SwaggerRankSerializer(required=False) is_completed = serializers.BooleanField(default=False, required=False) class Meta: @@ -42,9 +98,9 @@ class Meta: "todo_id", "content", "category_id", - "start_date", - "end_date", - "order", + "date", + "due_time", + "rank", "is_completed", ] @@ -52,11 +108,11 @@ class Meta: class SwaggerSubTodoPatchSerializer(serializers.ModelSerializer): subtodo_id = serializers.IntegerField() content = serializers.CharField(max_length=255, required=False) - todo = serializers.PrimaryKeyRelatedField( + todo_id = serializers.PrimaryKeyRelatedField( queryset=Todo.objects.all(), required=False ) date = serializers.DateField(allow_null=True, required=False) - order = SwaggerOrderserializer(required=False) + rank = SwaggerRankSerializer(required=False) is_completed = serializers.BooleanField(default=False, required=False) class Meta: @@ -64,8 +120,9 @@ class Meta: fields = [ "subtodo_id", "content", - "todo", + "todo_id", "date", - "order", + "due_time", + "rank", "is_completed", ] diff --git a/todos/views.py b/todos/views.py index 2ea0891..19f0ac4 100644 --- a/todos/views.py +++ b/todos/views.py @@ -20,8 +20,11 @@ ) from todos.swagger_serializers import ( SwaggerCategoryPatchSerializer, + SwaggerCategorySerializer, SwaggerSubTodoPatchSerializer, + SwaggerSubTodoSerializer, SwaggerTodoPatchSerializer, + SwaggerTodoSerializer, ) from todos.utils import sentry_validation_error, set_sentry_user @@ -41,9 +44,9 @@ class TodoView(APIView): @swagger_auto_schema( tags=["Todo"], - request_body=TodoSerializer, + request_body=SwaggerTodoSerializer, operation_summary="Create a todo", - responses={201: TodoSerializer}, + responses={201: SwaggerTodoSerializer}, ) def post(self, request): """ @@ -96,13 +99,6 @@ def post(self, request): @swagger_auto_schema( tags=["Todo"], manual_parameters=[ - openapi.Parameter( - "user_id", - openapi.IN_QUERY, - type=openapi.TYPE_INTEGER, - description="user_id", - required=True, - ), openapi.Parameter( "start_date", openapi.IN_QUERY, @@ -164,7 +160,7 @@ def get(self, request): tags=["Todo"], request_body=SwaggerTodoPatchSerializer, operation_summary="Update a todo", - responses={200: TodoSerializer}, + responses={200: SwaggerTodoSerializer}, ) def patch(self, request): """ @@ -228,7 +224,7 @@ def patch(self, request): }, ), operation_summary="Delete a todo", - responses={200: TodoSerializer}, + responses={200: SwaggerTodoSerializer}, ) def delete(self, request): """ @@ -278,9 +274,9 @@ class SubTodoView(APIView): @swagger_auto_schema( tags=["SubTodo"], - request_body=SubTodoSerializer(many=True), + request_body=SwaggerSubTodoSerializer(many=True), operation_summary="Create a subtodo", - responses={201: SubTodoSerializer}, + responses={201: SwaggerSubTodoSerializer}, ) def post(self, request): """ @@ -328,7 +324,7 @@ def post(self, request): ) ], operation_summary="Get a subtodo", - responses={200: SubTodoSerializer}, + responses={200: SwaggerSubTodoSerializer}, ) def get(self, request): """ @@ -364,7 +360,7 @@ def get(self, request): tags=["SubTodo"], request_body=SwaggerSubTodoPatchSerializer, operation_summary="Update a subtodo", - responses={200: SubTodoSerializer}, + responses={200: SwaggerSubTodoSerializer}, ) def patch(self, request): """ @@ -396,7 +392,6 @@ def patch(self, request): {"error": "SubTodo not found"}, status=status.HTTP_404_NOT_FOUND, ) - # rank 관련 로직 필요 serializer = SubTodoSerializer( context={"request": request}, instance=sub_todo, @@ -435,7 +430,7 @@ def patch(self, request): }, ), operation_summary="Delete a subtodo", - responses={200: SubTodoSerializer}, + responses={200: SwaggerSubTodoSerializer}, ) def delete(self, request): """ @@ -489,9 +484,9 @@ class CategoryView(APIView): @swagger_auto_schema( tags=["Category"], - request_body=CategorySerializer, + request_body=SwaggerCategorySerializer, operation_summary="Create a category", - responses={201: CategorySerializer}, + responses={201: SwaggerCategorySerializer}, ) def post(self, request): """ @@ -540,7 +535,7 @@ def post(self, request): tags=["Category"], request_body=SwaggerCategoryPatchSerializer, operation_summary="Update a category", - responses={200: CategorySerializer}, + responses={200: SwaggerCategorySerializer}, ) def patch(self, request): """ @@ -612,17 +607,8 @@ def patch(self, request): @swagger_auto_schema( tags=["Category"], - manual_parameters=[ - openapi.Parameter( - "user_id", - openapi.IN_QUERY, - type=openapi.TYPE_INTEGER, - description="user_id", - required=True, - ) - ], operation_summary="Get a category", - responses={200: CategorySerializer}, + responses={200: SwaggerCategorySerializer}, ) def get(self, request): """ @@ -667,7 +653,7 @@ def get(self, request): }, ), operation_summary="Delete a category", - responses={200: CategorySerializer}, + responses={200: SwaggerCategorySerializer}, ) def delete(self, request): """ From 3bf33161d882aa551fc0bdfaa0930ea14aa8b64c Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 17 Oct 2024 15:22:34 +0900 Subject: [PATCH 170/229] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 7 +++++++ accounts/views.py | 27 +++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index f6ef857..bfe45e9 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,3 +1,5 @@ +from datetime import timezone + import sentry_sdk from django.contrib.auth.models import AbstractUser from django.contrib.auth.validators import UnicodeUsernameValidator @@ -69,6 +71,11 @@ def get_or_create_user(self, email): sentry_sdk.capture_message("Get User", level="info") return user + def delete_user(self, instance): + instance.deleted_at = timezone.now() + instance.save(update_fields=["deleted_at"]) + return instance + class Device(models.Model): user_id = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/accounts/views.py b/accounts/views.py index 7b138ec..976c4f4 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -68,7 +68,9 @@ def get(self, request): "username": request.user.username, } ) - user = User.objects.get(username=request.user.username) + user = User.objects.get( + username=request.user.username, deleted_at=None + ) serializer = UserSerializer(user) return Response(serializer.data, status=status.HTTP_200_OK) except User.DoesNotExist as e: @@ -97,12 +99,15 @@ def patch(self, request): } ) if request.data.get("is_premium"): - user.is_premium = request.data.get("is_premium") + user.is_premium = request.data.get( + "is_premium", deleted_at=None + ) if request.data.get("is_subscribed"): - user.is_subscribed = request.data.get("is_subscribed") + user.is_subscribed = request.data.get( + "is_subscribed", deleted_at=None + ) user.save() serializer = UserSerializer(user) - sentry_sdk.capture_message("User updated", level="info") return Response(serializer.data, status=status.HTTP_200_OK) except User.DoesNotExist as e: sentry_sdk.capture_exception(e) @@ -116,6 +121,20 @@ def patch(self, request): status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + def delete(self, request): + try: + user = request.user + user = User.delete_user(user) + return Response( + {"message": "User deleted"}, status=status.HTTP_200_OK + ) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": "An unexpected error occurred"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + class AndroidClientView(APIView): def get(self, request): From 75344456e51dec360c48ee1b4aa0a7feacceb09c Mon Sep 17 00:00:00 2001 From: earthyoung Date: Thu, 17 Oct 2024 15:36:12 +0900 Subject: [PATCH 171/229] =?UTF-8?q?fix:=20monkeypatch=20setattr=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20force=5Fauthenticate=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index 482a141..f3cc1c2 100644 --- a/conftest.py +++ b/conftest.py @@ -33,7 +33,7 @@ def create_user(db): @pytest.fixture def authenticated_client(create_user): - client.force_authenticate(user=create_user) + client.force_authenticate(user=create_user, token={"device": None}) yield client client.force_authenticate(user=None) # logout @@ -159,4 +159,4 @@ def recommend_result(): # FCM 알림 함수를 기본적으로 disable하는 fixture @pytest.fixture(autouse=True) def patch_send_push_notification_device(monkeypatch): - monkeypatch.setattr('todos.firebase_messaging.send_push_notification_device', lambda: None) + monkeypatch.setattr('todos.firebase_messaging.send_push_notification_device', lambda *args, **kwargs: None) From 11480db0632ab7a492d9b9a6283c3d416680ab5a Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 17 Oct 2024 15:49:47 +0900 Subject: [PATCH 172/229] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(test=20?= =?UTF-8?q?=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 5 ++--- accounts/tests.py | 17 +++++++++++++++++ accounts/views.py | 12 ++++-------- onestep_be/settings.py | 2 +- requirements.txt | 1 + 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index bfe45e9..adf3ce3 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,9 +1,8 @@ -from datetime import timezone - import sentry_sdk from django.contrib.auth.models import AbstractUser from django.contrib.auth.validators import UnicodeUsernameValidator from django.db import models +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from accounts.utils import send_email, send_welcome_email @@ -71,7 +70,7 @@ def get_or_create_user(self, email): sentry_sdk.capture_message("Get User", level="info") return user - def delete_user(self, instance): + def delete_user(instance): instance.deleted_at = timezone.now() instance.save(update_fields=["deleted_at"]) return instance diff --git a/accounts/tests.py b/accounts/tests.py index d3cf302..e3fc47e 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -63,6 +63,23 @@ def test_update_user_is_premium( assert response.data["is_premium"] +@pytest.mark.django_db +def test_delete_user( + create_user, + authenticated_client, +): + url = reverse("user") # URL name for the categoryView patch method + response = authenticated_client.delete(url, {}, format="json") + assert response.status_code == 200 + + assert ( + User.objects.filter( + id=create_user.id, deleted_at__isnull=True + ).exists() + is False + ) + + @pytest.mark.django_db def test_google_login(invalid_token): client = APIClient() diff --git a/accounts/views.py b/accounts/views.py index 976c4f4..8097f35 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -69,7 +69,7 @@ def get(self, request): } ) user = User.objects.get( - username=request.user.username, deleted_at=None + username=request.user.username, deleted_at__isnull=True ) serializer = UserSerializer(user) return Response(serializer.data, status=status.HTTP_200_OK) @@ -99,13 +99,9 @@ def patch(self, request): } ) if request.data.get("is_premium"): - user.is_premium = request.data.get( - "is_premium", deleted_at=None - ) + user.is_premium = request.data.get("is_premium") if request.data.get("is_subscribed"): - user.is_subscribed = request.data.get( - "is_subscribed", deleted_at=None - ) + user.is_subscribed = request.data.get("is_subscribed") user.save() serializer = UserSerializer(user) return Response(serializer.data, status=status.HTTP_200_OK) @@ -124,7 +120,7 @@ def patch(self, request): def delete(self, request): try: user = request.user - user = User.delete_user(user) + user = User.delete_user(instance=user) return Response( {"message": "User deleted"}, status=status.HTTP_200_OK ) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index ee0d77e..754957a 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -15,11 +15,11 @@ from pathlib import Path import django.db.models.signals +import pymysql import resend import sentry_sdk from openai import OpenAI from sentry_sdk.integrations.django import DjangoIntegration -import pymysql from accounts.aws import get_secret diff --git a/requirements.txt b/requirements.txt index 170b1e8..b6778e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -110,3 +110,4 @@ swapper==1.4.0 django_crontab==0.7.1 Faker==27.0.0 django-lexorank==0.1.3 +pymysql==1.1.1 From e0973f9bd7e21c934c1ee2df629b5f8360fb5dd3 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 17 Oct 2024 16:16:45 +0900 Subject: [PATCH 173/229] =?UTF-8?q?feat:=20user=20social=5Fprovider=20:=20?= =?UTF-8?q?google,=20apple=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0015_alter_user_social_provider.py | 18 ++++++++++++++++++ accounts/models.py | 4 +--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 accounts/migrations/0015_alter_user_social_provider.py diff --git a/accounts/migrations/0015_alter_user_social_provider.py b/accounts/migrations/0015_alter_user_social_provider.py new file mode 100644 index 0000000..93d61df --- /dev/null +++ b/accounts/migrations/0015_alter_user_social_provider.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-10-17 07:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0014_user_is_premium'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='social_provider', + field=models.CharField(choices=[('GOOGLE', 'Google'), ('APPLE', 'Apple')], default='GOOGLE', max_length=30), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index f6ef857..9bc8f2e 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -19,8 +19,7 @@ class Meta: class User(AbstractUser, TimeStamp): class SocialProvider(models.TextChoices): GOOGLE = "GOOGLE" - KAKAO = "KAKAO" - NAVER = "NAVER" + APPLE = "APPLE" username_validator = UnicodeUsernameValidator() username = models.CharField( @@ -57,7 +56,6 @@ def get_or_create_user(self, email): email, user.username, ) - sentry_sdk.capture_message("Sent welcome email", level="info") except Exception as e: sentry_sdk.capture_exception(e) sentry_sdk.capture_message( From c7e1eb49b605650835a6092ca1f663e5a545d019 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 17 Oct 2024 20:41:37 +0900 Subject: [PATCH 174/229] feat: GET ios client id --- accounts/urls.py | 1 + accounts/views.py | 16 ++++++++++++++++ conftest.py | 5 ++++- onestep_be/settings.py | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/accounts/urls.py b/accounts/urls.py index 04d224c..1677160 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -16,4 +16,5 @@ path("login/google/", GoogleLogin.as_view(), name="google_login"), path("user/", UserRetrieveView.as_view(), name="user"), path("android/", AndroidClientView.as_view(), name="android"), + path("ios/", AndroidClientView.as_view(), name="ios"), ] diff --git a/accounts/views.py b/accounts/views.py index 7b138ec..354d8a5 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -131,3 +131,19 @@ def get(self, request): {"error": "Android client id not found"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class IOSClientView(APIView): + def get(self, request): + try: + IOS_CLIENT_ID = settings.SECRETS.get("IOS_CLIENT_ID") + return Response( + {"ios_client_id": IOS_CLIENT_ID}, + status=status.HTTP_200_OK, + ) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": "IOS client id not found"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/conftest.py b/conftest.py index f3cc1c2..19c8795 100644 --- a/conftest.py +++ b/conftest.py @@ -159,4 +159,7 @@ def recommend_result(): # FCM 알림 함수를 기본적으로 disable하는 fixture @pytest.fixture(autouse=True) def patch_send_push_notification_device(monkeypatch): - monkeypatch.setattr('todos.firebase_messaging.send_push_notification_device', lambda *args, **kwargs: None) + monkeypatch.setattr( + "todos.firebase_messaging.send_push_notification_device", + lambda *args, **kwargs: None, + ) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index ee0d77e..754957a 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -15,11 +15,11 @@ from pathlib import Path import django.db.models.signals +import pymysql import resend import sentry_sdk from openai import OpenAI from sentry_sdk.integrations.django import DjangoIntegration -import pymysql from accounts.aws import get_secret From a4b1e9ea4a120e294d6359ab4f9938f130b2c8e9 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 18 Oct 2024 18:47:36 +0900 Subject: [PATCH 175/229] fix : ios client id -> google ios client id --- accounts/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index 354d8a5..662ee04 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -136,9 +136,9 @@ def get(self, request): class IOSClientView(APIView): def get(self, request): try: - IOS_CLIENT_ID = settings.SECRETS.get("IOS_CLIENT_ID") + GOOGLE_IOS_CLIENT_ID = settings.SECRETS.get("GOOGLE_IOS_CLIENT_ID") return Response( - {"ios_client_id": IOS_CLIENT_ID}, + {"ios_client_id": GOOGLE_IOS_CLIENT_ID}, status=status.HTTP_200_OK, ) except Exception as e: From 4d88a2221b6d974745e1abe20088bd1ade097c5b Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 18 Oct 2024 19:36:44 +0900 Subject: [PATCH 176/229] feat : ios google login --- accounts/exceptions.py | 11 ++++- accounts/models.py | 5 --- accounts/tests.py | 91 +++++++++++++++++++++++++++++++++++++++++- accounts/tokens.py | 5 +++ accounts/views.py | 59 +++++++++++++++++++-------- 5 files changed, 147 insertions(+), 24 deletions(-) diff --git a/accounts/exceptions.py b/accounts/exceptions.py index 052369e..2c8cf72 100644 --- a/accounts/exceptions.py +++ b/accounts/exceptions.py @@ -3,5 +3,14 @@ class LoginException(exceptions.APIException): status_code = status.HTTP_400_BAD_REQUEST - default_detail = "device token and token is required" + default_detail = "device type and token is required" default_code = "error" + + def __init__(self, detail=None, code=None): + if detail is not None: + self.detail = detail + else: + self.detail = self.default_detail + if code is not None: + self.default_code = code + super().__init__(self.detail, self.default_code) diff --git a/accounts/models.py b/accounts/models.py index 9bc8f2e..988d069 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -48,9 +48,6 @@ def get_or_create_user(self, email): try: user = User.objects.get(username=email) except User.DoesNotExist: - sentry_sdk.capture_message( - "User does not exist. Creating new user" - ) user = User.objects.create(username=email, password="") send_welcome_email( email, @@ -63,8 +60,6 @@ def get_or_create_user(self, email): ) raise e - sentry_sdk.set_user({"id": user.id}) - sentry_sdk.capture_message("Get User", level="info") return user diff --git a/accounts/tests.py b/accounts/tests.py index d3cf302..56656cd 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -85,7 +85,7 @@ def api_client(self): @patch("accounts.views.id_token.verify_oauth2_token") @patch("accounts.models.send_welcome_email") - def test_google_login_new_user( + def test_google_login_android_new_user( self, mock_send_welcome_email, mock_verify_oauth2_token, api_client ): # Mock the token verification response @@ -98,7 +98,94 @@ def test_google_login_new_user( url = reverse("google_login") # Create a mock request - data = {"token": "mock_token", "device_token": "mock_device_token"} + data = { + "token": "mock_token", + "device_token": "mock_device_token", + "type": 0, + } + + # Call the view + response = api_client.post(url, data, format="json") + + # Check that the user was created + user = User.objects.get(username="testuser@example.com") + assert user is not None + + # Check that the device was created + device = FCMDevice.objects.get( + user_id=user.id, registration_id="mock_device_token" + ) + assert device is not None + + # Check that the email was sent + mock_send_welcome_email.assert_called_once_with( + user.username, + user.username, + ) + + # Check the response status + assert response.status_code == status.HTTP_200_OK + assert "refresh" in response.data + assert "access" in response.data + + @patch("accounts.views.id_token.verify_oauth2_token") + @patch("accounts.models.send_welcome_email") + def test_google_login_ios_new_user( + self, mock_send_welcome_email, mock_verify_oauth2_token, api_client + ): + # Mock the token verification response + mock_verify_oauth2_token.return_value = { + "iss": "accounts.google.com", + "email": "testuser@example.com", + } + + # Define the URL for the GoogleLogin view + url = reverse("google_login") + + # Create a mock request + data = { + "token": "mock_token", + "type": 1, + } + + # Call the view + response = api_client.post(url, data, format="json") + + # Check that the user was created + user = User.objects.get(username="testuser@example.com") + assert user is not None + + # Check that the email was sent + mock_send_welcome_email.assert_called_once_with( + user.username, + user.username, + ) + + # Check the response status + assert response.status_code == status.HTTP_200_OK + assert "refresh" in response.data + assert "access" in response.data + + @patch("accounts.views.id_token.verify_oauth2_token") + @patch("accounts.models.send_welcome_email") + def test_google_login_ios_new_user_with_device( + self, mock_send_welcome_email, mock_verify_oauth2_token, api_client + ): + # Mock the token verification response + mock_verify_oauth2_token.return_value = { + "iss": "accounts.google.com", + "email": "testuser@example.com", + } + + # Define the URL for the GoogleLogin view + url = reverse("google_login") + + # Create a mock request + data = { + "token": "mock_token", + "device_token": "mock_device_token", + "type": 1, + } # Call the view response = api_client.post(url, data, format="json") diff --git a/accounts/tokens.py b/accounts/tokens.py index a4713a1..10e6dcc 100644 --- a/accounts/tokens.py +++ b/accounts/tokens.py @@ -7,3 +7,8 @@ def for_user(cls, user, device_token): token = super().for_user(user) token["device"] = device_token return token + + @classmethod + def for_user_without_device(cls, user): + token = super().for_user(user) + return token diff --git a/accounts/views.py b/accounts/views.py index 662ee04..f8ed2bf 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -17,30 +17,26 @@ JWT_SECRET_KEY = settings.SECRETS.get("JWT_SECRET_KEY") -GOOGLE_CLIENT_ID = settings.SECRETS.get("GCID") +GOOGLE_ANDROID_CLIENT_ID = settings.SECRETS.get("GCID") +GOOGLE_IOS_CLIENT_ID = settings.SECRETS.get("GOOGLE_IOS_CLIENT_ID") class GoogleLogin(APIView): + """ + request : token, device_token, type(0 : android, 1 : ios) + """ + authentication_classes = [] permission_classes = [AllowAny] def post(self, request): - token = request.data.get("token") - device_token = request.data.get("device_token") - if not device_token or not token: - sentry_sdk.capture_exception(LoginException()) - raise LoginException() try: - idinfo = id_token.verify_oauth2_token( - token, requests.Request(), audience=GOOGLE_CLIENT_ID - ) + device_type, token, device_token = self.validate_request(request) + idinfo = self.verify_token(device_type, token) if "accounts.google.com" in idinfo["iss"]: email = idinfo["email"] user = User.get_or_create_user(email) - FCMDevice.objects.get_or_create( - user=user, registration_id=device_token - ) - refresh = CustomRefreshToken.for_user(user, device_token) + refresh = self.handle_device_token(user, device_token) return Response( { "refresh": str(refresh), @@ -54,6 +50,39 @@ def post(self, request): {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) + def validate_request(self, request): + device_type = request.data.get("type", None) + token = request.data.get("token") + device_token = request.data.get("device_token", None) + if not token or device_type is None: + raise LoginException() + if device_type == 0 and not device_token: + raise LoginException("Device token is required for android login") + return device_type, token, device_token + + def verify_token(self, device_type, token): + if device_type == 0: # Android + return id_token.verify_oauth2_token( + token, + requests.Request(), + audience=GOOGLE_ANDROID_CLIENT_ID, + ) + elif device_type == 1: # iOS + return id_token.verify_oauth2_token( + token, requests.Request(), audience=GOOGLE_IOS_CLIENT_ID + ) + else: + raise LoginException("Invalid device type") + + def handle_device_token(self, user, device_token): + if device_token: + FCMDevice.objects.get_or_create( + user=user, registration_id=device_token + ) + return CustomRefreshToken.for_user(user, device_token) + else: + return CustomRefreshToken.for_user_without_device(user) + class UserRetrieveView(APIView): serializer_class = UserSerializer @@ -120,9 +149,8 @@ def patch(self, request): class AndroidClientView(APIView): def get(self, request): try: - ANDROID_CLIENT_ID = settings.SECRETS.get("ANDROID_CLIENT_ID") return Response( - {"android_client_id": ANDROID_CLIENT_ID}, + {"android_client_id": GOOGLE_ANDROID_CLIENT_ID}, status=status.HTTP_200_OK, ) except Exception as e: @@ -136,7 +164,6 @@ def get(self, request): class IOSClientView(APIView): def get(self, request): try: - GOOGLE_IOS_CLIENT_ID = settings.SECRETS.get("GOOGLE_IOS_CLIENT_ID") return Response( {"ios_client_id": GOOGLE_IOS_CLIENT_ID}, status=status.HTTP_200_OK, From d9ef7f26a4c85ee605ff34a5b2ddc6916f20bc53 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 19 Oct 2024 06:26:52 +0900 Subject: [PATCH 177/229] feat :apple social login --- accounts/views.py | 147 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/accounts/views.py b/accounts/views.py index f8ed2bf..8509539 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,6 +1,10 @@ +from datetime import timedelta + +import jwt import sentry_sdk from django.conf import settings from django.contrib.auth import get_user_model +from django.utils import timezone from fcm_django.models import FCMDevice from google.auth.transport import requests from google.oauth2 import id_token @@ -84,6 +88,149 @@ def handle_device_token(self, user, device_token): return CustomRefreshToken.for_user_without_device(user) +class AppleLogin(APIView): + """ + request : token, device_token, type(0 : android, 1 : ios) + """ + + ACCESS_TOKEN_URL = "https://appleid.apple.com/auth/token" + APPLE_APP_ID = settings.SECRETS.get("APPLE_APP_ID") + APPLE_SERVICE_ID = settings.SECRETS.get("APPLE_SERVICE_ID") + APPLE_CERTICATE_FILE = settings.SECRETS.get("APPLE_CERTICATE_FILE") + APPLE_KEY = settings.SECRETS.get("APPLE_KEY") + APPLE_KEY_ID = settings.SECRETS.get("APPLE_KEY_ID") + APPLE_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys" + + authentication_classes = [] + permission_classes = [AllowAny] + + def post(self, request): + try: + device_type, token, device_token = self.validate_request(request) + idinfo = self.verify_token(device_type, token) + if "accounts.google.com" in idinfo["iss"]: + email = idinfo["email"] + user = User.get_or_create_user(email) + refresh = self.handle_device_token(user, device_token) + return Response( + { + "refresh": str(refresh), + "access": str(refresh.access_token), + }, + status=status.HTTP_200_OK, + ) + except Exception as e: + sentry_sdk.capture_exception(e) + return Response( + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) + + def verify_token(self, access_token): + """ + Finish the auth process once the access_token was retrieved + Get the email from ID token received from apple + """ + + client_id, client_secret = self.get_key_and_secret() + + headers = {"content-type": "application/x-www-form-urlencoded"} + data = { + "client_id": client_id, + "client_secret": client_secret, + "refresh_token": access_token, + "grant_type": "refresh_token", + } + try: + response = requests.post( + AppleLogin.ACCESS_TOKEN_URL, data=data, headers=headers + ) + response_dict = response.json() + id_token = response_dict.get("id_token", None) + + if id_token: + decoded_user_info = self.verify_apple_token(id_token) + return decoded_user_info + except Exception as e: + sentry_sdk.capture_exception(e) + raise Exception(e) + + raise LoginException("Invalid token") + + def verify_apple_token(self, token): + # Fetch Apple's public keys + response = requests.get(AppleLogin.APPLE_PUBLIC_KEYS_URL) + if response.status_code != 200: + raise LoginException("Unable to fetch Apple's public keys") + apple_public_keys = response.json()["keys"] + + # Decode the token header to get the key ID + headers = jwt.get_unverified_header(token) + kid = headers["kid"] + + # Find the corresponding public key + key = next((k for k in apple_public_keys if k["kid"] == kid), None) + if key is None: + raise LoginException("Invalid token") + + # Construct the public key + public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key) + + # Verify the token + try: + idinfo = jwt.decode( + token, + public_key, + algorithms=["RS256"], + audience=AppleLogin.APPLE_SERVICE_ID, + issuer="https://appleid.apple.com", + ) + except jwt.ExpiredSignatureError: + raise LoginException("Token has expired") + except jwt.InvalidTokenError: + raise LoginException("Invalid token") + + return idinfo + + def get_key_and_secret(self): + headers = {"alg": "ES256", "kid": AppleLogin.APPLE_KEY_ID} + + payload = { + "iss": AppleLogin.APPLE_KEY, + "iat": timezone.now(), + "exp": timezone.now() + timedelta(days=1), + "aud": "https://appleid.apple.com", + "sub": AppleLogin.APPLE_SERVICE_ID, + } + + client_secret = jwt.encode( + payload, + AppleLogin.APPLE_CERTICATE_FILE, + algorithm="ES256", + headers=headers, + ).decode("utf-8") + + return AppleLogin.APPLE_SERVICE_ID, client_secret + + def validate_request(self, request): + device_type = request.data.get("type", None) + token = request.data.get("token") + device_token = request.data.get("device_token", None) + if not token or device_type is None: + raise LoginException() + if device_type == 0 and not device_token: + raise LoginException("Device token is required for android login") + return device_type, token, device_token + + def handle_device_token(self, user, device_token): + if device_token: + FCMDevice.objects.get_or_create( + user=user, registration_id=device_token + ) + return CustomRefreshToken.for_user(user, device_token) + else: + return CustomRefreshToken.for_user_without_device(user) + + class UserRetrieveView(APIView): serializer_class = UserSerializer queryset = User.objects.all() From b84394c133eeb555bd999d7a5fe55b47ba0d76b8 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 19 Oct 2024 21:17:15 +0900 Subject: [PATCH 178/229] fix : async model --- conftest.py | 4 +- onestep_be/settings.py | 4 +- requirements.txt | 4 + .../tests/test_recommend_check_rate_limit.py | 68 ++++++++++-- todos/views.py | 104 ++++++++++-------- 5 files changed, 121 insertions(+), 63 deletions(-) diff --git a/conftest.py b/conftest.py index 19c8795..d07a688 100644 --- a/conftest.py +++ b/conftest.py @@ -145,8 +145,8 @@ def recommend_result(): Mock( message=Mock( content=( - '{"id": 1, "content": "subtask", "start_date": "2024-09-01", ' # noqa - '"end_date": "2024-09-24", "category_id": 1 ' + '{"id": 1, "content": "study algebra", "date": "2024-09-01", ' # noqa: E501 + '"due_time": "None", "category_id": 1 ' '"is_completed": false, "children": []}' ) ) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 754957a..0d9e551 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -18,7 +18,7 @@ import pymysql import resend import sentry_sdk -from openai import OpenAI +from openai import AsyncOpenAI from sentry_sdk.integrations.django import DjangoIntegration from accounts.aws import get_secret @@ -189,7 +189,7 @@ # Add OpenAI API Key OPENAI_API_KEY = SECRETS.get("OPENAI_API_KEY") -client = OpenAI(api_key=OPENAI_API_KEY) +openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY) # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators diff --git a/requirements.txt b/requirements.txt index b6778e5..59b5aa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -111,3 +111,7 @@ django_crontab==0.7.1 Faker==27.0.0 django-lexorank==0.1.3 pymysql==1.1.1 +py-cpuinfo==9.0.0 +pytest-benchmark==4.0.0 +pytest_asyncio==0.24.0 +debugpy==1.8.7 diff --git a/todos/tests/test_recommend_check_rate_limit.py b/todos/tests/test_recommend_check_rate_limit.py index 0227268..81d5f69 100644 --- a/todos/tests/test_recommend_check_rate_limit.py +++ b/todos/tests/test_recommend_check_rate_limit.py @@ -1,42 +1,69 @@ -import time +import asyncio from unittest.mock import patch +import httpx +import pytest from django.urls import reverse from rest_framework import status +from rest_framework_simplejwt.tokens import AccessToken +@pytest.mark.django_db +@pytest.mark.asyncio @patch("todos.views.client.chat.completions.create") -def test_rate_limit_exceeded( - mock_llm, authenticated_client, create_todo, recommend_result +async def test_rate_limit_exceeded( + mock_llm, create_user, create_todo, recommend_result ): mock_llm.return_value = recommend_result - url = reverse("recommend") - response = authenticated_client.get(url, {"todo_id": create_todo.id}) + url = "https://dev.stepby.one/todos/recommend/" + access_token = str(AccessToken.for_user(create_user)) + + async with httpx.AsyncClient() as client: + # Attach the Authorization header with the Bearer token + client.headers.update( + { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + ) + response = await client.get(url, params={"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK - response = authenticated_client.get(url, {"todo_id": create_todo.id}) - assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS - assert response.data["error"] == "Rate limit exceeded" + async with httpx.AsyncClient() as client: + # Attach the Authorization header with the Bearer token + client.headers.update( + { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + ) + response = await client.get(url, params={"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS + assert response.json()["error"] == "Rate limit exceeded" +@pytest.mark.django_db +@pytest.mark.asyncio @patch("todos.views.client.chat.completions.create") -def test_rate_limit_passed( +async def test_rate_limit_passed( mock_llm, authenticated_client, create_todo, recommend_result ): - url = reverse("recommend") + url = "https://dev.stepby.one/todos/recommend/" mock_llm.return_value = recommend_result response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK - time.sleep(10) + await asyncio.sleep(10) # Non-blocking sleep response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK +@pytest.mark.django_db +@pytest.mark.asyncio @patch("todos.views.client.chat.completions.create") -def test_rate_limit_premium( +async def test_rate_limit_premium( mock_llm, authenticated_client, create_user, create_todo, recommend_result ): create_user.is_premium = True @@ -50,3 +77,20 @@ def test_rate_limit_premium( response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +@pytest.mark.asyncio +async def test_recommend_benchmark( + benchmark, authenticated_client, create_todo +): + url = reverse("recommend") + + async def async_benchmark(): + response = await authenticated_client.get( + url, {"todo_id": create_todo.id} + ) + return response + + response = await benchmark(async_benchmark) + assert response.status_code == status.HTTP_200_OK diff --git a/todos/views.py b/todos/views.py index 19f0ac4..219652f 100644 --- a/todos/views.py +++ b/todos/views.py @@ -1,4 +1,7 @@ # todos/views.py + + +import asyncio import json import sentry_sdk @@ -9,7 +12,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from onestep_be.settings import client +from onestep_be.settings import openai_client from todos.firebase_messaging import send_push_notification_device from todos.models import Category, SubTodo, Todo, UserLastUsage from todos.serializers import ( @@ -51,7 +54,7 @@ class TodoView(APIView): def post(self, request): """ - 이 함수는 todo를 생성하는 함수입니다. - - 입력 : start_date, end_date, content, category, parent_id + - 입력 : date, due_time, content, category, parent_id - content 는 암호화 되어야 합니다. - category_id 는 category에 존재해야합니다. - content는 1자 이상 50자 이하여야합니다. @@ -166,7 +169,7 @@ def patch(self, request): """ - 이 함수는 todo를 수정하는 함수입니다. - 입력 : todo_id, 수정 내용 - - 수정 내용은 content, category, start_date, end_date 중 하나 이상이어야 합니다. + - 수정 내용은 content, category, date, due_time 중 하나 이상이어야 합니다. - rank 의 경우 아래와 같이 제시된다. "rank" : { "prev_id" : 1, @@ -284,7 +287,6 @@ def post(self, request): - 입력 : todo, date, content - subtodo 는 리스트에 여러 객체가 들어간 형태를 가집니다. - content 는 암호화 되어야 합니다(// 미정) - - date 는 parent의 start_date와 end_date의 사이여야 합니다. """ set_sentry_user(request.user) data = request.data @@ -767,18 +769,13 @@ class RecommendSubTodo(APIView): operation_summary="Recommend subtodo", responses={200: SubTodoSerializer}, ) - async def get(self, request): + def get(self, request): """ - 이 함수는 sub todo를 추천하는 함수입니다. - - 입력 : todo_id, recommend_category - - todo_id에 해당하는 todo_id 의 Contents 를 바탕으로 sub todo를 추천합니다. - - 커스텀의 경우 사용자의 이전 기록들을 바탕으로 추천합니다. - - 추천할 때의 subtodo 는 약 1시간의 작업으로 openAI 의 api를 통해 추천합니다. - """ # noqa: E501 - + """ set_sentry_user(request.user) - user_id = request.user.id + user_id = request.GET.get("user_id") flag, message = UserLastUsage.check_rate_limit( user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS ) @@ -793,43 +790,20 @@ async def get(self, request): {"error": "todo_id must be provided"}, status=status.HTTP_400_BAD_REQUEST, ) + try: + # 비동기적으로 OpenAI API 호출 처리 todo = Todo.objects.get_with_id(id=todo_id) - completion = client.chat.completions.create( - model="gpt-4o-mini", - messages=[ - { - "role": "system", - "content": """너는 퍼스널 매니저야. - 너가 하는 일은 이 사람이 할 이야기를 듣고 약 1시간 정도면 끝낼 수 있도록 작업을 나눠주는 식으로 진행할 거야. - 아래는 너가 나눠줄 작업 형식이야. - { id : 1, content: "3학년 2학기 운영체제 중간고사 준비", start_date="2024-09-01", end_date="2024-09-24"} - 이런 형식으로 작성된 작업을 받았을 때 너는 이 작업을 어떻게 나눠줄 것인지를 알려주면 돼. - Output a JSON object structured like: - {id, content, start_date, end_date, category_id, order, is_completed, children : [ - {content, date, todo(parent todo id)}, ... ,{content, date, todo(parent todo id)}]} - [조건] - - date 는 부모의 start_date를 따를 것 - - 작업은 한 서브투두를 해결하는데 1시간 정도로 이루어지도록 제시할 것 - - 언어는 주어진 todo content의 언어에 따를 것 - """, # noqa: E501 - }, - { - "role": "user", - "content": f"id: {todo.id}, \ - content: {todo.content}, \ - start_date: {todo.start_date}, \ - end_date: {todo.end_date}, \ - category_id: {todo.category_id}, \ - order: {todo.order}, \ - is_completed: {todo.is_completed}", - }, - ], - response_format={"type": "json_object"}, - ) - sentry_sdk.capture_message( - "SubTodo recommended", level="info", extra={"todo_id": todo.id} - ) + todo_data = { + "id": todo.id, + "content": todo.content, + "date": todo.date, + "due_time": todo.due_time, + "category_id": todo.category_id, + "rank": todo.rank, + "is_completed": todo.is_completed, + } + completion = asyncio.run(self.get_openai_completion(todo_data)) return Response( json.loads(completion.choices[0].message.content), status=status.HTTP_200_OK, @@ -842,6 +816,42 @@ async def get(self, request): ) except Exception as e: sentry_sdk.capture_exception(e) + print("error", e) return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) + + # 비동기적으로 OpenAI API를 호출하는 함수 + async def get_openai_completion(self, todo): + return await openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + { + "role": "system", + "content": """너는 퍼스널 매니저야. + 너가 하는 일은 이 사람이 할 이야기를 듣고 약 1시간 정도면 끝낼 수 있도록 작업을 나눠주는 식으로 진행할 거야. + 아래는 너가 나눠줄 작업 형식이야. + { id : 1, content: "3학년 2학기 운영체제 중간고사 준비", date="2024-09-01", due_time="2024-09-24"} + 이런 형식으로 작성된 작업을 받았을 때 너는 이 작업을 어떻게 나눠줄 것인지를 알려주면 돼. + Output a JSON object structured like: + {id, content, date, due_time, category_id, rank, is_completed, children : [ + {content, date, todo(parent todo id)}, ... ,{content, date, todo(parent todo id)}]} + [조건] + - date 는 부모의 date를 따를 것 + - 작업은 한 서브투두를 해결하는데 1시간 정도로 이루어지도록 제시할 것 + - 언어는 주어진 todo content의 언어에 따를 것 + """, # noqa: E501 + }, + { + "role": "user", + "content": f"id: {todo["id"]}, \ + content: {todo["content"]}, \ + date: {todo["date"]}, \ + due_time: {todo["due_time"]}, \ + category_id: {todo["category_id"]}, \ + rank: {todo["rank"]}, \ + is_completed: {todo["is_completed"]}", + }, + ], + response_format={"type": "json_object"}, + ) From d12f39bc27bc508d04abbb8fdd446ccba691db69 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 19 Oct 2024 21:50:25 +0900 Subject: [PATCH 179/229] =?UTF-8?q?fix=20:=20test=20code=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conftest.py | 2 +- .../tests/test_recommend_check_rate_limit.py | 19 ++++++++++++------- todos/views.py | 3 +-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/conftest.py b/conftest.py index d07a688..95dc42c 100644 --- a/conftest.py +++ b/conftest.py @@ -146,7 +146,7 @@ def recommend_result(): message=Mock( content=( '{"id": 1, "content": "study algebra", "date": "2024-09-01", ' # noqa: E501 - '"due_time": "None", "category_id": 1 ' + '"due_time": "None", "category_id": 1, ' '"is_completed": false, "children": []}' ) ) diff --git a/todos/tests/test_recommend_check_rate_limit.py b/todos/tests/test_recommend_check_rate_limit.py index 81d5f69..eb994ca 100644 --- a/todos/tests/test_recommend_check_rate_limit.py +++ b/todos/tests/test_recommend_check_rate_limit.py @@ -1,5 +1,6 @@ -import asyncio -from unittest.mock import patch +# import asyncio +import time +from unittest.mock import AsyncMock, patch import httpx import pytest @@ -10,7 +11,7 @@ @pytest.mark.django_db @pytest.mark.asyncio -@patch("todos.views.client.chat.completions.create") +@patch("todos.views.openai_client.chat.completions.create") async def test_rate_limit_exceeded( mock_llm, create_user, create_todo, recommend_result ): @@ -27,6 +28,8 @@ async def test_rate_limit_exceeded( } ) response = await client.get(url, params={"todo_id": create_todo.id}) + if response.status_code == status.HTTP_400_BAD_REQUEST: + print(response) assert response.status_code == status.HTTP_200_OK async with httpx.AsyncClient() as client: @@ -44,8 +47,10 @@ async def test_rate_limit_exceeded( @pytest.mark.django_db @pytest.mark.asyncio -@patch("todos.views.client.chat.completions.create") -async def test_rate_limit_passed( +@patch( + "todos.views.openai_client.chat.completions.create", new_callable=AsyncMock +) +def test_rate_limit_passed( mock_llm, authenticated_client, create_todo, recommend_result ): url = "https://dev.stepby.one/todos/recommend/" @@ -54,7 +59,7 @@ async def test_rate_limit_passed( response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK - await asyncio.sleep(10) # Non-blocking sleep + time.sleep(10) # Non-blocking sleep response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK @@ -62,7 +67,7 @@ async def test_rate_limit_passed( @pytest.mark.django_db @pytest.mark.asyncio -@patch("todos.views.client.chat.completions.create") +@patch("todos.views.openai_client.chat.completions.create") async def test_rate_limit_premium( mock_llm, authenticated_client, create_user, create_todo, recommend_result ): diff --git a/todos/views.py b/todos/views.py index 219652f..c2a0bd0 100644 --- a/todos/views.py +++ b/todos/views.py @@ -775,7 +775,7 @@ def get(self, request): """ set_sentry_user(request.user) - user_id = request.GET.get("user_id") + user_id = request.user.id flag, message = UserLastUsage.check_rate_limit( user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS ) @@ -816,7 +816,6 @@ def get(self, request): ) except Exception as e: sentry_sdk.capture_exception(e) - print("error", e) return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) From 64f328adc6a395ec9636bbabd961bcf0e88b99fb Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sun, 20 Oct 2024 17:06:26 +0900 Subject: [PATCH 180/229] =?UTF-8?q?fix=20:=20apple=20social=20login=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 118 ++++++++++++---------------------------------- 1 file changed, 31 insertions(+), 87 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index 8509539..eaf38c7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,10 +1,7 @@ -from datetime import timedelta - import jwt import sentry_sdk from django.conf import settings from django.contrib.auth import get_user_model -from django.utils import timezone from fcm_django.models import FCMDevice from google.auth.transport import requests from google.oauth2 import id_token @@ -60,8 +57,6 @@ def validate_request(self, request): device_token = request.data.get("device_token", None) if not token or device_type is None: raise LoginException() - if device_type == 0 and not device_token: - raise LoginException("Device token is required for android login") return device_type, token, device_token def verify_token(self, device_type, token): @@ -93,12 +88,7 @@ class AppleLogin(APIView): request : token, device_token, type(0 : android, 1 : ios) """ - ACCESS_TOKEN_URL = "https://appleid.apple.com/auth/token" - APPLE_APP_ID = settings.SECRETS.get("APPLE_APP_ID") APPLE_SERVICE_ID = settings.SECRETS.get("APPLE_SERVICE_ID") - APPLE_CERTICATE_FILE = settings.SECRETS.get("APPLE_CERTICATE_FILE") - APPLE_KEY = settings.SECRETS.get("APPLE_KEY") - APPLE_KEY_ID = settings.SECRETS.get("APPLE_KEY_ID") APPLE_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys" authentication_classes = [] @@ -107,109 +97,65 @@ class AppleLogin(APIView): def post(self, request): try: device_type, token, device_token = self.validate_request(request) - idinfo = self.verify_token(device_type, token) - if "accounts.google.com" in idinfo["iss"]: - email = idinfo["email"] - user = User.get_or_create_user(email) - refresh = self.handle_device_token(user, device_token) - return Response( - { - "refresh": str(refresh), - "access": str(refresh.access_token), - }, - status=status.HTTP_200_OK, - ) + # verify apple token and get email + email = self.verify_token(device_type, token) + user = User.get_or_create_user(email) + refresh = self.handle_device_token(user, device_token) + return Response( + { + "refresh": str(refresh), + "access": str(refresh.access_token), + }, + status=status.HTTP_200_OK, + ) except Exception as e: sentry_sdk.capture_exception(e) return Response( {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - def verify_token(self, access_token): + def verify_token(self, token): """ Finish the auth process once the access_token was retrieved Get the email from ID token received from apple """ - - client_id, client_secret = self.get_key_and_secret() - - headers = {"content-type": "application/x-www-form-urlencoded"} - data = { - "client_id": client_id, - "client_secret": client_secret, - "refresh_token": access_token, - "grant_type": "refresh_token", - } - try: - response = requests.post( - AppleLogin.ACCESS_TOKEN_URL, data=data, headers=headers - ) - response_dict = response.json() - id_token = response_dict.get("id_token", None) - - if id_token: - decoded_user_info = self.verify_apple_token(id_token) - return decoded_user_info - except Exception as e: - sentry_sdk.capture_exception(e) - raise Exception(e) - - raise LoginException("Invalid token") - - def verify_apple_token(self, token): - # Fetch Apple's public keys + # Apple 공개 키 가져오기 response = requests.get(AppleLogin.APPLE_PUBLIC_KEYS_URL) if response.status_code != 200: raise LoginException("Unable to fetch Apple's public keys") apple_public_keys = response.json()["keys"] + # JWT 디코드 및 서명 검증 + header = jwt.get_unverified_header(token) - # Decode the token header to get the key ID - headers = jwt.get_unverified_header(token) - kid = headers["kid"] + # 헤더의 'kid'와 일치하는 Apple 공개 키 선택 + public_key = next( + key + for key in apple_public_keys["keys"] + if key["kid"] == header["kid"] + ) - # Find the corresponding public key - key = next((k for k in apple_public_keys if k["kid"] == kid), None) - if key is None: + if public_key is None: raise LoginException("Invalid token") - # Construct the public key - public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key) - # Verify the token try: - idinfo = jwt.decode( - token, + decoded_token = jwt.decode( + id_token, public_key, - algorithms=["RS256"], + algorithms=["RS256"], # Apple의 서명 알고리즘 audience=AppleLogin.APPLE_SERVICE_ID, issuer="https://appleid.apple.com", ) + return decoded_token.get("email") except jwt.ExpiredSignatureError: + sentry_sdk.capture_exception(jwt.ExpiredSignatureError) raise LoginException("Token has expired") except jwt.InvalidTokenError: + sentry_sdk.capture_exception(jwt.ExpiredSignatureError) raise LoginException("Invalid token") - - return idinfo - - def get_key_and_secret(self): - headers = {"alg": "ES256", "kid": AppleLogin.APPLE_KEY_ID} - - payload = { - "iss": AppleLogin.APPLE_KEY, - "iat": timezone.now(), - "exp": timezone.now() + timedelta(days=1), - "aud": "https://appleid.apple.com", - "sub": AppleLogin.APPLE_SERVICE_ID, - } - - client_secret = jwt.encode( - payload, - AppleLogin.APPLE_CERTICATE_FILE, - algorithm="ES256", - headers=headers, - ).decode("utf-8") - - return AppleLogin.APPLE_SERVICE_ID, client_secret + except Exception as e: + sentry_sdk.capture_exception(e) + raise LoginException("An unexpected error occurred") def validate_request(self, request): device_type = request.data.get("type", None) @@ -217,8 +163,6 @@ def validate_request(self, request): device_token = request.data.get("device_token", None) if not token or device_type is None: raise LoginException() - if device_type == 0 and not device_token: - raise LoginException("Device token is required for android login") return device_type, token, device_token def handle_device_token(self, user, device_token): From 4c544e7a1520bcd2b6248887576631d2ab8add5e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sun, 20 Oct 2024 17:32:02 +0900 Subject: [PATCH 181/229] fix : sentry Environment settings --- onestep_be/setting/dev.py | 5 +++-- onestep_be/setting/prod.py | 4 ++-- onestep_be/setting/test.py | 7 ++++--- onestep_be/settings.py | 39 ++++++++++++++++++++------------------ 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/onestep_be/setting/dev.py b/onestep_be/setting/dev.py index a6f9eca..efe6eee 100644 --- a/onestep_be/setting/dev.py +++ b/onestep_be/setting/dev.py @@ -1,5 +1,5 @@ -from onestep_be.settings import * - +from accounts.aws import get_secret +from onestep_be.settings import set_sentry_setting SECRETS = eval(get_secret()) @@ -14,3 +14,4 @@ } } +set_sentry_setting("Development") diff --git a/onestep_be/setting/prod.py b/onestep_be/setting/prod.py index f530887..ddb543c 100644 --- a/onestep_be/setting/prod.py +++ b/onestep_be/setting/prod.py @@ -1,6 +1,5 @@ -from onestep_be.settings import * from accounts.aws import get_secret - +from onestep_be.settings import set_sentry_setting ALLOWED_HOSTS = ["*"] DEBUG = False @@ -17,3 +16,4 @@ } } +set_sentry_setting("Production") diff --git a/onestep_be/setting/test.py b/onestep_be/setting/test.py index 4d11be8..4c79a50 100644 --- a/onestep_be/setting/test.py +++ b/onestep_be/setting/test.py @@ -1,8 +1,9 @@ -from onestep_be.settings import * - +from onestep_be.settings import MIDDLEWARE, set_sentry_setting SKIP_AUTHENTICATION = True if SKIP_AUTHENTICATION: MIDDLEWARE.insert(0, "accounts.middleware.SkipAuthMiddleware") - MIDDLEWARE.remove("todos.middleware.FCMAlarmMiddleware") \ No newline at end of file + MIDDLEWARE.remove("todos.middleware.FCMAlarmMiddleware") + +set_sentry_setting("Testing") diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 754957a..9f3b51f 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -249,24 +249,27 @@ # sentry settings -sentry_sdk.init( - dsn="https://9425334e0e90c405218fa9613cea9a03@o4507736964136960.ingest.us.sentry.io/4507763025117184", - traces_sample_rate=0.1, - release=PROJECT_VERSION, - profiles_sample_rate=0.1, - integrations=[ - DjangoIntegration( - transaction_style="url", - middleware_spans=True, - signals_spans=True, - signals_denylist=[ - django.db.models.signals.pre_init, - django.db.models.signals.post_init, - ], - cache_spans=False, - ), - ], -) + +def set_sentry_setting(SENTRY_ENVIRONMENT): + sentry_sdk.init( + dsn="https://9425334e0e90c405218fa9613cea9a03@o4507736964136960.ingest.us.sentry.io/4507763025117184", + traces_sample_rate=0.1, + release=PROJECT_VERSION, + profiles_sample_rate=0.1, + environment=SENTRY_ENVIRONMENT, + integrations=[ + DjangoIntegration( + transaction_style="url", + middleware_spans=True, + signals_spans=True, + signals_denylist=[ + django.db.models.signals.pre_init, + django.db.models.signals.post_init, + ], + cache_spans=False, + ), + ], + ) resend.api_key = SECRETS.get("RESEND") From 8c8dd0acd54228c2d417aa7f991faf92498e209f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sun, 20 Oct 2024 17:34:43 +0900 Subject: [PATCH 182/229] fix : sentry Environment settings --- onestep_be/setting/dev.py | 4 ++-- onestep_be/setting/test.py | 4 ++-- onestep_be/settings.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/onestep_be/setting/dev.py b/onestep_be/setting/dev.py index efe6eee..726ca66 100644 --- a/onestep_be/setting/dev.py +++ b/onestep_be/setting/dev.py @@ -1,5 +1,5 @@ from accounts.aws import get_secret -from onestep_be.settings import set_sentry_setting +from onestep_be.settings import set_sentry_init_setting SECRETS = eval(get_secret()) @@ -14,4 +14,4 @@ } } -set_sentry_setting("Development") +set_sentry_init_setting("Development") diff --git a/onestep_be/setting/test.py b/onestep_be/setting/test.py index 4c79a50..d04ee6d 100644 --- a/onestep_be/setting/test.py +++ b/onestep_be/setting/test.py @@ -1,4 +1,4 @@ -from onestep_be.settings import MIDDLEWARE, set_sentry_setting +from onestep_be.settings import MIDDLEWARE, set_sentry_init_setting SKIP_AUTHENTICATION = True @@ -6,4 +6,4 @@ MIDDLEWARE.insert(0, "accounts.middleware.SkipAuthMiddleware") MIDDLEWARE.remove("todos.middleware.FCMAlarmMiddleware") -set_sentry_setting("Testing") +set_sentry_init_setting("Testing") diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 9f3b51f..92e8650 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -250,7 +250,7 @@ # sentry settings -def set_sentry_setting(SENTRY_ENVIRONMENT): +def set_sentry_init_setting(SENTRY_ENVIRONMENT): sentry_sdk.init( dsn="https://9425334e0e90c405218fa9613cea9a03@o4507736964136960.ingest.us.sentry.io/4507763025117184", traces_sample_rate=0.1, From 1f61e00b2fbb8cf96c6b3337b32985add504532f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 21 Oct 2024 17:58:04 +0900 Subject: [PATCH 183/229] =?UTF-8?q?fix:=20dockerfile=20=EC=98=A4=ED=83=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.dev | 2 +- Dockerfile.prod | 2 +- Dockerfile.test | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 7af9dcb..a8e4164 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_workers.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.dev"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod index 01457ee..76e79c6 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_workers.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.prod"] diff --git a/Dockerfile.test b/Dockerfile.test index f28016f..560419e 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_workers.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.test"] From 70eec3308b408df374e482652c745c90cdc9621f Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 21 Oct 2024 18:06:02 +0900 Subject: [PATCH 184/229] =?UTF-8?q?fix:=20dockerfile=20=EC=98=A4=ED=83=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.dev | 2 +- Dockerfile.prod | 2 +- Dockerfile.test | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index a8e4164..7af9dcb 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_workers.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.dev"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod index 76e79c6..01457ee 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_workers.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.prod"] diff --git a/Dockerfile.test b/Dockerfile.test index 560419e..f28016f 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_workers.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.test"] From 7f2b07d043a12498ee5bf3169801c939ce514488 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 21 Oct 2024 18:08:11 +0900 Subject: [PATCH 185/229] =?UTF-8?q?fix:=20dockerfile=20=EC=98=A4=ED=83=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20uvicorn=5Fworker=20->=20uvicorn.workers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile.dev | 2 +- Dockerfile.prod | 2 +- Dockerfile.test | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 7af9dcb..1bd1e3e 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn.workers.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.dev gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.dev"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod index 01457ee..62e1f3c 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn.workers.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.prod gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.prod"] diff --git a/Dockerfile.test b/Dockerfile.test index f28016f..7f1ab2a 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -15,6 +15,6 @@ COPY . /app/ EXPOSE 8000 # 서버 실행 명령 -CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker"] +CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn.workers.UvicornWorker"] # CMD ["sh", "-c", "python manage.py makemigrations && python manage.py migrate && DJANGO_SETTINGS_MODULE=onestep_be.setting.test gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application"] # CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --settings=onestep_be.setting.test"] From 56ea674aca85501c220e11fe4837c017c4116e1d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 21 Oct 2024 19:47:15 +0900 Subject: [PATCH 186/229] =?UTF-8?q?fix=20:=20Middleware=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/urls.py | 3 ++- onestep_be/setting/test.py | 4 +--- todos/middleware.py | 46 -------------------------------------- 3 files changed, 3 insertions(+), 50 deletions(-) delete mode 100644 todos/middleware.py diff --git a/accounts/urls.py b/accounts/urls.py index 1677160..e0bbd83 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -7,6 +7,7 @@ from accounts.views import ( AndroidClientView, GoogleLogin, + IOSClientView, UserRetrieveView, ) @@ -16,5 +17,5 @@ path("login/google/", GoogleLogin.as_view(), name="google_login"), path("user/", UserRetrieveView.as_view(), name="user"), path("android/", AndroidClientView.as_view(), name="android"), - path("ios/", AndroidClientView.as_view(), name="ios"), + path("ios/", IOSClientView.as_view(), name="ios"), ] diff --git a/onestep_be/setting/test.py b/onestep_be/setting/test.py index 4d11be8..d37a0a4 100644 --- a/onestep_be/setting/test.py +++ b/onestep_be/setting/test.py @@ -1,8 +1,6 @@ -from onestep_be.settings import * - +from onestep_be.settings import MIDDLEWARE SKIP_AUTHENTICATION = True if SKIP_AUTHENTICATION: MIDDLEWARE.insert(0, "accounts.middleware.SkipAuthMiddleware") - MIDDLEWARE.remove("todos.middleware.FCMAlarmMiddleware") \ No newline at end of file diff --git a/todos/middleware.py b/todos/middleware.py deleted file mode 100644 index 8b33277..0000000 --- a/todos/middleware.py +++ /dev/null @@ -1,46 +0,0 @@ -from fcm_django.models import FCMDevice -import firebase_admin -import os -from firebase_admin import credentials, messaging -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onestep_be.settings') -from django.conf import settings -from fcm_django.models import FCMDevice -from todos.firebase_messaging import send_push_notification - - -FCM_ALARM_PATH_TODO = "/todos/todo" -FCM_ALARM_PATH_SUBTODO = "/todos/subtodo" -FCM_ALARM_PATH_CATEGORY = "/todos/category" - -FCM_ALARM_PATHS = [FCM_ALARM_PATH_TODO, FCM_ALARM_PATH_SUBTODO, FCM_ALARM_PATH_CATEGORY] -FCM_ALARM_METHODS = [ - "POST", "PATCH", "DELETE" -] - - -class FCMAlarmMiddleware: - - def __init__(self, get_response): - self.get_response = get_response - - def startswith_fcm_alarm_paths(self, path): - for p in FCM_ALARM_PATHS: - if path.startswith(p): - return True - return False - - def __call__(self, request): - - if request.method in FCM_ALARM_METHODS and self.startswith_fcm_alarm_paths(request.path): - fcm_token = request.auth.token - other_device = FCMDevice.objects.filter(user=request.user).exclude(registration_id=fcm_token) - - if other_device.exists(): - device_id = other_device.first().registration_id - - if request.path.startsWith(FCM_ALARM_PATH_TODO): - send_push_notification(device_id, "Todo", "") - elif request.path.startsWith(FCM_ALARM_PATH_SUBTODO): - send_push_notification(device_id, "Subtodo", "") - elif request.path.startsWith(FCM_ALARM_PATH_CATEGORY): - send_push_notification(device_id, "Category", "") From f20392eb47da6cf7c6e00778b902209fa8908daf Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 21 Oct 2024 19:57:28 +0900 Subject: [PATCH 187/229] =?UTF-8?q?fix=20:=20ios=20view=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/views.py b/accounts/views.py index 59113eb..374c592 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -151,7 +151,7 @@ def get(self, request): class IOSClientView(APIView): def get(self, request): try: - IOS_CLIENT_ID = settings.SECRETS.get("IOS_CLIENT_ID") + IOS_CLIENT_ID = settings.SECRETS.get("GOOGLE_IOS_CLIENT_ID") return Response( {"ios_client_id": IOS_CLIENT_ID}, status=status.HTTP_200_OK, From 386478d60a9cb67d4f329f6f367fc3d66a5254be Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 22 Oct 2024 00:29:14 +0900 Subject: [PATCH 188/229] =?UTF-8?q?feat=20:=20=EC=83=88=EB=A1=9C=EC=9A=B4?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=EC=9D=B8=EC=A7=80=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20response=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 4 +++- accounts/urls.py | 2 ++ accounts/views.py | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index 01db2dc..77c8895 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -47,6 +47,7 @@ class SocialProvider(models.TextChoices): @classmethod def get_or_create_user(self, email): try: + is_new = False user = User.objects.get(username=email) except User.DoesNotExist: user = User.objects.create(username=email, password="") @@ -54,6 +55,7 @@ def get_or_create_user(self, email): email, user.username, ) + is_new = True except Exception as e: sentry_sdk.capture_exception(e) sentry_sdk.capture_message( @@ -61,7 +63,7 @@ def get_or_create_user(self, email): ) raise e - return user + return user, is_new def delete_user(instance): instance.deleted_at = timezone.now() diff --git a/accounts/urls.py b/accounts/urls.py index e0bbd83..636d80d 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -6,6 +6,7 @@ from accounts.views import ( AndroidClientView, + AppleLogin, GoogleLogin, IOSClientView, UserRetrieveView, @@ -15,6 +16,7 @@ path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"), path("login/google/", GoogleLogin.as_view(), name="google_login"), + path("login/apple/", AppleLogin.as_view(), name="apple_login"), path("user/", UserRetrieveView.as_view(), name="user"), path("android/", AndroidClientView.as_view(), name="android"), path("ios/", IOSClientView.as_view(), name="ios"), diff --git a/accounts/views.py b/accounts/views.py index 344be86..2efa0a0 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -36,12 +36,13 @@ def post(self, request): idinfo = self.verify_token(device_type, token) if "accounts.google.com" in idinfo["iss"]: email = idinfo["email"] - user = User.get_or_create_user(email) + user, is_new = User.get_or_create_user(email) refresh = self.handle_device_token(user, device_token) return Response( { "refresh": str(refresh), "access": str(refresh.access_token), + "is_new": is_new, }, status=status.HTTP_200_OK, ) @@ -99,12 +100,13 @@ def post(self, request): device_type, token, device_token = self.validate_request(request) # verify apple token and get email email = self.verify_token(device_type, token) - user = User.get_or_create_user(email) + user, is_new = User.get_or_create_user(email) refresh = self.handle_device_token(user, device_token) return Response( { "refresh": str(refresh), "access": str(refresh.access_token), + "is_new": is_new, }, status=status.HTTP_200_OK, ) From c99692104f209fb04b71caffc5aded93eb71aa93 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 22 Oct 2024 16:34:52 +0900 Subject: [PATCH 189/229] fix : recommend Test benchmark --- .../tests/test_recommend_check_rate_limit.py | 68 ++++++------------- todos/views.py | 32 +++++---- 2 files changed, 38 insertions(+), 62 deletions(-) diff --git a/todos/tests/test_recommend_check_rate_limit.py b/todos/tests/test_recommend_check_rate_limit.py index eb994ca..9e57872 100644 --- a/todos/tests/test_recommend_check_rate_limit.py +++ b/todos/tests/test_recommend_check_rate_limit.py @@ -2,73 +2,54 @@ import time from unittest.mock import AsyncMock, patch -import httpx import pytest from django.urls import reverse from rest_framework import status -from rest_framework_simplejwt.tokens import AccessToken @pytest.mark.django_db @pytest.mark.asyncio -@patch("todos.views.openai_client.chat.completions.create") -async def test_rate_limit_exceeded( - mock_llm, create_user, create_todo, recommend_result +@patch( + "todos.views.openai_client.chat.completions.create", new_callable=AsyncMock +) +def test_rate_limit_exceeded( + mock_llm, authenticated_client, create_todo, recommend_result ): mock_llm.return_value = recommend_result - url = "https://dev.stepby.one/todos/recommend/" - access_token = str(AccessToken.for_user(create_user)) - - async with httpx.AsyncClient() as client: - # Attach the Authorization header with the Bearer token - client.headers.update( - { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/json", - } - ) - response = await client.get(url, params={"todo_id": create_todo.id}) - if response.status_code == status.HTTP_400_BAD_REQUEST: - print(response) + url = reverse("recommend") + + response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK - async with httpx.AsyncClient() as client: - # Attach the Authorization header with the Bearer token - client.headers.update( - { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/json", - } - ) - response = await client.get(url, params={"todo_id": create_todo.id}) - assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS - assert response.json()["error"] == "Rate limit exceeded" + response = authenticated_client.get(url, {"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS + assert response.json()["error"] == "Rate limit exceeded" @pytest.mark.django_db -@pytest.mark.asyncio @patch( "todos.views.openai_client.chat.completions.create", new_callable=AsyncMock ) def test_rate_limit_passed( mock_llm, authenticated_client, create_todo, recommend_result ): - url = "https://dev.stepby.one/todos/recommend/" + url = reverse("recommend") mock_llm.return_value = recommend_result response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK - time.sleep(10) # Non-blocking sleep + time.sleep(10) response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK @pytest.mark.django_db -@pytest.mark.asyncio -@patch("todos.views.openai_client.chat.completions.create") -async def test_rate_limit_premium( +@patch( + "todos.views.openai_client.chat.completions.create", new_callable=AsyncMock +) +def test_rate_limit_premium( mock_llm, authenticated_client, create_user, create_todo, recommend_result ): create_user.is_premium = True @@ -85,17 +66,10 @@ async def test_rate_limit_premium( @pytest.mark.django_db -@pytest.mark.asyncio -async def test_recommend_benchmark( - benchmark, authenticated_client, create_todo -): +def test_time_openAI(authenticated_client, create_todo, benchmark): url = reverse("recommend") - async def async_benchmark(): - response = await authenticated_client.get( - url, {"todo_id": create_todo.id} - ) - return response - - response = await benchmark(async_benchmark) + response = benchmark( + lambda: authenticated_client.get(url, {"todo_id": create_todo.id}) + ) assert response.status_code == status.HTTP_200_OK diff --git a/todos/views.py b/todos/views.py index c2a0bd0..7fd57d3 100644 --- a/todos/views.py +++ b/todos/views.py @@ -776,22 +776,25 @@ def get(self, request): set_sentry_user(request.user) user_id = request.user.id - flag, message = UserLastUsage.check_rate_limit( - user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS - ) - if flag is False: - return Response( - {"error": message}, status=status.HTTP_429_TOO_MANY_REQUESTS - ) - todo_id = request.GET.get("todo_id") - if todo_id is None: - sentry_sdk.capture_message("Todo_id not provided", level="error") - return Response( - {"error": "todo_id must be provided"}, - status=status.HTTP_400_BAD_REQUEST, + try: + flag, message = UserLastUsage.check_rate_limit( + user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS ) - try: + if flag is False: + return Response( + {"error": message}, + status=status.HTTP_429_TOO_MANY_REQUESTS, + ) + todo_id = request.GET.get("todo_id") + if todo_id is None: + sentry_sdk.capture_message( + "Todo_id not provided", level="error" + ) + return Response( + {"error": "todo_id must be provided"}, + status=status.HTTP_400_BAD_REQUEST, + ) # 비동기적으로 OpenAI API 호출 처리 todo = Todo.objects.get_with_id(id=todo_id) todo_data = { @@ -808,7 +811,6 @@ def get(self, request): json.loads(completion.choices[0].message.content), status=status.HTTP_200_OK, ) - except Todo.DoesNotExist as e: sentry_sdk.capture_exception(e) return Response( From c862790307a4cb375684f4ca0b82c9c0e6ac0633 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 22 Oct 2024 16:53:34 +0900 Subject: [PATCH 190/229] =?UTF-8?q?fix=20:device=20type=20=EC=83=81?= =?UTF-8?q?=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index 2efa0a0..8d44ae9 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -20,6 +20,8 @@ JWT_SECRET_KEY = settings.SECRETS.get("JWT_SECRET_KEY") GOOGLE_ANDROID_CLIENT_ID = settings.SECRETS.get("GCID") GOOGLE_IOS_CLIENT_ID = settings.SECRETS.get("GOOGLE_IOS_CLIENT_ID") +DEVICE_TYPE_ANDROID = 0 +DEVICE_TYPE_IOS = 1 class GoogleLogin(APIView): @@ -61,13 +63,13 @@ def validate_request(self, request): return device_type, token, device_token def verify_token(self, device_type, token): - if device_type == 0: # Android + if device_type == DEVICE_TYPE_ANDROID: # Android return id_token.verify_oauth2_token( token, requests.Request(), audience=GOOGLE_ANDROID_CLIENT_ID, ) - elif device_type == 1: # iOS + elif device_type == DEVICE_TYPE_IOS: # iOS return id_token.verify_oauth2_token( token, requests.Request(), audience=GOOGLE_IOS_CLIENT_ID ) From 6826a86eb12a6f8111330ce5c1ac0260b88ce3d7 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 22 Oct 2024 21:15:34 +0900 Subject: [PATCH 191/229] fix: sentry dsn --- onestep_be/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 0d9e551..5700b49 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -249,8 +249,9 @@ # sentry settings +SENTRY_DSN = SECRETS.get("SENTRY_DSN") sentry_sdk.init( - dsn="https://9425334e0e90c405218fa9613cea9a03@o4507736964136960.ingest.us.sentry.io/4507763025117184", + dsn=SENTRY_DSN, traces_sample_rate=0.1, release=PROJECT_VERSION, profiles_sample_rate=0.1, From c430acab609618d113a652ba9577a14c5e99c3b0 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Wed, 23 Oct 2024 17:21:47 +0900 Subject: [PATCH 192/229] fix : recommend api --- onestep_be/setting/dev.py | 3 - onestep_be/setting/prod.py | 3 - onestep_be/setting/test.py | 4 +- onestep_be/settings.py | 55 +++++++++-------- .../tests/test_recommend_check_rate_limit.py | 10 ++-- todos/tests/test_todo_post.py | 2 +- todos/views.py | 59 +++++++++---------- version.txt | 1 - 8 files changed, 67 insertions(+), 70 deletions(-) diff --git a/onestep_be/setting/dev.py b/onestep_be/setting/dev.py index 726ca66..06b96ea 100644 --- a/onestep_be/setting/dev.py +++ b/onestep_be/setting/dev.py @@ -1,5 +1,4 @@ from accounts.aws import get_secret -from onestep_be.settings import set_sentry_init_setting SECRETS = eval(get_secret()) @@ -13,5 +12,3 @@ "PORT": SECRETS.get("DB_PORT"), } } - -set_sentry_init_setting("Development") diff --git a/onestep_be/setting/prod.py b/onestep_be/setting/prod.py index ddb543c..2c92112 100644 --- a/onestep_be/setting/prod.py +++ b/onestep_be/setting/prod.py @@ -1,5 +1,4 @@ from accounts.aws import get_secret -from onestep_be.settings import set_sentry_setting ALLOWED_HOSTS = ["*"] DEBUG = False @@ -15,5 +14,3 @@ "PORT": SECRETS.get("DB_PORT"), } } - -set_sentry_setting("Production") diff --git a/onestep_be/setting/test.py b/onestep_be/setting/test.py index b9ef915..d37a0a4 100644 --- a/onestep_be/setting/test.py +++ b/onestep_be/setting/test.py @@ -1,8 +1,6 @@ -from onestep_be.settings import MIDDLEWARE, set_sentry_init_setting +from onestep_be.settings import MIDDLEWARE SKIP_AUTHENTICATION = True if SKIP_AUTHENTICATION: MIDDLEWARE.insert(0, "accounts.middleware.SkipAuthMiddleware") - -set_sentry_init_setting("Testing") diff --git a/onestep_be/settings.py b/onestep_be/settings.py index f6b2059..a70d360 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -15,6 +15,7 @@ from pathlib import Path import django.db.models.signals +import httpx import pymysql import resend import sentry_sdk @@ -189,7 +190,15 @@ # Add OpenAI API Key OPENAI_API_KEY = SECRETS.get("OPENAI_API_KEY") -openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY) +# openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY) +openai_client = AsyncOpenAI( + api_key=OPENAI_API_KEY, + http_client=httpx.AsyncClient( + limits=httpx.Limits( + max_connections=1000, max_keepalive_connections=100 + ), + ), +) # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators @@ -244,34 +253,34 @@ PROJECT_VERSION = file.read().strip() if PROJECT_VERSION == "": PROJECT_VERSION = "Unknown" + SENTRY_ENVIRONMENT = "localhost" + else: + SENTRY_ENVIRONMENT = PROJECT_VERSION.split("@")[0] except FileNotFoundError: PROJECT_VERSION = "Unknown" # 파일이 없을 경우 기본값 # sentry settings - SENTRY_DSN = SECRETS.get("SENTRY_DSN") -def set_sentry_init_setting(SENTRY_ENVIRONMENT): - sentry_sdk.init( - dsn=SENTRY_DSN, - traces_sample_rate=0.1, - release=PROJECT_VERSION, - profiles_sample_rate=0.1, - environment=SENTRY_ENVIRONMENT, - integrations=[ - DjangoIntegration( - transaction_style="url", - middleware_spans=True, - signals_spans=True, - signals_denylist=[ - django.db.models.signals.pre_init, - django.db.models.signals.post_init, - ], - cache_spans=False, - ), - ], - ) - +sentry_sdk.init( + dsn=SENTRY_DSN, + traces_sample_rate=1.0, + release=PROJECT_VERSION, + profiles_sample_rate=1.0, + environment=SENTRY_ENVIRONMENT, + integrations=[ + DjangoIntegration( + transaction_style="url", + middleware_spans=True, + signals_spans=True, + signals_denylist=[ + django.db.models.signals.pre_init, + django.db.models.signals.post_init, + ], + cache_spans=False, + ), + ], +) resend.api_key = SECRETS.get("RESEND") diff --git a/todos/tests/test_recommend_check_rate_limit.py b/todos/tests/test_recommend_check_rate_limit.py index 9e57872..a79d18f 100644 --- a/todos/tests/test_recommend_check_rate_limit.py +++ b/todos/tests/test_recommend_check_rate_limit.py @@ -66,10 +66,12 @@ def test_rate_limit_premium( @pytest.mark.django_db -def test_time_openAI(authenticated_client, create_todo, benchmark): +def test_openAI_five_times(authenticated_client, create_todo): url = reverse("recommend") - response = benchmark( - lambda: authenticated_client.get(url, {"todo_id": create_todo.id}) - ) + for _ in range(5): + response = authenticated_client.get(url, {"todo_id": create_todo.id}) + assert response.status_code == status.HTTP_200_OK + + response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK diff --git a/todos/tests/test_todo_post.py b/todos/tests/test_todo_post.py index 4db5afd..4f423b0 100644 --- a/todos/tests/test_todo_post.py +++ b/todos/tests/test_todo_post.py @@ -23,7 +23,6 @@ def test_create_todo_success( ): url = reverse("todos") data = { - "user_id": create_user.id, "date": date, "due_time": None, "content": content, @@ -32,6 +31,7 @@ def test_create_todo_success( response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 assert "id" in response.data + assert response.data.rank == "mzzzz" @pytest.mark.django_db diff --git a/todos/views.py b/todos/views.py index 7fd57d3..d884fed 100644 --- a/todos/views.py +++ b/todos/views.py @@ -14,7 +14,7 @@ from onestep_be.settings import openai_client from todos.firebase_messaging import send_push_notification_device -from todos.models import Category, SubTodo, Todo, UserLastUsage +from todos.models import Category, SubTodo, Todo from todos.serializers import ( CategorySerializer, GetTodoSerializer, @@ -74,7 +74,6 @@ def post(self, request): ) if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( request.auth.get("device"), request.user, @@ -775,17 +774,17 @@ def get(self, request): """ set_sentry_user(request.user) - user_id = request.user.id + # user_id = request.user.id try: - flag, message = UserLastUsage.check_rate_limit( - user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS - ) - - if flag is False: - return Response( - {"error": message}, - status=status.HTTP_429_TOO_MANY_REQUESTS, - ) + # flag, message = UserLastUsage.check_rate_limit( + # user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS + # ) + + # if flag is False: + # return Response( + # {"error": message}, + # status=status.HTTP_429_TOO_MANY_REQUESTS, + # ) todo_id = request.GET.get("todo_id") if todo_id is None: sentry_sdk.capture_message( @@ -803,8 +802,6 @@ def get(self, request): "date": todo.date, "due_time": todo.due_time, "category_id": todo.category_id, - "rank": todo.rank, - "is_completed": todo.is_completed, } completion = asyncio.run(self.get_openai_completion(todo_data)) return Response( @@ -830,28 +827,26 @@ async def get_openai_completion(self, todo): { "role": "system", "content": """너는 퍼스널 매니저야. - 너가 하는 일은 이 사람이 할 이야기를 듣고 약 1시간 정도면 끝낼 수 있도록 작업을 나눠주는 식으로 진행할 거야. - 아래는 너가 나눠줄 작업 형식이야. - { id : 1, content: "3학년 2학기 운영체제 중간고사 준비", date="2024-09-01", due_time="2024-09-24"} - 이런 형식으로 작성된 작업을 받았을 때 너는 이 작업을 어떻게 나눠줄 것인지를 알려주면 돼. - Output a JSON object structured like: - {id, content, date, due_time, category_id, rank, is_completed, children : [ - {content, date, todo(parent todo id)}, ... ,{content, date, todo(parent todo id)}]} - [조건] - - date 는 부모의 date를 따를 것 - - 작업은 한 서브투두를 해결하는데 1시간 정도로 이루어지도록 제시할 것 - - 언어는 주어진 todo content의 언어에 따를 것 - """, # noqa: E501 + 너가 하는 일은 이 사람이 할 이야기를 듣고 작업을 나눠줘야 해. + 아래는 너가 나눠줄 작업 형식이야. + { id : 1, content: "운영체제 중간고사 준비", date="2024-09-01", due_time=None} + 이런 형식으로 작성된 작업을 받았을 때 너는 이 작업을 어떻게 나눠줄 것인지를 알려주면 돼. + Output a JSON object structured like: + {id, content, date, due_time, category_id, children : [ + {content, date, todo(parent todo id)}, ...}]} + [조건] + - date 는 부모의 date를 따를 것 + - 작업은 한 서브투두를 해결하는데 1시간 정도로 이루어지도록 제시할 것 + - 언어는 주어진 todo content의 언어에 따를 것 + """, # noqa: E501 }, { "role": "user", "content": f"id: {todo["id"]}, \ - content: {todo["content"]}, \ - date: {todo["date"]}, \ - due_time: {todo["due_time"]}, \ - category_id: {todo["category_id"]}, \ - rank: {todo["rank"]}, \ - is_completed: {todo["is_completed"]}", + content: {todo["content"]}, \ + date: {todo["date"]}, \ + due_time: {todo["due_time"]}, \ + category_id: {todo["category_id"]}", }, ], response_format={"type": "json_object"}, diff --git a/version.txt b/version.txt index d92c44e..e69de29 100644 --- a/version.txt +++ b/version.txt @@ -1 +0,0 @@ -2024.09.09.06.54.01 From 23daa352b38ea2f9877a3e5f75aa78f5ae066ca8 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 24 Oct 2024 13:02:25 +0900 Subject: [PATCH 193/229] =?UTF-8?q?fix=20:=20models=20user=5Fid=20?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20=EB=9E=AD=ED=81=AC=EA=B0=92=EC=9D=84=20?= =?UTF-8?q?=EC=A3=BC=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/todos/models.py b/todos/models.py index 33c9f6c..721dd87 100644 --- a/todos/models.py +++ b/todos/models.py @@ -128,6 +128,7 @@ class Todo(TimeStamp, RankedModel): user_id = models.ForeignKey(User, on_delete=models.CASCADE) is_completed = models.BooleanField(default=False) rank = RankField(insert_to_bottom=True) + order_with_respect_to = "user_id" objects = TodosManager() @@ -145,6 +146,7 @@ class SubTodo(TimeStamp, RankedModel): date = models.DateField(null=True) is_completed = models.BooleanField(default=False) rank = RankField(insert_to_bottom=True) + order_with_respect_to = "todo_id" objects = TodosManager() @@ -160,6 +162,7 @@ class Category(TimeStamp, RankedModel): ) title = models.CharField(max_length=100, null=True) rank = RankField(insert_to_bottom=True) + order_with_respect_to = "user_id" objects = TodosManager() From a9e5e864343c5d6214ce9afd711993fc19233d0c Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 24 Oct 2024 14:27:41 +0900 Subject: [PATCH 194/229] =?UTF-8?q?fix=20:=20user=20login=20=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20repsonse=20=EC=97=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/accounts/views.py b/accounts/views.py index 8d44ae9..e4a4e2d 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -45,6 +45,7 @@ def post(self, request): "refresh": str(refresh), "access": str(refresh.access_token), "is_new": is_new, + "email": email, }, status=status.HTTP_200_OK, ) @@ -109,6 +110,7 @@ def post(self, request): "refresh": str(refresh), "access": str(refresh.access_token), "is_new": is_new, + "email": email, }, status=status.HTTP_200_OK, ) From 486cd6de8ee18acc0380b93dadaf1a26f1fa588e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 24 Oct 2024 15:00:01 +0900 Subject: [PATCH 195/229] =?UTF-8?q?fix=20:=20import=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- onestep_be/setting/dev.py | 2 +- onestep_be/setting/prod.py | 4 ++-- onestep_be/setting/test.py | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/onestep_be/setting/dev.py b/onestep_be/setting/dev.py index 726ca66..a207b0f 100644 --- a/onestep_be/setting/dev.py +++ b/onestep_be/setting/dev.py @@ -1,5 +1,5 @@ from accounts.aws import get_secret -from onestep_be.settings import set_sentry_init_setting +from onestep_be.settings import * SECRETS = eval(get_secret()) diff --git a/onestep_be/setting/prod.py b/onestep_be/setting/prod.py index ddb543c..4ec295d 100644 --- a/onestep_be/setting/prod.py +++ b/onestep_be/setting/prod.py @@ -1,5 +1,5 @@ from accounts.aws import get_secret -from onestep_be.settings import set_sentry_setting +from onestep_be.settings import * ALLOWED_HOSTS = ["*"] DEBUG = False @@ -16,4 +16,4 @@ } } -set_sentry_setting("Production") +set_sentry_init_setting("Production") diff --git a/onestep_be/setting/test.py b/onestep_be/setting/test.py index b9ef915..242a3f2 100644 --- a/onestep_be/setting/test.py +++ b/onestep_be/setting/test.py @@ -1,4 +1,4 @@ -from onestep_be.settings import MIDDLEWARE, set_sentry_init_setting +from onestep_be.settings import * SKIP_AUTHENTICATION = True diff --git a/pyproject.toml b/pyproject.toml index a661462..c8e141a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires-python = ">=3.10" [tool.ruff] # Set the maximum line length to 79. line-length = 79 -exclude = ["*/migrations"] +exclude = ["*/migrations", "onestep_be/setting"] [tool.ruff.lint] # Add the `line-too-long` rule to the enforced rule set. From edf7af44ddd105e603e692342d67478d6d835376 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 24 Oct 2024 15:24:59 +0900 Subject: [PATCH 196/229] fix : merge with develop --- onestep_be/setting/prod.py | 1 - onestep_be/setting/test.py | 4 ---- onestep_be/settings.py | 43 +++++++++++++++++++------------------- pytest.ini | 2 +- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/onestep_be/setting/prod.py b/onestep_be/setting/prod.py index 6f5cc50..dfacafe 100644 --- a/onestep_be/setting/prod.py +++ b/onestep_be/setting/prod.py @@ -18,5 +18,4 @@ } } -set_sentry_init_setting("Production") diff --git a/onestep_be/setting/test.py b/onestep_be/setting/test.py index b89b322..5a4e577 100644 --- a/onestep_be/setting/test.py +++ b/onestep_be/setting/test.py @@ -1,10 +1,6 @@ - from onestep_be.settings import * - SKIP_AUTHENTICATION = True if SKIP_AUTHENTICATION: MIDDLEWARE.insert(0, "accounts.middleware.SkipAuthMiddleware") - -set_sentry_init_setting("Testing") \ No newline at end of file diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 6713ae3..a70d360 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -253,6 +253,9 @@ PROJECT_VERSION = file.read().strip() if PROJECT_VERSION == "": PROJECT_VERSION = "Unknown" + SENTRY_ENVIRONMENT = "localhost" + else: + SENTRY_ENVIRONMENT = PROJECT_VERSION.split("@")[0] except FileNotFoundError: PROJECT_VERSION = "Unknown" # 파일이 없을 경우 기본값 @@ -260,26 +263,24 @@ SENTRY_DSN = SECRETS.get("SENTRY_DSN") -def set_sentry_init_setting(SENTRY_ENVIRONMENT): - sentry_sdk.init( - dsn="https://9425334e0e90c405218fa9613cea9a03@o4507736964136960.ingest.us.sentry.io/4507763025117184", - traces_sample_rate=0.1, - release=PROJECT_VERSION, - profiles_sample_rate=0.1, - environment=SENTRY_ENVIRONMENT, - integrations=[ - DjangoIntegration( - transaction_style="url", - middleware_spans=True, - signals_spans=True, - signals_denylist=[ - django.db.models.signals.pre_init, - django.db.models.signals.post_init, - ], - cache_spans=False, - ), - ], - ) - +sentry_sdk.init( + dsn=SENTRY_DSN, + traces_sample_rate=1.0, + release=PROJECT_VERSION, + profiles_sample_rate=1.0, + environment=SENTRY_ENVIRONMENT, + integrations=[ + DjangoIntegration( + transaction_style="url", + middleware_spans=True, + signals_spans=True, + signals_denylist=[ + django.db.models.signals.pre_init, + django.db.models.signals.post_init, + ], + cache_spans=False, + ), + ], +) resend.api_key = SECRETS.get("RESEND") diff --git a/pytest.ini b/pytest.ini index 4db0b8e..1fab35f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ # pytest.ini [pytest] -DJANGO_SETTINGS_MODULE = onestep_be.setting +DJANGO_SETTINGS_MODULE = onestep_be.settings python_files = test_*.py *_test.py tests.py \ No newline at end of file From beb466ec128bbf164dc49f96bcf85e84ba5802cc Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Thu, 24 Oct 2024 17:54:53 +0900 Subject: [PATCH 197/229] =?UTF-8?q?fix=20:=20recommend=20api=20timeout=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- onestep_be/settings.py | 3 ++- todos/views.py | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index a70d360..9d4faa9 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -195,8 +195,9 @@ api_key=OPENAI_API_KEY, http_client=httpx.AsyncClient( limits=httpx.Limits( - max_connections=1000, max_keepalive_connections=100 + max_connections=100, max_keepalive_connections=100 ), + timeout=httpx.Timeout(timeout=600.0, connect=5.0), ), ) diff --git a/todos/views.py b/todos/views.py index d884fed..ba6c61e 100644 --- a/todos/views.py +++ b/todos/views.py @@ -8,7 +8,7 @@ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -752,7 +752,7 @@ def get(self, request): class RecommendSubTodo(APIView): - permission_classes = [AllowAny] + permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=["RecommendSubTodo"], @@ -842,11 +842,11 @@ async def get_openai_completion(self, todo): }, { "role": "user", - "content": f"id: {todo["id"]}, \ - content: {todo["content"]}, \ - date: {todo["date"]}, \ - due_time: {todo["due_time"]}, \ - category_id: {todo["category_id"]}", + "content": f"id: {todo['id']}, " + f"content: {todo['content']}, " + f"date: {todo['date']}, " + f"due_time: {todo['due_time']}, " + f"category_id: {todo['category_id']}", }, ], response_format={"type": "json_object"}, From 0641e0fe0296e51dabe9054f514f186c5659dcda Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 25 Oct 2024 15:43:34 +0900 Subject: [PATCH 198/229] fix : recomend api --- onestep_be/settings.py | 12 +----------- todos/views.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 9d4faa9..7501d47 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -15,7 +15,6 @@ from pathlib import Path import django.db.models.signals -import httpx import pymysql import resend import sentry_sdk @@ -190,16 +189,7 @@ # Add OpenAI API Key OPENAI_API_KEY = SECRETS.get("OPENAI_API_KEY") -# openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY) -openai_client = AsyncOpenAI( - api_key=OPENAI_API_KEY, - http_client=httpx.AsyncClient( - limits=httpx.Limits( - max_connections=100, max_keepalive_connections=100 - ), - timeout=httpx.Timeout(timeout=600.0, connect=5.0), - ), -) +openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY) # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators diff --git a/todos/views.py b/todos/views.py index ba6c61e..42d3dc7 100644 --- a/todos/views.py +++ b/todos/views.py @@ -14,7 +14,7 @@ from onestep_be.settings import openai_client from todos.firebase_messaging import send_push_notification_device -from todos.models import Category, SubTodo, Todo +from todos.models import Category, SubTodo, Todo, UserLastUsage from todos.serializers import ( CategorySerializer, GetTodoSerializer, @@ -774,17 +774,17 @@ def get(self, request): """ set_sentry_user(request.user) - # user_id = request.user.id + user_id = request.user.id try: - # flag, message = UserLastUsage.check_rate_limit( - # user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS - # ) - - # if flag is False: - # return Response( - # {"error": message}, - # status=status.HTTP_429_TOO_MANY_REQUESTS, - # ) + flag, message = UserLastUsage.check_rate_limit( + user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS + ) + + if flag is False: + return Response( + {"error": message}, + status=status.HTTP_429_TOO_MANY_REQUESTS, + ) todo_id = request.GET.get("todo_id") if todo_id is None: sentry_sdk.capture_message( From cd329e6ad079493df7144964eca96f6e6003aeeb Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 25 Oct 2024 17:39:00 +0900 Subject: [PATCH 199/229] feat : apple social login --- accounts/views.py | 70 +++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index e4a4e2d..4899369 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,10 +1,12 @@ import jwt +import requests import sentry_sdk from django.conf import settings from django.contrib.auth import get_user_model from fcm_django.models import FCMDevice -from google.auth.transport import requests +from google.auth.transport import requests as google_requests from google.oauth2 import id_token +from jwt.algorithms import RSAAlgorithm from rest_framework import status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response @@ -67,12 +69,12 @@ def verify_token(self, device_type, token): if device_type == DEVICE_TYPE_ANDROID: # Android return id_token.verify_oauth2_token( token, - requests.Request(), + google_requests.Request(), audience=GOOGLE_ANDROID_CLIENT_ID, ) elif device_type == DEVICE_TYPE_IOS: # iOS return id_token.verify_oauth2_token( - token, requests.Request(), audience=GOOGLE_IOS_CLIENT_ID + token, google_requests.Request(), audience=GOOGLE_IOS_CLIENT_ID ) else: raise LoginException("Invalid device type") @@ -92,7 +94,7 @@ class AppleLogin(APIView): request : token, device_token, type(0 : android, 1 : ios) """ - APPLE_SERVICE_ID = settings.SECRETS.get("APPLE_SERVICE_ID") + APPLE_SERVICE_ID = settings.SECRETS.get("APPLE_APP_ID") APPLE_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys" authentication_classes = [] @@ -102,7 +104,7 @@ def post(self, request): try: device_type, token, device_token = self.validate_request(request) # verify apple token and get email - email = self.verify_token(device_type, token) + email = self.verify_token(token) user, is_new = User.get_or_create_user(email) refresh = self.handle_device_token(user, device_token) return Response( @@ -120,48 +122,39 @@ def post(self, request): {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) - def verify_token(self, token): + def verify_token(self, identity_token): """ Finish the auth process once the access_token was retrieved Get the email from ID token received from apple """ - # Apple 공개 키 가져오기 - response = requests.get(AppleLogin.APPLE_PUBLIC_KEYS_URL) - if response.status_code != 200: - raise LoginException("Unable to fetch Apple's public keys") - apple_public_keys = response.json()["keys"] - # JWT 디코드 및 서명 검증 - header = jwt.get_unverified_header(token) - - # 헤더의 'kid'와 일치하는 Apple 공개 키 선택 - public_key = next( - key - for key in apple_public_keys["keys"] - if key["kid"] == header["kid"] - ) - - if public_key is None: - raise LoginException("Invalid token") - # Verify the token try: + # JWT의 헤더에서 kid 추출 + unverified_header = jwt.get_unverified_header(identity_token) + kid = unverified_header["kid"] + + # kid에 맞는 Apple 공개 키 가져오기 + public_key = self.get_apple_public_key(kid) + + # 토큰 디코드 및 검증 decoded_token = jwt.decode( - id_token, + identity_token, public_key, - algorithms=["RS256"], # Apple의 서명 알고리즘 - audience=AppleLogin.APPLE_SERVICE_ID, + algorithms=["RS256"], + audience=self.APPLE_SERVICE_ID, issuer="https://appleid.apple.com", ) - return decoded_token.get("email") - except jwt.ExpiredSignatureError: - sentry_sdk.capture_exception(jwt.ExpiredSignatureError) + email = decoded_token.get("email", None) + return email + except jwt.ExpiredSignatureError as e: + sentry_sdk.capture_exception(e) raise LoginException("Token has expired") - except jwt.InvalidTokenError: - sentry_sdk.capture_exception(jwt.ExpiredSignatureError) + except jwt.InvalidTokenError as e: + sentry_sdk.capture_exception(e) raise LoginException("Invalid token") except Exception as e: sentry_sdk.capture_exception(e) - raise LoginException("An unexpected error occurred") + raise LoginException(f"An unexpected error occurred: {e}") def validate_request(self, request): device_type = request.data.get("type", None) @@ -180,6 +173,17 @@ def handle_device_token(self, user, device_token): else: return CustomRefreshToken.for_user_without_device(user) + def get_apple_public_key(self, kid): + # SSL 검증 비활성화하고 Apple 공개 키를 가져옴 + response = requests.get(self.APPLE_PUBLIC_KEYS_URL, verify=False) + keys = response.json().get("keys", []) + + # kid에 맞는 키 찾기 + for key in keys: + if key["kid"] == kid: + return RSAAlgorithm.from_jwk(key) + raise ValueError("Matching key not found") + class UserRetrieveView(APIView): serializer_class = UserSerializer From 1deb65483ed0a847f73feb23b02941eb75a289df Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 25 Oct 2024 17:39:36 +0900 Subject: [PATCH 200/229] fix: service id -> app id --- accounts/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index 4899369..31a7334 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -94,7 +94,7 @@ class AppleLogin(APIView): request : token, device_token, type(0 : android, 1 : ios) """ - APPLE_SERVICE_ID = settings.SECRETS.get("APPLE_APP_ID") + APPLE_APP_ID = settings.SECRETS.get("APPLE_APP_ID") APPLE_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys" authentication_classes = [] @@ -141,7 +141,7 @@ def verify_token(self, identity_token): identity_token, public_key, algorithms=["RS256"], - audience=self.APPLE_SERVICE_ID, + audience=self.APPLE_APP_ID, issuer="https://appleid.apple.com", ) email = decoded_token.get("email", None) From 695bee16581cbe225f986c715ef7396138761a6d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 25 Oct 2024 17:49:05 +0900 Subject: [PATCH 201/229] =?UTF-8?q?refactor=20:=20login=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 118 +++++++++++----------------------------------- 1 file changed, 27 insertions(+), 91 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index 31a7334..fb3ab92 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -6,7 +6,6 @@ from fcm_django.models import FCMDevice from google.auth.transport import requests as google_requests from google.oauth2 import id_token -from jwt.algorithms import RSAAlgorithm from rest_framework import status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response @@ -26,31 +25,25 @@ DEVICE_TYPE_IOS = 1 -class GoogleLogin(APIView): - """ - request : token, device_token, type(0 : android, 1 : ios) - """ - +class BaseLogin(APIView): authentication_classes = [] permission_classes = [AllowAny] def post(self, request): try: device_type, token, device_token = self.validate_request(request) - idinfo = self.verify_token(device_type, token) - if "accounts.google.com" in idinfo["iss"]: - email = idinfo["email"] - user, is_new = User.get_or_create_user(email) - refresh = self.handle_device_token(user, device_token) - return Response( - { - "refresh": str(refresh), - "access": str(refresh.access_token), - "is_new": is_new, - "email": email, - }, - status=status.HTTP_200_OK, - ) + email = self.verify_token(device_type, token) + user, is_new = User.get_or_create_user(email) + refresh = self.handle_device_token(user, device_token) + return Response( + { + "refresh": str(refresh), + "access": str(refresh.access_token), + "is_new": is_new, + "email": email, + }, + status=status.HTTP_200_OK, + ) except Exception as e: sentry_sdk.capture_exception(e) return Response( @@ -65,6 +58,17 @@ def validate_request(self, request): raise LoginException() return device_type, token, device_token + def handle_device_token(self, user, device_token): + if device_token: + FCMDevice.objects.get_or_create( + user=user, registration_id=device_token + ) + return CustomRefreshToken.for_user(user, device_token) + else: + return CustomRefreshToken.for_user_without_device(user) + + +class GoogleLogin(BaseLogin): def verify_token(self, device_type, token): if device_type == DEVICE_TYPE_ANDROID: # Android return id_token.verify_oauth2_token( @@ -79,64 +83,16 @@ def verify_token(self, device_type, token): else: raise LoginException("Invalid device type") - def handle_device_token(self, user, device_token): - if device_token: - FCMDevice.objects.get_or_create( - user=user, registration_id=device_token - ) - return CustomRefreshToken.for_user(user, device_token) - else: - return CustomRefreshToken.for_user_without_device(user) - - -class AppleLogin(APIView): - """ - request : token, device_token, type(0 : android, 1 : ios) - """ +class AppleLogin(BaseLogin): APPLE_APP_ID = settings.SECRETS.get("APPLE_APP_ID") APPLE_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys" - authentication_classes = [] - permission_classes = [AllowAny] - - def post(self, request): + def verify_token(self, device_type, identity_token): try: - device_type, token, device_token = self.validate_request(request) - # verify apple token and get email - email = self.verify_token(token) - user, is_new = User.get_or_create_user(email) - refresh = self.handle_device_token(user, device_token) - return Response( - { - "refresh": str(refresh), - "access": str(refresh.access_token), - "is_new": is_new, - "email": email, - }, - status=status.HTTP_200_OK, - ) - except Exception as e: - sentry_sdk.capture_exception(e) - return Response( - {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST - ) - - def verify_token(self, identity_token): - """ - Finish the auth process once the access_token was retrieved - Get the email from ID token received from apple - """ - # Verify the token - try: - # JWT의 헤더에서 kid 추출 unverified_header = jwt.get_unverified_header(identity_token) kid = unverified_header["kid"] - - # kid에 맞는 Apple 공개 키 가져오기 public_key = self.get_apple_public_key(kid) - - # 토큰 디코드 및 검증 decoded_token = jwt.decode( identity_token, public_key, @@ -156,32 +112,12 @@ def verify_token(self, identity_token): sentry_sdk.capture_exception(e) raise LoginException(f"An unexpected error occurred: {e}") - def validate_request(self, request): - device_type = request.data.get("type", None) - token = request.data.get("token") - device_token = request.data.get("device_token", None) - if not token or device_type is None: - raise LoginException() - return device_type, token, device_token - - def handle_device_token(self, user, device_token): - if device_token: - FCMDevice.objects.get_or_create( - user=user, registration_id=device_token - ) - return CustomRefreshToken.for_user(user, device_token) - else: - return CustomRefreshToken.for_user_without_device(user) - def get_apple_public_key(self, kid): - # SSL 검증 비활성화하고 Apple 공개 키를 가져옴 response = requests.get(self.APPLE_PUBLIC_KEYS_URL, verify=False) keys = response.json().get("keys", []) - - # kid에 맞는 키 찾기 for key in keys: if key["kid"] == kid: - return RSAAlgorithm.from_jwk(key) + return jwt.algorithms.RSAAlgorithm.from_jwk(key) raise ValueError("Matching key not found") From 032b80e0c7ca340cf381b97443d372ccaf9ade12 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 25 Oct 2024 17:52:54 +0900 Subject: [PATCH 202/229] =?UTF-8?q?fix=20:=20apple=20login=20ios=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=EB=B6=88=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/accounts/views.py b/accounts/views.py index fb3ab92..f26310b 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -89,6 +89,8 @@ class AppleLogin(BaseLogin): APPLE_PUBLIC_KEYS_URL = "https://appleid.apple.com/auth/keys" def verify_token(self, device_type, identity_token): + if device_type != DEVICE_TYPE_IOS: + raise LoginException("Invalid device type") try: unverified_header = jwt.get_unverified_header(identity_token) kid = unverified_header["kid"] From 6f93b5a67055c6ae90b6b6268cb10d15d4ef33c0 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 25 Oct 2024 18:01:36 +0900 Subject: [PATCH 203/229] =?UTF-8?q?fix=20:=20applelogin=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20warning=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/accounts/views.py b/accounts/views.py index f26310b..5aba5b7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,6 +1,7 @@ import jwt import requests import sentry_sdk +import urllib3 from django.conf import settings from django.contrib.auth import get_user_model from fcm_django.models import FCMDevice @@ -115,6 +116,7 @@ def verify_token(self, device_type, identity_token): raise LoginException(f"An unexpected error occurred: {e}") def get_apple_public_key(self, kid): + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) response = requests.get(self.APPLE_PUBLIC_KEYS_URL, verify=False) keys = response.json().get("keys", []) for key in keys: From cd8a5d236c4fc1c9a1cc9f4eeddc21fc7a69ddbc Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 25 Oct 2024 18:45:12 +0900 Subject: [PATCH 204/229] =?UTF-8?q?fix=20:=20google=20login=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/views.py | 28 ++++++++++++++++++---------- conftest.py | 6 +++++- onestep_be/settings.py | 11 ++++++++++- todos/views.py | 24 ++++++++++++------------ 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/accounts/views.py b/accounts/views.py index 5aba5b7..a4887f8 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -71,19 +71,27 @@ def handle_device_token(self, user, device_token): class GoogleLogin(BaseLogin): def verify_token(self, device_type, token): - if device_type == DEVICE_TYPE_ANDROID: # Android - return id_token.verify_oauth2_token( - token, - google_requests.Request(), - audience=GOOGLE_ANDROID_CLIENT_ID, - ) - elif device_type == DEVICE_TYPE_IOS: # iOS - return id_token.verify_oauth2_token( - token, google_requests.Request(), audience=GOOGLE_IOS_CLIENT_ID - ) + audience = self.get_audience(device_type) + idinfo = id_token.verify_oauth2_token( + token, + google_requests.Request(), + audience=audience, + ) + self.validate_issuer(idinfo) + return idinfo["email"] + + def get_audience(self, device_type): + if device_type == DEVICE_TYPE_ANDROID: + return GOOGLE_ANDROID_CLIENT_ID + elif device_type == DEVICE_TYPE_IOS: + return GOOGLE_IOS_CLIENT_ID else: raise LoginException("Invalid device type") + def validate_issuer(self, idinfo): + if "accounts.google.com" not in idinfo["iss"]: + raise LoginException("Invalid token") + class AppleLogin(BaseLogin): APPLE_APP_ID = settings.SECRETS.get("APPLE_APP_ID") diff --git a/conftest.py b/conftest.py index 95dc42c..38a545c 100644 --- a/conftest.py +++ b/conftest.py @@ -17,7 +17,11 @@ @pytest.fixture(scope="module") def invalid_token(): - response = {"token": "token", "deviceToken": "device_token"} + response = { + "token": "token", + "deviceToken": "device_token", + "type": 0, + } return response diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 7501d47..411a59e 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -15,6 +15,7 @@ from pathlib import Path import django.db.models.signals +import httpx import pymysql import resend import sentry_sdk @@ -189,7 +190,15 @@ # Add OpenAI API Key OPENAI_API_KEY = SECRETS.get("OPENAI_API_KEY") -openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY) +openai_client = AsyncOpenAI( + api_key=OPENAI_API_KEY, + http_client=httpx.AsyncClient( + limits=httpx.Limits( + max_connections=1000, max_keepalive_connections=100 + ), + ), + timeout=httpx.Timeout(timeout=8.0, connect=5.0), +) # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators diff --git a/todos/views.py b/todos/views.py index 42d3dc7..3aa905d 100644 --- a/todos/views.py +++ b/todos/views.py @@ -14,7 +14,7 @@ from onestep_be.settings import openai_client from todos.firebase_messaging import send_push_notification_device -from todos.models import Category, SubTodo, Todo, UserLastUsage +from todos.models import Category, SubTodo, Todo from todos.serializers import ( CategorySerializer, GetTodoSerializer, @@ -752,7 +752,7 @@ def get(self, request): class RecommendSubTodo(APIView): - permission_classes = [IsAuthenticated] + # permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=["RecommendSubTodo"], @@ -774,17 +774,17 @@ def get(self, request): """ set_sentry_user(request.user) - user_id = request.user.id + # user_id = request.user.id try: - flag, message = UserLastUsage.check_rate_limit( - user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS - ) - - if flag is False: - return Response( - {"error": message}, - status=status.HTTP_429_TOO_MANY_REQUESTS, - ) + # flag, message = UserLastUsage.check_rate_limit( + # user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS + # ) + + # if flag is False: + # return Response( + # {"error": message}, + # status=status.HTTP_429_TOO_MANY_REQUESTS, + # ) todo_id = request.GET.get("todo_id") if todo_id is None: sentry_sdk.capture_message( From 31e83f5feafab6e44741f534861368fb56559f83 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 28 Oct 2024 17:26:58 +0900 Subject: [PATCH 205/229] =?UTF-8?q?fix=20:=20get=5For=5Fcreate=5Fuser?= =?UTF-8?q?=EC=97=90=EC=84=9C=20deleted=5Fat=5F=5Fisnull=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/accounts/models.py b/accounts/models.py index 77c8895..cf13786 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -48,7 +48,7 @@ class SocialProvider(models.TextChoices): def get_or_create_user(self, email): try: is_new = False - user = User.objects.get(username=email) + user = User.objects.get(username=email, deleted_at__isnull=True) except User.DoesNotExist: user = User.objects.create(username=email, password="") send_welcome_email( @@ -58,9 +58,6 @@ def get_or_create_user(self, email): is_new = True except Exception as e: sentry_sdk.capture_exception(e) - sentry_sdk.capture_message( - "Failed to get or create user", level="error" - ) raise e return user, is_new From 274eafda26583524d9813f5717a9cda5a72b4ace Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 29 Oct 2024 00:33:11 +0900 Subject: [PATCH 206/229] =?UTF-8?q?fix=20:=20=ED=83=88=ED=87=B4=ED=95=9C?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=EA=B0=80=20=EC=9E=AC=20=EC=A0=91=EC=86=8D?= =?UTF-8?q?=EC=8B=9C=20=EA=B8=B0=EC=A1=B4=20=EA=B3=84=EC=A0=95=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- accounts/admin.py | 14 +++++++++++++- accounts/models.py | 6 +++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/accounts/admin.py b/accounts/admin.py index 192b164..abc5ef3 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -5,11 +5,20 @@ class UserAdmin(admin.ModelAdmin): - readonly_fields = ["id", "social_provider", "username"] + readonly_fields = [ + "id", + "social_provider", + "username", + "created_at", + "updated_at", + ] list_display = [ "id", "username", "social_provider", + "created_at", + "deleted_at", + "updated_at", "is_active", "is_staff", "is_superuser", @@ -22,6 +31,9 @@ class UserAdmin(admin.ModelAdmin): "id", "username", "social_provider", + "created_at", + "deleted_at", + "updated_at", "is_active", "is_staff", "is_superuser", diff --git a/accounts/models.py b/accounts/models.py index cf13786..70b3627 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -48,7 +48,11 @@ class SocialProvider(models.TextChoices): def get_or_create_user(self, email): try: is_new = False - user = User.objects.get(username=email, deleted_at__isnull=True) + user = User.objects.get(username=email) + if user.deleted_at is not None: + user.deleted_at = None + user.save(update_fields=["deleted_at"]) + is_new = True except User.DoesNotExist: user = User.objects.create(username=email, password="") send_welcome_email( From 9ae74d37e2eb2ce30e64aa0ac9e2b689874d0fb9 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 29 Oct 2024 15:48:27 +0900 Subject: [PATCH 207/229] =?UTF-8?q?feat:=20time-machine=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=2010=EC=B4=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + .../tests/test_recommend_check_rate_limit.py | 19 ++++------------ todos/views.py | 22 +++++++++---------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/requirements.txt b/requirements.txt index 59b5aa1..f631972 100644 --- a/requirements.txt +++ b/requirements.txt @@ -115,3 +115,4 @@ py-cpuinfo==9.0.0 pytest-benchmark==4.0.0 pytest_asyncio==0.24.0 debugpy==1.8.7 +time-machine==2.16.0 diff --git a/todos/tests/test_recommend_check_rate_limit.py b/todos/tests/test_recommend_check_rate_limit.py index a79d18f..b3e2b12 100644 --- a/todos/tests/test_recommend_check_rate_limit.py +++ b/todos/tests/test_recommend_check_rate_limit.py @@ -1,5 +1,5 @@ # import asyncio -import time +import datetime as dt from unittest.mock import AsyncMock, patch import pytest @@ -31,15 +31,16 @@ def test_rate_limit_exceeded( "todos.views.openai_client.chat.completions.create", new_callable=AsyncMock ) def test_rate_limit_passed( - mock_llm, authenticated_client, create_todo, recommend_result + mock_llm, authenticated_client, create_todo, recommend_result, time_machine ): url = reverse("recommend") mock_llm.return_value = recommend_result + time_machine.move_to(dt.datetime(2024, 3, 3)) response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK - time.sleep(10) + time_machine.shift(dt.timedelta(seconds=10)) response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK @@ -63,15 +64,3 @@ def test_rate_limit_premium( response = authenticated_client.get(url, {"todo_id": create_todo.id}) assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_openAI_five_times(authenticated_client, create_todo): - url = reverse("recommend") - - for _ in range(5): - response = authenticated_client.get(url, {"todo_id": create_todo.id}) - assert response.status_code == status.HTTP_200_OK - - response = authenticated_client.get(url, {"todo_id": create_todo.id}) - assert response.status_code == status.HTTP_200_OK diff --git a/todos/views.py b/todos/views.py index 3aa905d..16c8e35 100644 --- a/todos/views.py +++ b/todos/views.py @@ -14,7 +14,7 @@ from onestep_be.settings import openai_client from todos.firebase_messaging import send_push_notification_device -from todos.models import Category, SubTodo, Todo +from todos.models import Category, SubTodo, Todo, UserLastUsage from todos.serializers import ( CategorySerializer, GetTodoSerializer, @@ -774,17 +774,17 @@ def get(self, request): """ set_sentry_user(request.user) - # user_id = request.user.id + user_id = request.user.id try: - # flag, message = UserLastUsage.check_rate_limit( - # user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS - # ) - - # if flag is False: - # return Response( - # {"error": message}, - # status=status.HTTP_429_TOO_MANY_REQUESTS, - # ) + flag, message = UserLastUsage.check_rate_limit( + user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS + ) + + if flag is False: + return Response( + {"error": message}, + status=status.HTTP_429_TOO_MANY_REQUESTS, + ) todo_id = request.GET.get("todo_id") if todo_id is None: sentry_sdk.capture_message( From 5db5860aac7377bfe60fc9ead5a9139f803bf6f4 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 29 Oct 2024 20:32:59 +0900 Subject: [PATCH 208/229] fix : settings --- onestep_be/settings.py | 4 ++-- todos/views.py | 25 ++++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 411a59e..fd2a5f6 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -195,9 +195,9 @@ http_client=httpx.AsyncClient( limits=httpx.Limits( max_connections=1000, max_keepalive_connections=100 - ), + ) ), - timeout=httpx.Timeout(timeout=8.0, connect=5.0), + timeout=httpx.Timeout(120), ) # Password validation diff --git a/todos/views.py b/todos/views.py index 16c8e35..20b7c32 100644 --- a/todos/views.py +++ b/todos/views.py @@ -14,7 +14,7 @@ from onestep_be.settings import openai_client from todos.firebase_messaging import send_push_notification_device -from todos.models import Category, SubTodo, Todo, UserLastUsage +from todos.models import Category, SubTodo, Todo from todos.serializers import ( CategorySerializer, GetTodoSerializer, @@ -774,17 +774,17 @@ def get(self, request): """ set_sentry_user(request.user) - user_id = request.user.id + # user_id = request.user.id try: - flag, message = UserLastUsage.check_rate_limit( - user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS - ) - - if flag is False: - return Response( - {"error": message}, - status=status.HTTP_429_TOO_MANY_REQUESTS, - ) + # flag, message = UserLastUsage.check_rate_limit( + # user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS + # ) + + # if flag is False: + # return Response( + # {"error": message}, + # status=status.HTTP_429_TOO_MANY_REQUESTS, + # ) todo_id = request.GET.get("todo_id") if todo_id is None: sentry_sdk.capture_message( @@ -833,9 +833,8 @@ async def get_openai_completion(self, todo): 이런 형식으로 작성된 작업을 받았을 때 너는 이 작업을 어떻게 나눠줄 것인지를 알려주면 돼. Output a JSON object structured like: {id, content, date, due_time, category_id, children : [ - {content, date, todo(parent todo id)}, ...}]} + {content, todo(parent todo id)}, ...}]} [조건] - - date 는 부모의 date를 따를 것 - 작업은 한 서브투두를 해결하는데 1시간 정도로 이루어지도록 제시할 것 - 언어는 주어진 todo content의 언어에 따를 것 """, # noqa: E501 From 661e3b298d9f252074899ae6c41e43b438680f13 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 1 Nov 2024 17:44:38 +0900 Subject: [PATCH 209/229] =?UTF-8?q?fix=20:=20yaml=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=20version.txt=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=20&&=20healthcheck=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20tracking=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy_to_ecs.yml | 2 +- .github/workflows/deploy_to_ecs_prod.yml | 2 +- onestep_be/settings.py | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index adeaf08..efc6a89 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -16,7 +16,7 @@ jobs: - name: Update version.txt run: | VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 - echo "onestep_dev@${VERSION}" > version.txt + echo "onestep_dev@${VERSION}" > backend/version.txt - name: Set Dockerfile Path id: dockerfile-path diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 818eced..a335653 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -16,7 +16,7 @@ jobs: - name: Update version.txt run: | VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 - echo "onestep_prod@${VERSION}" > version.txt + echo "onestep_prod@${VERSION}" > backend/version.txt - name: Set Dockerfile Path id: dockerfile-path diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 411a59e..988810b 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -13,6 +13,7 @@ import os from datetime import timedelta from pathlib import Path +from urllib.parse import urlparse import django.db.models.signals import httpx @@ -263,6 +264,16 @@ SENTRY_DSN = SECRETS.get("SENTRY_DSN") +# sentry Filtering +def setnry_filter_transactions(event, hint): + url_string = event["request"]["url"] + parsed_url = urlparse(url_string) + + if parsed_url.path == "/auth/android" or parsed_url.path == "/swagger": + return None + return event + + sentry_sdk.init( dsn=SENTRY_DSN, traces_sample_rate=1.0, @@ -281,6 +292,7 @@ cache_spans=False, ), ], + before_send_transaction=setnry_filter_transactions, ) resend.api_key = SECRETS.get("RESEND") From 25459331151f423c6bc450d000d11978a09a163d Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 1 Nov 2024 18:24:50 +0900 Subject: [PATCH 210/229] =?UTF-8?q?feat=20:=20asgi=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- onestep_be/settings.py | 7 +++++-- requirements.txt | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 988810b..4601220 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -21,6 +21,7 @@ import resend import sentry_sdk from openai import AsyncOpenAI +from sentry_sdk.integrations.asyncio import AsyncioIntegration from sentry_sdk.integrations.django import DjangoIntegration from accounts.aws import get_secret @@ -269,7 +270,7 @@ def setnry_filter_transactions(event, hint): url_string = event["request"]["url"] parsed_url = urlparse(url_string) - if parsed_url.path == "/auth/android" or parsed_url.path == "/swagger": + if parsed_url.path == "/auth/android/" or parsed_url.path == "/swagger/": return None return event @@ -279,7 +280,8 @@ def setnry_filter_transactions(event, hint): traces_sample_rate=1.0, release=PROJECT_VERSION, profiles_sample_rate=1.0, - environment=SENTRY_ENVIRONMENT, + # environment=SENTRY_ENVIRONMENT, + environment="Testing", integrations=[ DjangoIntegration( transaction_style="url", @@ -291,6 +293,7 @@ def setnry_filter_transactions(event, hint): ], cache_spans=False, ), + AsyncioIntegration(), ], before_send_transaction=setnry_filter_transactions, ) diff --git a/requirements.txt b/requirements.txt index 59b5aa1..434fc82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -72,7 +72,7 @@ pyzmq==26.1.0 requests==2.32.3 rsa==4.7.2 s3transfer==0.10.2 -sentry-sdk==2.13.0 +sentry-sdk==2.17.0 setuptools==69.5.1 simplejson==3.19.2 six==1.16.0 From e2e02e2750196eb6239c75bea6bcab0f09f23bf8 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 1 Nov 2024 21:25:55 +0900 Subject: [PATCH 211/229] =?UTF-8?q?fix:=20is=5Fcompleted=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20with=20To?= =?UTF-8?q?do?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todos/serializers.py b/todos/serializers.py index 630cd5d..7fe4ea8 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -148,7 +148,7 @@ def validate(self, data): if request.method == "PATCH": if not any( - data.get(field) + data.get(field) is not None for field in [ "content", "category_id", From 56d4a726e738916567ad7ae484cb818847e6c0e7 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 1 Nov 2024 21:50:49 +0900 Subject: [PATCH 212/229] =?UTF-8?q?fix:=20is=5Fcompleted=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20with=20Su?= =?UTF-8?q?btodo,=20Category?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/serializers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/todos/serializers.py b/todos/serializers.py index 7fe4ea8..684f63f 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -29,7 +29,8 @@ def validate(self, data): request = self.context["request"] if request.method == "PATCH": if not any( - data.get(field) for field in ["color", "title", "rank"] + data.get(field) is not None + for field in ["color", "title", "rank"] ): raise serializers.ValidationError( "At least one of color, title, rank must be provided" @@ -64,7 +65,7 @@ def validate(self, data): request = self.context["request"] if request.method == "PATCH": if not any( - data.get(field) + data.get(field) is not None for field in ["content", "date", "is_completed", "patch_rank"] ): raise serializers.ValidationError( From 56d1cc356679631faa6c263e7d6a46895978b2e7 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Fri, 1 Nov 2024 21:51:06 +0900 Subject: [PATCH 213/229] =?UTF-8?q?fix:=20is=5Fcompleted=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20with=20Su?= =?UTF-8?q?btodo,=20Category?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/todos/serializers.py b/todos/serializers.py index 684f63f..13cad5c 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -29,8 +29,7 @@ def validate(self, data): request = self.context["request"] if request.method == "PATCH": if not any( - data.get(field) is not None - for field in ["color", "title", "rank"] + data.get(field) for field in ["color", "title", "rank"] ): raise serializers.ValidationError( "At least one of color, title, rank must be provided" From c74fe21ce2695913a619e9cfa5febba3ca2d88e5 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Sat, 2 Nov 2024 16:23:25 +0900 Subject: [PATCH 214/229] fix : test rank --- todos/models.py | 5 +++-- todos/tests/test_todo_post.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/todos/models.py b/todos/models.py index 721dd87..e735350 100644 --- a/todos/models.py +++ b/todos/models.py @@ -25,12 +25,13 @@ def update_rank(self, instance, prev_id, next_id): if prev_id is None and next_id is None: return instance elif prev_id is None: # Move to the top - instance.place_on_top() + next_instance = self.get_queryset().get(id=next_id) + instance.place_before(before_obj=next_instance) elif next_id is None: # Move to the bottom instance.place_on_bottom() else: # Move to after prev_id prev_instance = self.get_queryset().get(id=prev_id) - instance.place_after(prev_instance) + instance.place_after(after_obj=prev_instance) return instance def get_queryset(self): diff --git a/todos/tests/test_todo_post.py b/todos/tests/test_todo_post.py index 4f423b0..70a32a1 100644 --- a/todos/tests/test_todo_post.py +++ b/todos/tests/test_todo_post.py @@ -31,7 +31,7 @@ def test_create_todo_success( response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 assert "id" in response.data - assert response.data.rank == "mzzzz" + assert response.data["rank"] == "mzzzzz" @pytest.mark.django_db From 5e3326774a2b2bc05ac1d4b9b6f9008ac15a2e3c Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 4 Nov 2024 20:34:27 +0900 Subject: [PATCH 215/229] fix : lexorank logic --- .gitmodules | 3 ++ Lexorank | 1 + conftest.py | 10 ++-- onestep_be/settings.py | 13 +---- requirements.txt | 3 +- ...k_remove_todo_rank_subtodo_ank_and_more.py | 38 ++++++++++++++ ...e_ank_subtodo_rank_rename_ank_todo_rank.py | 23 +++++++++ todos/models.py | 49 +++++++++++++------ todos/serializers.py | 6 ++- todos/tests/test_subtodo_delete.py | 3 +- todos/tests/test_subtodo_get.py | 14 ++++-- todos/tests/test_subtodo_patch.py | 19 +++++-- todos/tests/test_todo_delete.py | 3 +- todos/tests/test_todo_get.py | 2 + todos/tests/test_todo_patch.py | 16 ++++-- todos/views.py | 38 +++++++------- 16 files changed, 169 insertions(+), 72 deletions(-) create mode 100644 .gitmodules create mode 160000 Lexorank create mode 100644 todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py create mode 100644 todos/migrations/0018_rename_ank_subtodo_rank_rename_ank_todo_rank.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e844430 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Lexorank"] + path = Lexorank + url = ../Lexorank diff --git a/Lexorank b/Lexorank new file mode 160000 index 0000000..9000200 --- /dev/null +++ b/Lexorank @@ -0,0 +1 @@ +Subproject commit 900020020b8cdd305e28be868fc3473e1caf03a8 diff --git a/conftest.py b/conftest.py index 38a545c..4d1da62 100644 --- a/conftest.py +++ b/conftest.py @@ -108,13 +108,9 @@ def content(): @pytest.fixture -def order(): - orders = ["0|azzzzz:", "0|hzzzzz:", "0|lzzzzz:"] - - def get_order(index): - return orders[index] - - return get_order +def rank(): + orders = ["0|Vzzzzz:", "0|4n210Vz:", "0|35RYXWjz:"] + return orders @pytest.fixture diff --git a/onestep_be/settings.py b/onestep_be/settings.py index fd2a5f6..55b237c 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -15,11 +15,10 @@ from pathlib import Path import django.db.models.signals -import httpx import pymysql import resend import sentry_sdk -from openai import AsyncOpenAI +from openai import OpenAI from sentry_sdk.integrations.django import DjangoIntegration from accounts.aws import get_secret @@ -190,15 +189,7 @@ # Add OpenAI API Key OPENAI_API_KEY = SECRETS.get("OPENAI_API_KEY") -openai_client = AsyncOpenAI( - api_key=OPENAI_API_KEY, - http_client=httpx.AsyncClient( - limits=httpx.Limits( - max_connections=1000, max_keepalive_connections=100 - ) - ), - timeout=httpx.Timeout(120), -) +openai_client = OpenAI(api_key=OPENAI_API_KEY) # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators diff --git a/requirements.txt b/requirements.txt index f631972..f23be54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ coreschema==0.0.4 cryptography==42.0.8 distro==1.9.0 dj-rest-auth==6.0.0 -Django==4.2.16 +Django==5.0.6 django-allauth==0.63.3 django-cors-headers==4.4.0 django-db-connection-pool==1.2.5 @@ -109,7 +109,6 @@ protobuf==5.28.2 swapper==1.4.0 django_crontab==0.7.1 Faker==27.0.0 -django-lexorank==0.1.3 pymysql==1.1.1 py-cpuinfo==9.0.0 pytest-benchmark==4.0.0 diff --git a/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py b/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py new file mode 100644 index 0000000..e227c85 --- /dev/null +++ b/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.16 on 2024-11-04 08:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0016_alter_category_color'), + ] + + operations = [ + migrations.RemoveField( + model_name='subtodo', + name='rank', + ), + migrations.RemoveField( + model_name='todo', + name='rank', + ), + migrations.AddField( + model_name='subtodo', + name='ank', + field=models.CharField(default='0|Vzzzzz:', max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name='todo', + name='ank', + field=models.CharField(default='0|Vzzzzz:', max_length=255), + preserve_default=False, + ), + migrations.AlterField( + model_name='category', + name='rank', + field=models.CharField(max_length=255), + ), + ] diff --git a/todos/migrations/0018_rename_ank_subtodo_rank_rename_ank_todo_rank.py b/todos/migrations/0018_rename_ank_subtodo_rank_rename_ank_todo_rank.py new file mode 100644 index 0000000..be8c6f3 --- /dev/null +++ b/todos/migrations/0018_rename_ank_subtodo_rank_rename_ank_todo_rank.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.16 on 2024-11-04 08:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='subtodo', + old_name='ank', + new_name='rank', + ), + migrations.RenameField( + model_name='todo', + old_name='ank', + new_name='rank', + ), + ] diff --git a/todos/models.py b/todos/models.py index 33c9f6c..70f4486 100644 --- a/todos/models.py +++ b/todos/models.py @@ -3,10 +3,9 @@ from django.db import models from django.db.models import Count, Prefetch, Q from django.utils import timezone -from django_lexorank.fields import RankField -from django_lexorank.models import RankedModel from accounts.models import User +from Lexorank.src.LexoRank import LexoRank class TodosManager(models.Manager): @@ -21,17 +20,37 @@ def delete_many(self, instances): instance.save() return instances - def update_rank(self, instance, prev_id, next_id): + def get_next_rank(self, user_id): + # Get the last rank of the user + get_list = ( + self.get_queryset().filter(user_id=user_id).order_by("rank").last() + ) + if get_list is None: + return str(LexoRank.middle()) + return str(LexoRank.gen_next(LexoRank.parse(get_list.rank))) + + def get_update_rank(self, instance, prev_id, next_id): if prev_id is None and next_id is None: - return instance + return instance.rank elif prev_id is None: # Move to the top - instance.place_on_top() + get_next_rank = self.get_queryset().get(id=next_id).rank + get_rank = str(LexoRank.parse(get_next_rank).gen_prev()) + return get_rank elif next_id is None: # Move to the bottom - instance.place_on_bottom() + prev_instance = LexoRank.parse( + self.get_queryset().get(id=prev_id).rank + ) + return str(LexoRank.gen_next(prev_instance.rank)) else: # Move to after prev_id - prev_instance = self.get_queryset().get(id=prev_id) - instance.place_after(prev_instance) - return instance + prev_instance = LexoRank.parse( + self.get_queryset().get(id=prev_id).rank + ) + next_instance = LexoRank.parse( + self.get_queryset().get(id=next_id).rank + ) + return str( + LexoRank.between(prev_instance.rank, next_instance.rank) + ) def get_queryset(self): return super().get_queryset().filter(deleted_at__isnull=True) @@ -119,7 +138,7 @@ class Meta: abstract = True -class Todo(TimeStamp, RankedModel): +class Todo(TimeStamp): id = models.AutoField(primary_key=True) content = models.CharField(max_length=255) category_id = models.ForeignKey("Category", on_delete=models.CASCADE) @@ -127,7 +146,7 @@ class Todo(TimeStamp, RankedModel): date = models.DateField(null=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE) is_completed = models.BooleanField(default=False) - rank = RankField(insert_to_bottom=True) + rank = models.CharField(max_length=255) objects = TodosManager() @@ -135,7 +154,7 @@ def __str__(self): return self.content -class SubTodo(TimeStamp, RankedModel): +class SubTodo(TimeStamp): id = models.AutoField(primary_key=True) content = models.CharField(max_length=255) todo_id = models.ForeignKey( @@ -144,7 +163,7 @@ class SubTodo(TimeStamp, RankedModel): due_time = models.TimeField(null=True, blank=True) date = models.DateField(null=True) is_completed = models.BooleanField(default=False) - rank = RankField(insert_to_bottom=True) + rank = models.CharField(max_length=255) objects = TodosManager() @@ -152,14 +171,14 @@ def __str__(self): return self.content -class Category(TimeStamp, RankedModel): +class Category(TimeStamp): id = models.AutoField(primary_key=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=1) color = models.SmallIntegerField( validators=[MinValueValidator(0), MaxValueValidator(8)] ) title = models.CharField(max_length=100, null=True) - rank = RankField(insert_to_bottom=True) + rank = models.CharField(max_length=255) objects = TodosManager() diff --git a/todos/serializers.py b/todos/serializers.py index 630cd5d..3c404ba 100644 --- a/todos/serializers.py +++ b/todos/serializers.py @@ -77,9 +77,10 @@ def validate(self, data): def update(self, instance, validated_data): for attr, value in validated_data.items(): if attr == "patch_rank": - SubTodo.objects.update_rank( + rank = SubTodo.objects.get_update_rank( instance, value.get("prev_id"), value.get("next_id") ) + setattr(instance, "rank", rank) else: setattr(instance, attr, value) instance.updated_at = timezone.now() @@ -169,9 +170,10 @@ def update(self, instance, validated_data): for attr, value in validated_data.items(): if attr == "patch_rank": # If the rank field is provided, update the rank field - Todo.objects.update_rank( + rank = Todo.objects.get_update_rank( instance, value.get("prev_id"), value.get("next_id") ) + setattr(instance, "rank", rank) else: setattr(instance, attr, value) diff --git a/todos/tests/test_subtodo_delete.py b/todos/tests/test_subtodo_delete.py index a027c90..cfa475d 100644 --- a/todos/tests/test_subtodo_delete.py +++ b/todos/tests/test_subtodo_delete.py @@ -14,13 +14,14 @@ @pytest.mark.django_db def test_delete_subtodo_success( - create_todo, authenticated_client, content, date + create_todo, authenticated_client, content, date, rank ): subtodo = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[0], ) url = reverse("subtodos") data = {"subtodo_id": subtodo.id} diff --git a/todos/tests/test_subtodo_get.py b/todos/tests/test_subtodo_get.py index e219207..ea423d1 100644 --- a/todos/tests/test_subtodo_get.py +++ b/todos/tests/test_subtodo_get.py @@ -14,19 +14,21 @@ @pytest.mark.django_db -def test_get_subtodos(create_todo, authenticated_client, content, date): +def test_get_subtodos(create_todo, authenticated_client, content, date, rank): url = reverse("subtodos") SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[0], ) SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[1], ) response = authenticated_client.get( url, {"todo_id": create_todo.id}, format="json" @@ -37,7 +39,7 @@ def test_get_subtodos(create_todo, authenticated_client, content, date): @pytest.mark.django_db def test_get_subtodos_ordering( - create_todo, authenticated_client, content, date + create_todo, authenticated_client, content, date, rank ): url = reverse("subtodos") SubTodo.objects.create( @@ -45,18 +47,21 @@ def test_get_subtodos_ordering( date=date, todo_id=create_todo, is_completed=False, + rank=rank[0], ) SubTodo.objects.create( content="2", date=date, todo_id=create_todo, is_completed=False, + rank=rank[1], ) SubTodo.objects.create( content="3", date=date, todo_id=create_todo, is_completed=False, + rank=rank[2], ) response = authenticated_client.get( url, {"todo_id": create_todo.id}, format="json" @@ -69,7 +74,7 @@ def test_get_subtodos_ordering( @pytest.mark.django_db def test_get_subtodos_between_dates( - create_todo, authenticated_client, content + create_todo, authenticated_client, content, rank ): url = reverse("subtodos") SubTodo.objects.create( @@ -77,18 +82,21 @@ def test_get_subtodos_between_dates( date="2024-08-02", todo_id=create_todo, is_completed=False, + rank=rank[0], ) SubTodo.objects.create( content=content, date="2024-08-04", todo_id=create_todo, is_completed=False, + rank=rank[1], ) SubTodo.objects.create( content=content, date="2024-08-06", todo_id=create_todo, is_completed=False, + rank=rank[2], ) response = authenticated_client.get( url, diff --git a/todos/tests/test_subtodo_patch.py b/todos/tests/test_subtodo_patch.py index 0ae6fb2..c41ed72 100644 --- a/todos/tests/test_subtodo_patch.py +++ b/todos/tests/test_subtodo_patch.py @@ -17,13 +17,14 @@ @pytest.mark.django_db def test_update_subtodo_success( - create_todo, authenticated_client, content, date + create_todo, authenticated_client, content, date, rank ): subtodo = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[0], ) url = reverse("subtodos") # URL name for the SubTodoView patch method data = { @@ -39,19 +40,21 @@ def test_update_subtodo_success( @pytest.mark.django_db def test_update_subtodo_success_top_rank( - create_todo, authenticated_client, content, date + create_todo, authenticated_client, content, date, rank ): subtodo = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[1], ) subtodo2 = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[1], ) url = reverse("subtodos") data = { @@ -71,19 +74,21 @@ def test_update_subtodo_success_top_rank( @pytest.mark.django_db def test_update_subtodo_success_bottom_rank( - create_todo, authenticated_client, content, date + create_todo, authenticated_client, content, date, rank ): subtodo = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[0], ) subtodo2 = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[1], ) url = reverse("subtodos") data = { @@ -103,25 +108,28 @@ def test_update_subtodo_success_bottom_rank( @pytest.mark.django_db def test_update_subtodo_success_between_rank( - create_todo, authenticated_client, content, date + create_todo, authenticated_client, content, date, rank ): subtodo = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[0], ) subtodo2 = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[1], ) subtodo3 = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[2], ) url = reverse("subtodos") data = { @@ -142,13 +150,14 @@ def test_update_subtodo_success_between_rank( @pytest.mark.django_db def test_update_subtodo_invalid_todo_id( - create_todo, authenticated_client, content, date + create_todo, authenticated_client, content, date, rank ): subtodo = SubTodo.objects.create( content=content, date=date, todo_id=create_todo, is_completed=False, + rank=rank[0], ) url = reverse("subtodos") data = { diff --git a/todos/tests/test_todo_delete.py b/todos/tests/test_todo_delete.py index a6c97d3..5c3a34d 100644 --- a/todos/tests/test_todo_delete.py +++ b/todos/tests/test_todo_delete.py @@ -14,7 +14,7 @@ @pytest.mark.django_db def test_delete_todo_success( - authenticated_client, create_category, create_user, date, content + authenticated_client, create_category, create_user, date, content, rank ): todo = Todo.objects.create( user_id=create_user, @@ -22,6 +22,7 @@ def test_delete_todo_success( due_time=None, content=content, category_id=create_category, + rank=rank[0], ) url = reverse("todos") data = {"todo_id": todo.id} diff --git a/todos/tests/test_todo_get.py b/todos/tests/test_todo_get.py index 6cb89da..761516e 100644 --- a/todos/tests/test_todo_get.py +++ b/todos/tests/test_todo_get.py @@ -32,6 +32,7 @@ def test_get_todos( due_time=None, content=content, category_id=create_category, + rank="0|Vzzzzz:", ) Todo.objects.create( user_id=create_user, @@ -39,6 +40,7 @@ def test_get_todos( due_time=None, content=content, category_id=create_category, + rank="0|4n210Vz:", ) response = authenticated_client.get( url, {"user_id": create_user.id}, format="json" diff --git a/todos/tests/test_todo_patch.py b/todos/tests/test_todo_patch.py index fca295b..994aa1d 100644 --- a/todos/tests/test_todo_patch.py +++ b/todos/tests/test_todo_patch.py @@ -3,6 +3,7 @@ import pytest from django.urls import reverse +from Lexorank.src.LexoRank import LexoRank from todos.models import Todo """ @@ -25,6 +26,7 @@ def test_update_todo_success( date, content, due_time, + rank, ): todo = Todo.objects.create( user_id=create_user, @@ -32,6 +34,7 @@ def test_update_todo_success( due_time=None, content=content, category_id=create_category, + rank=rank[0], ) url = reverse("todos") data = { @@ -48,7 +51,7 @@ def test_update_todo_success( @pytest.mark.django_db def test_update_todo_success_bottom_order( - create_user, create_category, authenticated_client, date, content + create_user, create_category, authenticated_client, date, content, rank ): todo = Todo.objects.create( user_id=create_user, @@ -56,6 +59,7 @@ def test_update_todo_success_bottom_order( due_time=None, content=content, category_id=create_category, + rank=rank[0], ) todo2 = Todo.objects.create( user_id=create_user, @@ -63,6 +67,7 @@ def test_update_todo_success_bottom_order( due_time=None, content=content, category_id=create_category, + rank=rank[1], ) url = reverse("todos") data = { @@ -79,7 +84,7 @@ def test_update_todo_success_bottom_order( @pytest.mark.django_db def test_update_todo_success_top_order( - create_user, create_category, authenticated_client, date, content + create_user, create_category, authenticated_client, date, content, rank ): todo = Todo.objects.create( user_id=create_user, @@ -87,6 +92,7 @@ def test_update_todo_success_top_order( due_time=None, content=content, category_id=create_category, + rank=rank[0], ) todo2 = Todo.objects.create( user_id=create_user, @@ -94,6 +100,7 @@ def test_update_todo_success_top_order( due_time=None, content=content, category_id=create_category, + rank=rank[1], ) url = reverse("todos") data = { @@ -105,12 +112,12 @@ def test_update_todo_success_top_order( } response = authenticated_client.patch(url, data, format="json") assert response.status_code == 200 - assert response.data["rank"] < todo.rank + assert LexoRank.parse(response.data["rank"]) < LexoRank.parse(todo.rank) @pytest.mark.django_db def test_update_todo_success_None_order( - create_user, create_category, authenticated_client, date, content + create_user, create_category, authenticated_client, date, content, rank ): todo = Todo.objects.create( user_id=create_user, @@ -118,6 +125,7 @@ def test_update_todo_success_None_order( due_time=None, content=content, category_id=create_category, + rank=rank[0], ) before_rank = todo.rank url = reverse("todos") diff --git a/todos/views.py b/todos/views.py index 20b7c32..087730e 100644 --- a/todos/views.py +++ b/todos/views.py @@ -1,7 +1,6 @@ # todos/views.py -import asyncio import json import sentry_sdk @@ -14,7 +13,7 @@ from onestep_be.settings import openai_client from todos.firebase_messaging import send_push_notification_device -from todos.models import Category, SubTodo, Todo +from todos.models import Category, SubTodo, Todo, UserLastUsage from todos.serializers import ( CategorySerializer, GetTodoSerializer, @@ -67,13 +66,13 @@ def post(self, request): try: data = request.data.copy() data["user_id"] = request.user.id + data["rank"] = Todo.objects.get_next_rank(request.user.id) set_sentry_user(request.user) serializer = TodoSerializer( context={"request": request}, data=data ) if serializer.is_valid(raise_exception=True): - serializer.save() send_push_notification_device( request.auth.get("device"), request.user, @@ -289,13 +288,12 @@ def post(self, request): """ set_sentry_user(request.user) data = request.data + data["rank"] = SubTodo.objects.get_next_rank(request.user.id) serializer = SubTodoSerializer( context={"request": request}, data=data, many=True ) if serializer.is_valid(raise_exception=True): - serializer.save() - send_push_notification_device( request.auth.get("device"), request.user, @@ -402,7 +400,6 @@ def patch(self, request): if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( request.auth.get("device"), request.user, @@ -500,13 +497,13 @@ def post(self, request): try: data = request.data.copy() data["user_id"] = request.user.id + data["rank"] = Category.objects.get_next_rank(request.user.id) serializer = CategorySerializer( context={"request": request}, data=data ) if serializer.is_valid(raise_exception=True): - serializer.save() send_push_notification_device( request.auth.get("device"), request.user, @@ -588,7 +585,6 @@ def patch(self, request): ) if serializer.is_valid(raise_exception=True): serializer.save() - send_push_notification_device( request.auth.get("device"), request.user, @@ -774,17 +770,17 @@ def get(self, request): """ set_sentry_user(request.user) - # user_id = request.user.id + user_id = request.user.id try: - # flag, message = UserLastUsage.check_rate_limit( - # user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS - # ) - - # if flag is False: - # return Response( - # {"error": message}, - # status=status.HTTP_429_TOO_MANY_REQUESTS, - # ) + flag, message = UserLastUsage.check_rate_limit( + user_id=user_id, RATE_LIMIT_SECONDS=RATE_LIMIT_SECONDS + ) + + if flag is False: + return Response( + {"error": message}, + status=status.HTTP_429_TOO_MANY_REQUESTS, + ) todo_id = request.GET.get("todo_id") if todo_id is None: sentry_sdk.capture_message( @@ -803,7 +799,7 @@ def get(self, request): "due_time": todo.due_time, "category_id": todo.category_id, } - completion = asyncio.run(self.get_openai_completion(todo_data)) + completion = self.get_openai_completion(todo_data) return Response( json.loads(completion.choices[0].message.content), status=status.HTTP_200_OK, @@ -820,8 +816,8 @@ def get(self, request): ) # 비동기적으로 OpenAI API를 호출하는 함수 - async def get_openai_completion(self, todo): - return await openai_client.chat.completions.create( + def get_openai_completion(self, todo): + return openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ { From 3a512ca2b0b01ccca57557ca286cd81987fc0874 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 4 Nov 2024 21:56:41 +0900 Subject: [PATCH 216/229] =?UTF-8?q?fix=20:=20lexorank=20+=20llm=20?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lexorank | 2 +- conftest.py | 8 +++++++- onestep_be/settings.py | 1 - todos/models.py | 34 +++++++++++++++++-------------- todos/tests/test_category_post.py | 3 +-- todos/tests/test_subtodo_post.py | 34 +++++++++++++++---------------- todos/tests/test_todo_get.py | 10 +++------ todos/tests/test_todo_patch.py | 7 +++++-- todos/tests/test_todo_post.py | 3 +-- todos/utils.py | 25 ----------------------- todos/views.py | 8 +++----- 11 files changed, 56 insertions(+), 79 deletions(-) diff --git a/Lexorank b/Lexorank index 9000200..9b91e72 160000 --- a/Lexorank +++ b/Lexorank @@ -1 +1 @@ -Subproject commit 900020020b8cdd305e28be868fc3473e1caf03a8 +Subproject commit 9b91e72a137e10ea850f1957044248c2a4d37ed8 diff --git a/conftest.py b/conftest.py index 4d1da62..a23e54f 100644 --- a/conftest.py +++ b/conftest.py @@ -48,11 +48,13 @@ def create_category( create_user, title="Test Category", color=1, + rank="0|hzzzzz:", ): category = Category.objects.create( user_id=create_user, title=title, color=color, + rank=rank, ) return category @@ -66,6 +68,7 @@ def create_todo( due_time=None, content="Test Todo", is_completed=False, + rank="0|hzzzzz:", ): todo = Todo.objects.create( user_id=create_user, @@ -74,6 +77,7 @@ def create_todo( category_id=create_category, content=content, is_completed=is_completed, + rank=rank, ) return todo @@ -86,6 +90,7 @@ def create_subtodo( date="2024-08-01", due_time=None, is_completed=False, + rank="0|hzzzzz:", ): subtodo = SubTodo.objects.create( content=content, @@ -93,6 +98,7 @@ def create_subtodo( due_time=due_time, todo=create_todo, is_completed=is_completed, + rank=rank, ) return subtodo @@ -109,7 +115,7 @@ def content(): @pytest.fixture def rank(): - orders = ["0|Vzzzzz:", "0|4n210Vz:", "0|35RYXWjz:"] + orders = ["0|hzzzzz:", "0|i00007:", "0|i0000f:"] return orders diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 55b237c..971e627 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -65,7 +65,6 @@ "rest_framework_simplejwt", "fcm_django", "django_crontab", - "django_lexorank", ] MIDDLEWARE = [ diff --git a/todos/models.py b/todos/models.py index 70f4486..1fa1a29 100644 --- a/todos/models.py +++ b/todos/models.py @@ -5,7 +5,7 @@ from django.utils import timezone from accounts.models import User -from Lexorank.src.LexoRank import LexoRank +from Lexorank.src.lexo_rank import LexoRank class TodosManager(models.Manager): @@ -20,8 +20,17 @@ def delete_many(self, instances): instance.save() return instances + def get_next_rank_subtodo(self, user_id): + get_list = ( + SubTodo.objects.filter(todo_id__user_id=user_id) + .select_related("todo_id") + .last() + ) + if get_list is None: + return str(LexoRank.middle()) + return str(LexoRank.gen_next(LexoRank.parse(get_list.rank))) + def get_next_rank(self, user_id): - # Get the last rank of the user get_list = ( self.get_queryset().filter(user_id=user_id).order_by("rank").last() ) @@ -37,20 +46,15 @@ def get_update_rank(self, instance, prev_id, next_id): get_rank = str(LexoRank.parse(get_next_rank).gen_prev()) return get_rank elif next_id is None: # Move to the bottom - prev_instance = LexoRank.parse( - self.get_queryset().get(id=prev_id).rank - ) - return str(LexoRank.gen_next(prev_instance.rank)) + get_prev_rank = self.get_queryset().get(id=prev_id).rank + get_rank = str(LexoRank.parse(get_prev_rank).gen_next()) + return get_rank else: # Move to after prev_id - prev_instance = LexoRank.parse( - self.get_queryset().get(id=prev_id).rank - ) - next_instance = LexoRank.parse( - self.get_queryset().get(id=next_id).rank - ) - return str( - LexoRank.between(prev_instance.rank, next_instance.rank) - ) + prev_rank = self.get_queryset().get(id=prev_id).rank + prev_lexo = LexoRank.parse(prev_rank) + next_rank = self.get_queryset().get(id=next_id).rank + next_instance = LexoRank.parse(next_rank) + return str(prev_lexo.between(next_instance)) def get_queryset(self): return super().get_queryset().filter(deleted_at__isnull=True) diff --git a/todos/tests/test_category_post.py b/todos/tests/test_category_post.py index 137ffca..3dafa49 100644 --- a/todos/tests/test_category_post.py +++ b/todos/tests/test_category_post.py @@ -13,7 +13,7 @@ @pytest.mark.django_db def test_create_category_success( - create_user, authenticated_client, content, order, color + create_user, authenticated_client, content, color ): url = reverse("category") data = { @@ -23,4 +23,3 @@ def test_create_category_success( } response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 - assert "id" in response.data diff --git a/todos/tests/test_subtodo_post.py b/todos/tests/test_subtodo_post.py index 5d53996..e726e8b 100644 --- a/todos/tests/test_subtodo_post.py +++ b/todos/tests/test_subtodo_post.py @@ -18,30 +18,28 @@ def test_create_subtodo_success( create_todo, authenticated_client, content, date ): url = reverse("subtodos") - data = [ - { - "content": content, - "date": date, - "todo_id": create_todo.id, - "is_completed": False, - } - ] + data = { + "content": content, + "date": date, + "due_time": None, + "todo_id": create_todo.id, + "is_completed": False, + } + response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 - response_data = response.data[0] # 리스트의 첫 번째 항목 접근 - assert "id" in response_data + assert response.data["content"] == content @pytest.mark.django_db def test_create_subtodo_invalid_todo_id(authenticated_client, content, date): url = reverse("subtodos") - data = [ - { - "content": content, - "date": date, - "todo_id": 999, # Invalid todo id - "is_completed": False, - } - ] + data = { + "content": content, + "date": date, + "due_time": None, + "todo_id": 999, # Invalid todo id + "is_completed": False, + } response = authenticated_client.post(url, data, format="json") assert response.status_code == 400 diff --git a/todos/tests/test_todo_get.py b/todos/tests/test_todo_get.py index 761516e..c1b8507 100644 --- a/todos/tests/test_todo_get.py +++ b/todos/tests/test_todo_get.py @@ -19,11 +19,7 @@ @pytest.mark.django_db def test_get_todos( - create_user, - date, - content, - create_category, - authenticated_client, + create_user, date, content, create_category, authenticated_client, rank ): url = reverse("todos") Todo.objects.create( @@ -32,7 +28,7 @@ def test_get_todos( due_time=None, content=content, category_id=create_category, - rank="0|Vzzzzz:", + rank=rank[0], ) Todo.objects.create( user_id=create_user, @@ -40,7 +36,7 @@ def test_get_todos( due_time=None, content=content, category_id=create_category, - rank="0|4n210Vz:", + rank=rank[1], ) response = authenticated_client.get( url, {"user_id": create_user.id}, format="json" diff --git a/todos/tests/test_todo_patch.py b/todos/tests/test_todo_patch.py index 994aa1d..75796c2 100644 --- a/todos/tests/test_todo_patch.py +++ b/todos/tests/test_todo_patch.py @@ -3,7 +3,7 @@ import pytest from django.urls import reverse -from Lexorank.src.LexoRank import LexoRank +from Lexorank.src.lexo_rank import LexoRank from todos.models import Todo """ @@ -143,7 +143,7 @@ def test_update_todo_success_None_order( @pytest.mark.django_db def test_update_todo_success_between_order( - create_user, create_category, authenticated_client, date, content + create_user, create_category, authenticated_client, date, content, rank ): todo = Todo.objects.create( user_id=create_user, @@ -151,6 +151,7 @@ def test_update_todo_success_between_order( due_time=None, content=content, category_id=create_category, + rank=rank[0], ) todo2 = Todo.objects.create( user_id=create_user, @@ -158,6 +159,7 @@ def test_update_todo_success_between_order( due_time=None, content=content, category_id=create_category, + rank=rank[1], ) todo3 = Todo.objects.create( user_id=create_user, @@ -165,6 +167,7 @@ def test_update_todo_success_between_order( due_time=None, content=content, category_id=create_category, + rank=rank[2], ) url = reverse("todos") data = { diff --git a/todos/tests/test_todo_post.py b/todos/tests/test_todo_post.py index 4f423b0..a8299bb 100644 --- a/todos/tests/test_todo_post.py +++ b/todos/tests/test_todo_post.py @@ -30,8 +30,7 @@ def test_create_todo_success( } response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 - assert "id" in response.data - assert response.data.rank == "mzzzz" + assert response.data["rank"] == "0|hzzzzz:" @pytest.mark.django_db diff --git a/todos/utils.py b/todos/utils.py index 6184809..afa5c10 100644 --- a/todos/utils.py +++ b/todos/utils.py @@ -1,30 +1,5 @@ import sentry_sdk -from todos.lexorank import LexoRank - - -def validate_lexo_order(prev, next, updated): - updated_lexo = LexoRank(updated) - if prev is None and next is None: - return True - if prev is None: - next_lexo = LexoRank(next) - if next_lexo.compare_to(updated_lexo) <= 0: - return False - elif next is None: - prev_lexo = LexoRank(prev) - if prev_lexo.compare_to(updated_lexo) >= 0: - return False - else: - prev_lexo = LexoRank(prev) - next_lexo = LexoRank(next) - if ( - prev_lexo.compare_to(updated_lexo) >= 0 - or next_lexo.compare_to(updated_lexo) <= 0 - ): - return False - return True - def set_sentry_user(user): sentry_sdk.set_user( diff --git a/todos/views.py b/todos/views.py index 087730e..14e7052 100644 --- a/todos/views.py +++ b/todos/views.py @@ -287,11 +287,9 @@ def post(self, request): - content 는 암호화 되어야 합니다(// 미정) """ set_sentry_user(request.user) - data = request.data - data["rank"] = SubTodo.objects.get_next_rank(request.user.id) - serializer = SubTodoSerializer( - context={"request": request}, data=data, many=True - ) + data = request.data.copy() + data["rank"] = SubTodo.objects.get_next_rank_subtodo(request.user.id) + serializer = SubTodoSerializer(context={"request": request}, data=data) if serializer.is_valid(raise_exception=True): send_push_notification_device( From 0bba17355f4a3e2abc06a13659c5b0be09f74cde Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Mon, 4 Nov 2024 22:28:11 +0900 Subject: [PATCH 217/229] =?UTF-8?q?fix=20:=20ecs=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy_to_ecs.yml | 2 +- .github/workflows/deploy_to_ecs_prod.yml | 2 +- todos/views.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index efc6a89..adeaf08 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -16,7 +16,7 @@ jobs: - name: Update version.txt run: | VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 - echo "onestep_dev@${VERSION}" > backend/version.txt + echo "onestep_dev@${VERSION}" > version.txt - name: Set Dockerfile Path id: dockerfile-path diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index a335653..818eced 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -16,7 +16,7 @@ jobs: - name: Update version.txt run: | VERSION=$(TZ=Asia/Seoul date +'%Y.%m.%d.%H.%M.%S') # 한국 시간대 사용 - echo "onestep_prod@${VERSION}" > backend/version.txt + echo "onestep_prod@${VERSION}" > version.txt - name: Set Dockerfile Path id: dockerfile-path diff --git a/todos/views.py b/todos/views.py index 14e7052..cf2ab92 100644 --- a/todos/views.py +++ b/todos/views.py @@ -746,7 +746,7 @@ def get(self, request): class RecommendSubTodo(APIView): - # permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=["RecommendSubTodo"], From 20efe5146c35797f9443e3cb6738b2403ee67cad Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 01:30:27 +0900 Subject: [PATCH 218/229] feat: add ssh private key --- .github/workflows/deploy_to_ecs.yml | 5 +++++ .github/workflows/deploy_to_ecs_prod.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index adeaf08..d686772 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -12,6 +12,11 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + submodules: true + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + persist-credentials: false + fetch-depth: 0 - name: Update version.txt run: | diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 818eced..499448f 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -12,6 +12,11 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + submodules: true + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + persist-credentials: false + fetch-depth: 0 - name: Update version.txt run: | From e52f4c5c23716d0a28e8f7245726e2faf672cdf4 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 01:33:07 +0900 Subject: [PATCH 219/229] fix : yaml --- .github/workflows/deploy_to_ecs_prod.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 499448f..818eced 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -12,11 +12,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - with: - submodules: true - ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} - persist-credentials: false - fetch-depth: 0 - name: Update version.txt run: | From f7ead847a4688cf0aba130a6cb113aa373b18ecf Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 01:33:36 +0900 Subject: [PATCH 220/229] fix : yaml --- .github/workflows/deploy_to_ecs.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index d686772..adeaf08 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -12,11 +12,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - with: - submodules: true - ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} - persist-credentials: false - fetch-depth: 0 - name: Update version.txt run: | From a3618e93e5790662d033c97a79b7af574cf1aa53 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 01:51:48 +0900 Subject: [PATCH 221/229] fix : ecs yml add token --- .github/workflows/deploy_to_ecs.yml | 3 +++ .github/workflows/deploy_to_ecs_prod.yml | 3 +++ .github/workflows/deploy_to_ecs_test.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/deploy_to_ecs.yml b/.github/workflows/deploy_to_ecs.yml index adeaf08..f614640 100644 --- a/.github/workflows/deploy_to_ecs.yml +++ b/.github/workflows/deploy_to_ecs.yml @@ -12,6 +12,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + token: ${{ secrets.ACCESS_TOKEN }} + submodules: true - name: Update version.txt run: | diff --git a/.github/workflows/deploy_to_ecs_prod.yml b/.github/workflows/deploy_to_ecs_prod.yml index 818eced..d1b3b7b 100644 --- a/.github/workflows/deploy_to_ecs_prod.yml +++ b/.github/workflows/deploy_to_ecs_prod.yml @@ -12,6 +12,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + token: ${{ secrets.ACCESS_TOKEN }} + submodules: true - name: Update version.txt run: | diff --git a/.github/workflows/deploy_to_ecs_test.yml b/.github/workflows/deploy_to_ecs_test.yml index 6f3dc21..31faf35 100644 --- a/.github/workflows/deploy_to_ecs_test.yml +++ b/.github/workflows/deploy_to_ecs_test.yml @@ -12,6 +12,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + token: ${{ secrets.ACCESS_TOKEN }} + submodules: true - name: Set Dockerfile Path id: dockerfile-path From 1a7d78c31f0cf60916bda790bd2a6a6837070836 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 02:12:31 +0900 Subject: [PATCH 222/229] fix : version txt --- Lexorank | 2 +- onestep_be/settings.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Lexorank b/Lexorank index 9b91e72..19f5317 160000 --- a/Lexorank +++ b/Lexorank @@ -1 +1 @@ -Subproject commit 9b91e72a137e10ea850f1957044248c2a4d37ed8 +Subproject commit 19f5317c6ab232eab0f1a7707d7e6462f84218cb diff --git a/onestep_be/settings.py b/onestep_be/settings.py index 304d5c7..f2594e0 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -10,7 +10,6 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ -import os from datetime import timedelta from pathlib import Path from urllib.parse import urlparse @@ -236,11 +235,11 @@ # BASE_DIR은 Django 프로젝트의 루트 디렉토리를 가리킵니다. # version.txt 파일이 BASE_DIR에 있다고 가정합니다. -VERSION_FILE_PATH = os.path.join(BASE_DIR, "version.txt") +VERSION_FILE_PATH = BASE_DIR.parent / "version.txt" # 파일 읽기 try: - with open(VERSION_FILE_PATH, "r") as file: + with VERSION_FILE_PATH.open("r") as file: # 파일의 내용을 읽어서 변수에 저장 PROJECT_VERSION = file.read().strip() if PROJECT_VERSION == "": From 645739a27f2de3c6ccf00e4179c21c2cb9323356 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 02:21:38 +0900 Subject: [PATCH 223/229] fix : migrations --- ...tegory_rank_alter_subtodo_rank_and_more.py | 48 +++++++++---------- ...k_remove_todo_rank_subtodo_ank_and_more.py | 4 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py b/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py index 4c8daf4..0e2bf1d 100644 --- a/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py +++ b/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py @@ -1,29 +1,29 @@ -# Generated by Django 4.2.16 on 2024-10-11 06:18 +# # Generated by Django 4.2.16 on 2024-10-11 06:18 -from django.db import migrations -import django_lexorank.fields +# from django.db import migrations +# import django_lexorank.fields -class Migration(migrations.Migration): +# class Migration(migrations.Migration): - dependencies = [ - ('todos', '0014_rename_order_category_rank_rename_order_subtodo_rank_and_more'), - ] +# dependencies = [ +# ('todos', '0014_rename_order_category_rank_rename_order_subtodo_rank_and_more'), +# ] - operations = [ - migrations.AlterField( - model_name='category', - name='rank', - field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), - ), - migrations.AlterField( - model_name='subtodo', - name='rank', - field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), - ), - migrations.AlterField( - model_name='todo', - name='rank', - field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), - ), - ] +# operations = [ +# migrations.AlterField( +# model_name='category', +# name='rank', +# field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), +# ), +# migrations.AlterField( +# model_name='subtodo', +# name='rank', +# field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), +# ), +# migrations.AlterField( +# model_name='todo', +# name='rank', +# field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), +# ), +# ] diff --git a/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py b/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py index e227c85..a9a98ba 100644 --- a/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py +++ b/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py @@ -21,13 +21,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='subtodo', name='ank', - field=models.CharField(default='0|Vzzzzz:', max_length=255), + field=models.CharField(default='0|hzzzzz:', max_length=255), preserve_default=False, ), migrations.AddField( model_name='todo', name='ank', - field=models.CharField(default='0|Vzzzzz:', max_length=255), + field=models.CharField(default='0|hzzzzz:', max_length=255), preserve_default=False, ), migrations.AlterField( From 75c5b7639e0fd2cddca7f955b5eb696e9dbc1e32 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 13:50:14 +0900 Subject: [PATCH 224/229] =?UTF-8?q?fix=20:=20get=5Fnext=5Frank=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todos/models.py b/todos/models.py index 8dc687a..ddc948e 100644 --- a/todos/models.py +++ b/todos/models.py @@ -28,7 +28,7 @@ def get_next_rank_subtodo(self, user_id): ) if get_list is None: return str(LexoRank.middle()) - return str(LexoRank.gen_next(LexoRank.parse(get_list.rank))) + return str((LexoRank.parse(get_list.rank)).gen_next()) def get_next_rank(self, user_id): get_list = ( From a56401988c67e99749be866d6c172b5142a8725e Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 14:17:38 +0900 Subject: [PATCH 225/229] fix : migrations --- ...gory_color_alter_category_rank_and_more.py | 34 +++++++++++++++++ ...tegory_rank_alter_subtodo_rank_and_more.py | 29 -------------- todos/migrations/0016_alter_category_color.py | 19 ---------- ...k_remove_todo_rank_subtodo_ank_and_more.py | 38 ------------------- ...e_ank_subtodo_rank_rename_ank_todo_rank.py | 23 ----------- todos/models.py | 6 +-- 6 files changed, 37 insertions(+), 112 deletions(-) create mode 100644 todos/migrations/0015_alter_category_color_alter_category_rank_and_more.py delete mode 100644 todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py delete mode 100644 todos/migrations/0016_alter_category_color.py delete mode 100644 todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py delete mode 100644 todos/migrations/0018_rename_ank_subtodo_rank_rename_ank_todo_rank.py diff --git a/todos/migrations/0015_alter_category_color_alter_category_rank_and_more.py b/todos/migrations/0015_alter_category_color_alter_category_rank_and_more.py new file mode 100644 index 0000000..bd352ee --- /dev/null +++ b/todos/migrations/0015_alter_category_color_alter_category_rank_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.16 on 2024-11-05 05:13 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('todos', '0014_rename_order_category_rank_rename_order_subtodo_rank_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='category', + name='color', + field=models.SmallIntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(8)]), + ), + migrations.AlterField( + model_name='category', + name='rank', + field=models.CharField(default='0|hzzzzz:', max_length=255), + ), + migrations.AlterField( + model_name='subtodo', + name='rank', + field=models.CharField(default='0|hzzzzz:', max_length=255), + ), + migrations.AlterField( + model_name='todo', + name='rank', + field=models.CharField(default='0|hzzzzz:', max_length=255), + ), + ] diff --git a/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py b/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py deleted file mode 100644 index 0e2bf1d..0000000 --- a/todos/migrations/0015_alter_category_rank_alter_subtodo_rank_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# # Generated by Django 4.2.16 on 2024-10-11 06:18 - -# from django.db import migrations -# import django_lexorank.fields - - -# class Migration(migrations.Migration): - -# dependencies = [ -# ('todos', '0014_rename_order_category_rank_rename_order_subtodo_rank_and_more'), -# ] - -# operations = [ -# migrations.AlterField( -# model_name='category', -# name='rank', -# field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), -# ), -# migrations.AlterField( -# model_name='subtodo', -# name='rank', -# field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), -# ), -# migrations.AlterField( -# model_name='todo', -# name='rank', -# field=django_lexorank.fields.RankField(db_index=True, editable=False, max_length=255), -# ), -# ] diff --git a/todos/migrations/0016_alter_category_color.py b/todos/migrations/0016_alter_category_color.py deleted file mode 100644 index 6904c48..0000000 --- a/todos/migrations/0016_alter_category_color.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.16 on 2024-10-12 08:35 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('todos', '0015_alter_category_rank_alter_subtodo_rank_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='category', - name='color', - field=models.SmallIntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(8)]), - ), - ] diff --git a/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py b/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py deleted file mode 100644 index a9a98ba..0000000 --- a/todos/migrations/0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-04 08:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('todos', '0016_alter_category_color'), - ] - - operations = [ - migrations.RemoveField( - model_name='subtodo', - name='rank', - ), - migrations.RemoveField( - model_name='todo', - name='rank', - ), - migrations.AddField( - model_name='subtodo', - name='ank', - field=models.CharField(default='0|hzzzzz:', max_length=255), - preserve_default=False, - ), - migrations.AddField( - model_name='todo', - name='ank', - field=models.CharField(default='0|hzzzzz:', max_length=255), - preserve_default=False, - ), - migrations.AlterField( - model_name='category', - name='rank', - field=models.CharField(max_length=255), - ), - ] diff --git a/todos/migrations/0018_rename_ank_subtodo_rank_rename_ank_todo_rank.py b/todos/migrations/0018_rename_ank_subtodo_rank_rename_ank_todo_rank.py deleted file mode 100644 index be8c6f3..0000000 --- a/todos/migrations/0018_rename_ank_subtodo_rank_rename_ank_todo_rank.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-04 08:46 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('todos', '0017_remove_subtodo_rank_remove_todo_rank_subtodo_ank_and_more'), - ] - - operations = [ - migrations.RenameField( - model_name='subtodo', - old_name='ank', - new_name='rank', - ), - migrations.RenameField( - model_name='todo', - old_name='ank', - new_name='rank', - ), - ] diff --git a/todos/models.py b/todos/models.py index ddc948e..9db8161 100644 --- a/todos/models.py +++ b/todos/models.py @@ -151,7 +151,7 @@ class Todo(TimeStamp): user_id = models.ForeignKey(User, on_delete=models.CASCADE) is_completed = models.BooleanField(default=False) - rank = models.CharField(max_length=255) + rank = models.CharField(max_length=255, default="0|hzzzzz:") objects = TodosManager() @@ -169,7 +169,7 @@ class SubTodo(TimeStamp): date = models.DateField(null=True) is_completed = models.BooleanField(default=False) - rank = models.CharField(max_length=255) + rank = models.CharField(max_length=255, default="0|hzzzzz:") objects = TodosManager() @@ -185,7 +185,7 @@ class Category(TimeStamp): ) title = models.CharField(max_length=100, null=True) - rank = models.CharField(max_length=255) + rank = models.CharField(max_length=255, default="0|hzzzzz:") objects = TodosManager() From 0691d849e827af73523489c8ed72a53efea2489a Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 14:54:32 +0900 Subject: [PATCH 226/229] =?UTF-8?q?fix=20:=20post=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20+=20rank=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/models.py | 2 +- todos/views.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/todos/models.py b/todos/models.py index 9db8161..701c6fb 100644 --- a/todos/models.py +++ b/todos/models.py @@ -36,7 +36,7 @@ def get_next_rank(self, user_id): ) if get_list is None: return str(LexoRank.middle()) - return str(LexoRank.gen_next(LexoRank.parse(get_list.rank))) + return str(LexoRank.parse(get_list.rank).gen_next()) def get_update_rank(self, instance, prev_id, next_id): if prev_id is None and next_id is None: diff --git a/todos/views.py b/todos/views.py index cf2ab92..62977b2 100644 --- a/todos/views.py +++ b/todos/views.py @@ -73,6 +73,7 @@ def post(self, request): context={"request": request}, data=data ) if serializer.is_valid(raise_exception=True): + serializer.save() send_push_notification_device( request.auth.get("device"), request.user, @@ -292,6 +293,7 @@ def post(self, request): serializer = SubTodoSerializer(context={"request": request}, data=data) if serializer.is_valid(raise_exception=True): + serializer.save() send_push_notification_device( request.auth.get("device"), request.user, @@ -502,6 +504,7 @@ def post(self, request): ) if serializer.is_valid(raise_exception=True): + serializer.save() send_push_notification_device( request.auth.get("device"), request.user, From 40900859a538d37daf93c32928551a6be4a919ae Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 15:18:34 +0900 Subject: [PATCH 227/229] =?UTF-8?q?Fix=20:=20subtodo=20post=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/tests/test_subtodo_post.py | 35 +++++++++++++++++--------------- todos/views.py | 10 ++++++--- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/todos/tests/test_subtodo_post.py b/todos/tests/test_subtodo_post.py index e726e8b..21fc3c3 100644 --- a/todos/tests/test_subtodo_post.py +++ b/todos/tests/test_subtodo_post.py @@ -18,28 +18,31 @@ def test_create_subtodo_success( create_todo, authenticated_client, content, date ): url = reverse("subtodos") - data = { - "content": content, - "date": date, - "due_time": None, - "todo_id": create_todo.id, - "is_completed": False, - } - + data = [ + { + "content": content, + "date": date, + "due_time": None, + "todo_id": create_todo.id, + "is_completed": False, + } + ] response = authenticated_client.post(url, data, format="json") assert response.status_code == 201 - assert response.data["content"] == content + assert response.data[0]["content"] == content @pytest.mark.django_db def test_create_subtodo_invalid_todo_id(authenticated_client, content, date): url = reverse("subtodos") - data = { - "content": content, - "date": date, - "due_time": None, - "todo_id": 999, # Invalid todo id - "is_completed": False, - } + data = [ + { + "content": content, + "date": date, + "due_time": None, + "todo_id": 999, # Invalid todo id + "is_completed": False, + } + ] response = authenticated_client.post(url, data, format="json") assert response.status_code == 400 diff --git a/todos/views.py b/todos/views.py index 62977b2..aa5818a 100644 --- a/todos/views.py +++ b/todos/views.py @@ -289,9 +289,13 @@ def post(self, request): """ set_sentry_user(request.user) data = request.data.copy() - data["rank"] = SubTodo.objects.get_next_rank_subtodo(request.user.id) - serializer = SubTodoSerializer(context={"request": request}, data=data) - + for i in range(len(data)): + data[i]["rank"] = SubTodo.objects.get_next_rank_subtodo( + request.user.id + ) + serializer = SubTodoSerializer( + context={"request": request}, data=data, many=True + ) if serializer.is_valid(raise_exception=True): serializer.save() send_push_notification_device( From e438afde9cffd583908a4158f449e12f20c17120 Mon Sep 17 00:00:00 2001 From: "alpakaka000808@gmail.com" Date: Tue, 5 Nov 2024 15:23:24 +0900 Subject: [PATCH 228/229] =?UTF-8?q?fix=20:=20subTodo=20lexorank=20logic=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todos/models.py | 3 +++ todos/tests/test_subtodo_post.py | 28 ++++++++++++++++++++++++++++ todos/views.py | 6 +++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/todos/models.py b/todos/models.py index 701c6fb..1f1bb63 100644 --- a/todos/models.py +++ b/todos/models.py @@ -30,6 +30,9 @@ def get_next_rank_subtodo(self, user_id): return str(LexoRank.middle()) return str((LexoRank.parse(get_list.rank)).gen_next()) + def gen_next_rank(self, prev_rank): + return str(LexoRank.parse(prev_rank).gen_next()) + def get_next_rank(self, user_id): get_list = ( self.get_queryset().filter(user_id=user_id).order_by("rank").last() diff --git a/todos/tests/test_subtodo_post.py b/todos/tests/test_subtodo_post.py index 21fc3c3..294ae4a 100644 --- a/todos/tests/test_subtodo_post.py +++ b/todos/tests/test_subtodo_post.py @@ -32,6 +32,34 @@ def test_create_subtodo_success( assert response.data[0]["content"] == content +@pytest.mark.django_db +def test_create_subtodo_success_many( + create_todo, authenticated_client, content, date +): + url = reverse("subtodos") + data = [ + { + "content": content, + "date": date, + "due_time": None, + "todo_id": create_todo.id, + "is_completed": False, + }, + { + "content": content + "2", + "date": date, + "due_time": None, + "todo_id": create_todo.id, + "is_completed": False, + }, + ] + response = authenticated_client.post(url, data, format="json") + assert response.status_code == 201 + assert response.data[0]["content"] == content + assert response.data[1]["content"] == content + "2" + assert response.data[0]["rank"] < response.data[1]["rank"] + + @pytest.mark.django_db def test_create_subtodo_invalid_todo_id(authenticated_client, content, date): url = reverse("subtodos") diff --git a/todos/views.py b/todos/views.py index aa5818a..155c0b7 100644 --- a/todos/views.py +++ b/todos/views.py @@ -289,10 +289,10 @@ def post(self, request): """ set_sentry_user(request.user) data = request.data.copy() + rank = SubTodo.objects.get_next_rank_subtodo(request.user.id) for i in range(len(data)): - data[i]["rank"] = SubTodo.objects.get_next_rank_subtodo( - request.user.id - ) + data[i]["rank"] = rank + rank = SubTodo.objects.gen_next_rank(rank) serializer = SubTodoSerializer( context={"request": request}, data=data, many=True ) From 3d28734868ab4b40876c7b72e45a030275bd93fd Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 5 Nov 2024 20:27:36 +0900 Subject: [PATCH 229/229] =?UTF-8?q?fix:=20access=20token,=20refresh=20toke?= =?UTF-8?q?n=20lifetime=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ onestep_be/settings.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b366a8b..8dbc09d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ # Icon must end with two \r Icon +# example.py +example_1.py + onestep_dev/ # Thumbnails ._* diff --git a/onestep_be/settings.py b/onestep_be/settings.py index f2594e0..076983b 100644 --- a/onestep_be/settings.py +++ b/onestep_be/settings.py @@ -104,8 +104,8 @@ # SimpleJWT settings SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30), - "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ACCESS_TOKEN_LIFETIME": timedelta(days=7), + "REFRESH_TOKEN_LIFETIME": timedelta(days=30), "ROTATE_REFRESH_TOKENS": False, "BLACKLIST_AFTER_ROTATION": True, "UPDATE_LAST_LOGIN": False,