From e964a701cc6101901ff2ba24099191cc7a2d1dcb Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:01:18 +0300 Subject: [PATCH 01/16] Create new_deploy.yml --- .github/workflows/new_deploy.yml | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/new_deploy.yml diff --git a/.github/workflows/new_deploy.yml b/.github/workflows/new_deploy.yml new file mode 100644 index 00000000..22de68e8 --- /dev/null +++ b/.github/workflows/new_deploy.yml @@ -0,0 +1,51 @@ +name: 'new deploy prod server' + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: run on server + uses: garygrossgarten/github-action-ssh@release + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + password: ${{ secrets.SERVER_PASSWORD }} + command: | + cd /root/new_api && + git checkout master && + git pull && + + rm -f .env && + touch .env && + + echo "DJANGO_SECRET_KEY=${{ secrets.DEV_DJANGO_SECRET_KEY }}" >> .env && + + echo "DATABASE_NAME=${{ secrets.DEV_DATABASE_NAME }}" >> .env && + echo "DATABASE_PASSWORD=${{ secrets.DEV_DATABASE_PASSWORD }}" >> .env && + echo "DATABASE_USER=${{ secrets.DEV_DATABASE_USER }}" >> .env && + echo "DATABASE_HOST=${{ secrets.DEV_DATABASE_HOST }}" >> .env && + echo "DATABASE_PORT=${{ secrets.DEV_DATABASE_PORT }}" >> .env && + + echo "EMAIL_USER=${{ secrets.EMAIL_USER }}" >> .env && + echo "EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }}" >> .env && + echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env && + echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env && + echo "SELECTEL_ACCOUNT_ID=${{ secrets.SELECTEL_ACCOUNT_ID }}" >> .env && + echo "SELECTEL_CONTAINER_NAME=${{ secrets.SELECTEL_CONTAINER_NAME }}" >> .env && + echo "SELECTEL_CONTAINER_PASSWORD=${{ secrets.SELECTEL_CONTAINER_PASSWORD }}" >> .env && + echo "SELECTEL_CONTAINER_USERNAME=${{ secrets.SELECTEL_CONTAINER_USERNAME }}" >> .env && + + echo "CLICKUP_API_TOKEN=${{ secrets.CLICKUP_API_TOKEN }}" >> .env && + echo "CLICKUP_SPACE_ID=${{ secrets.CLICKUP_SPACE_ID }}" >> .env && + + echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env && + + echo "UNISENDER_GO_API_KEY=${{ secrets.UNISENDER_GO_API_KEY }}" >> .env && + + docker compose -f docker-compose.prod-ci.yml -p prod up -d --build From cf368d08cc515c7f494de9b7f5e385c1d3e8e7d9 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:01:55 +0300 Subject: [PATCH 02/16] Update new_deploy.yml --- .github/workflows/new_deploy.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/new_deploy.yml b/.github/workflows/new_deploy.yml index 22de68e8..b2b08a11 100644 --- a/.github/workflows/new_deploy.yml +++ b/.github/workflows/new_deploy.yml @@ -1,10 +1,7 @@ name: 'new deploy prod server' on: - push: - branches: - - dev - workflow_dispatch: + workflow_dispatch: jobs: deploy: From bdcfdd495596c0b9c239572d9a5eda5bd500211e Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:05:30 +0300 Subject: [PATCH 03/16] Update new_deploy.yml --- .github/workflows/new_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/new_deploy.yml b/.github/workflows/new_deploy.yml index b2b08a11..122e1ed4 100644 --- a/.github/workflows/new_deploy.yml +++ b/.github/workflows/new_deploy.yml @@ -14,7 +14,7 @@ jobs: username: ${{ secrets.SERVER_USER }} password: ${{ secrets.SERVER_PASSWORD }} command: | - cd /root/new_api && + cd /home/app/new_procollab_deploy && git checkout master && git pull && From 70ade2382521b3efea10b1ca89195bd4fb5e015b Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:09:04 +0300 Subject: [PATCH 04/16] Update new_deploy.yml --- .github/workflows/new_deploy.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/new_deploy.yml b/.github/workflows/new_deploy.yml index 122e1ed4..bf27e4cd 100644 --- a/.github/workflows/new_deploy.yml +++ b/.github/workflows/new_deploy.yml @@ -15,8 +15,7 @@ jobs: password: ${{ secrets.SERVER_PASSWORD }} command: | cd /home/app/new_procollab_deploy && - git checkout master && - git pull && + git pull origin master && rm -f .env && touch .env && From 97f067f2869b63314687ee25f34f7e0ce9170a7d Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Sun, 20 Oct 2024 21:19:01 +0300 Subject: [PATCH 05/16] Update new_deploy.yml --- .github/workflows/new_deploy.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/new_deploy.yml b/.github/workflows/new_deploy.yml index bf27e4cd..823a8c06 100644 --- a/.github/workflows/new_deploy.yml +++ b/.github/workflows/new_deploy.yml @@ -20,13 +20,13 @@ jobs: rm -f .env && touch .env && - echo "DJANGO_SECRET_KEY=${{ secrets.DEV_DJANGO_SECRET_KEY }}" >> .env && + echo "DJANGO_SECRET_KEY=${{ secrets.DJANGO_SECRET_KEY }}" >> .env && - echo "DATABASE_NAME=${{ secrets.DEV_DATABASE_NAME }}" >> .env && - echo "DATABASE_PASSWORD=${{ secrets.DEV_DATABASE_PASSWORD }}" >> .env && - echo "DATABASE_USER=${{ secrets.DEV_DATABASE_USER }}" >> .env && - echo "DATABASE_HOST=${{ secrets.DEV_DATABASE_HOST }}" >> .env && - echo "DATABASE_PORT=${{ secrets.DEV_DATABASE_PORT }}" >> .env && + echo "DATABASE_NAME=${{ secrets.DATABASE_NAME }}" >> .env && + echo "DATABASE_PASSWORD=${{ secrets.DATABASE_PASSWORD }}" >> .env && + echo "DATABASE_USER=${{ secrets.DATABASE_USER }}" >> .env && + echo "DATABASE_HOST=${{ secrets.DATABASE_HOST }}" >> .env && + echo "DATABASE_PORT=${{ secrets.DATABASE_PORT }}" >> .env && echo "EMAIL_USER=${{ secrets.EMAIL_USER }}" >> .env && echo "EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }}" >> .env && From 3e1f0f31d427cb8712f71a5ce9b39bdd8e799822 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:37:01 +0300 Subject: [PATCH 06/16] Update new_deploy.yml --- .github/workflows/new_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/new_deploy.yml b/.github/workflows/new_deploy.yml index 823a8c06..23437e19 100644 --- a/.github/workflows/new_deploy.yml +++ b/.github/workflows/new_deploy.yml @@ -44,4 +44,4 @@ jobs: echo "UNISENDER_GO_API_KEY=${{ secrets.UNISENDER_GO_API_KEY }}" >> .env && - docker compose -f docker-compose.prod-ci.yml -p prod up -d --build + docker compose -f docker-compose.prod-ci.yml -p prod up -d --build --force-recreate From f2369333a7d5e5f82b7df90a1eabd53761646b3f Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:42:24 +0300 Subject: [PATCH 07/16] Update docker-compose.prod-ci.yml --- docker-compose.prod-ci.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docker-compose.prod-ci.yml b/docker-compose.prod-ci.yml index 226551b3..ee1a13eb 100644 --- a/docker-compose.prod-ci.yml +++ b/docker-compose.prod-ci.yml @@ -2,16 +2,31 @@ version: '3.9' services: web: + build: + context: . + dockerfile: ./Dockerfile image: ghcr.io/procollab-github/api:latest restart: unless-stopped volumes: - - log:/procollab/log + - ./log:/procollab/log env_file: - .env environment: HOST: 0.0.0.0 expose: - 8000 + + # web: + # image: ghcr.io/procollab-github/api:latest + # restart: unless-stopped + # volumes: + # - log:/procollab/log + # env_file: + # - .env + # environment: + # HOST: 0.0.0.0 + # expose: + # - 8000 grafana: image: grafana/grafana:latest restart: unless-stopped From 5c15a46055d9cee38c91685971b63c67fcea2479 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:42:47 +0300 Subject: [PATCH 08/16] Update new_deploy.yml --- .github/workflows/new_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/new_deploy.yml b/.github/workflows/new_deploy.yml index 23437e19..823a8c06 100644 --- a/.github/workflows/new_deploy.yml +++ b/.github/workflows/new_deploy.yml @@ -44,4 +44,4 @@ jobs: echo "UNISENDER_GO_API_KEY=${{ secrets.UNISENDER_GO_API_KEY }}" >> .env && - docker compose -f docker-compose.prod-ci.yml -p prod up -d --build --force-recreate + docker compose -f docker-compose.prod-ci.yml -p prod up -d --build From 8e3ac1a85efe38ef7c4cc31e7905d7711f7a8b06 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:54:22 +0300 Subject: [PATCH 09/16] Update dev-ci.yml --- .github/workflows/dev-ci.yml | 82 ++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 1600f184..b6f92f74 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -1,51 +1,51 @@ -name: 'Deploy dev server' +# name: 'Deploy dev server' -on: - push: - branches: - - dev - workflow_dispatch: +# on: +# push: +# branches: +# - dev +# workflow_dispatch: -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: run on server - uses: garygrossgarten/github-action-ssh@release - with: - host: ${{ secrets.DEV_SERVER_HOST }} - username: ${{ secrets.DEV_SERVER_USER }} - password: ${{ secrets.DEV_SERVER_PASSWORD }} - command: | - cd /root/api && - git checkout dev && - git pull && +# jobs: +# deploy: +# runs-on: ubuntu-latest +# steps: +# - name: run on server +# uses: garygrossgarten/github-action-ssh@release +# with: +# host: ${{ secrets.DEV_SERVER_HOST }} +# username: ${{ secrets.DEV_SERVER_USER }} +# password: ${{ secrets.DEV_SERVER_PASSWORD }} +# command: | +# cd /root/api && +# git checkout dev && +# git pull && - rm -f .env && - touch .env && +# rm -f .env && +# touch .env && - echo "DJANGO_SECRET_KEY=${{ secrets.DEV_DJANGO_SECRET_KEY }}" >> .env && +# echo "DJANGO_SECRET_KEY=${{ secrets.DEV_DJANGO_SECRET_KEY }}" >> .env && - echo "DATABASE_NAME=${{ secrets.DEV_DATABASE_NAME }}" >> .env && - echo "DATABASE_PASSWORD=${{ secrets.DEV_DATABASE_PASSWORD }}" >> .env && - echo "DATABASE_USER=${{ secrets.DEV_DATABASE_USER }}" >> .env && - echo "DATABASE_HOST=${{ secrets.DEV_DATABASE_HOST }}" >> .env && - echo "DATABASE_PORT=${{ secrets.DEV_DATABASE_PORT }}" >> .env && +# echo "DATABASE_NAME=${{ secrets.DEV_DATABASE_NAME }}" >> .env && +# echo "DATABASE_PASSWORD=${{ secrets.DEV_DATABASE_PASSWORD }}" >> .env && +# echo "DATABASE_USER=${{ secrets.DEV_DATABASE_USER }}" >> .env && +# echo "DATABASE_HOST=${{ secrets.DEV_DATABASE_HOST }}" >> .env && +# echo "DATABASE_PORT=${{ secrets.DEV_DATABASE_PORT }}" >> .env && - echo "EMAIL_USER=${{ secrets.EMAIL_USER }}" >> .env && - echo "EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }}" >> .env && - echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env && - echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env && - echo "SELECTEL_ACCOUNT_ID=${{ secrets.SELECTEL_ACCOUNT_ID }}" >> .env && - echo "SELECTEL_CONTAINER_NAME=${{ secrets.SELECTEL_CONTAINER_NAME }}" >> .env && - echo "SELECTEL_CONTAINER_PASSWORD=${{ secrets.SELECTEL_CONTAINER_PASSWORD }}" >> .env && - echo "SELECTEL_CONTAINER_USERNAME=${{ secrets.SELECTEL_CONTAINER_USERNAME }}" >> .env && +# echo "EMAIL_USER=${{ secrets.EMAIL_USER }}" >> .env && +# echo "EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }}" >> .env && +# echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env && +# echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env && +# echo "SELECTEL_ACCOUNT_ID=${{ secrets.SELECTEL_ACCOUNT_ID }}" >> .env && +# echo "SELECTEL_CONTAINER_NAME=${{ secrets.SELECTEL_CONTAINER_NAME }}" >> .env && +# echo "SELECTEL_CONTAINER_PASSWORD=${{ secrets.SELECTEL_CONTAINER_PASSWORD }}" >> .env && +# echo "SELECTEL_CONTAINER_USERNAME=${{ secrets.SELECTEL_CONTAINER_USERNAME }}" >> .env && - echo "CLICKUP_API_TOKEN=${{ secrets.CLICKUP_API_TOKEN }}" >> .env && - echo "CLICKUP_SPACE_ID=${{ secrets.CLICKUP_SPACE_ID }}" >> .env && +# echo "CLICKUP_API_TOKEN=${{ secrets.CLICKUP_API_TOKEN }}" >> .env && +# echo "CLICKUP_SPACE_ID=${{ secrets.CLICKUP_SPACE_ID }}" >> .env && - echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env && +# echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env && - echo "UNISENDER_GO_API_KEY=${{ secrets.UNISENDER_GO_API_KEY }}" >> .env && +# echo "UNISENDER_GO_API_KEY=${{ secrets.UNISENDER_GO_API_KEY }}" >> .env && - docker compose -f docker-compose.dev-ci.yml up -d --build --force-recreate +# docker compose -f docker-compose.dev-ci.yml up -d --build --force-recreate From 31008045ea4cc30d70a06a3480e07eb6ce412504 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko <110509023+sh1nkey@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:55:06 +0300 Subject: [PATCH 10/16] Update dev-ci.yml --- .github/workflows/dev-ci.yml | 82 ++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index b6f92f74..1600f184 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -1,51 +1,51 @@ -# name: 'Deploy dev server' +name: 'Deploy dev server' -# on: -# push: -# branches: -# - dev -# workflow_dispatch: +on: + push: + branches: + - dev + workflow_dispatch: -# jobs: -# deploy: -# runs-on: ubuntu-latest -# steps: -# - name: run on server -# uses: garygrossgarten/github-action-ssh@release -# with: -# host: ${{ secrets.DEV_SERVER_HOST }} -# username: ${{ secrets.DEV_SERVER_USER }} -# password: ${{ secrets.DEV_SERVER_PASSWORD }} -# command: | -# cd /root/api && -# git checkout dev && -# git pull && +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: run on server + uses: garygrossgarten/github-action-ssh@release + with: + host: ${{ secrets.DEV_SERVER_HOST }} + username: ${{ secrets.DEV_SERVER_USER }} + password: ${{ secrets.DEV_SERVER_PASSWORD }} + command: | + cd /root/api && + git checkout dev && + git pull && -# rm -f .env && -# touch .env && + rm -f .env && + touch .env && -# echo "DJANGO_SECRET_KEY=${{ secrets.DEV_DJANGO_SECRET_KEY }}" >> .env && + echo "DJANGO_SECRET_KEY=${{ secrets.DEV_DJANGO_SECRET_KEY }}" >> .env && -# echo "DATABASE_NAME=${{ secrets.DEV_DATABASE_NAME }}" >> .env && -# echo "DATABASE_PASSWORD=${{ secrets.DEV_DATABASE_PASSWORD }}" >> .env && -# echo "DATABASE_USER=${{ secrets.DEV_DATABASE_USER }}" >> .env && -# echo "DATABASE_HOST=${{ secrets.DEV_DATABASE_HOST }}" >> .env && -# echo "DATABASE_PORT=${{ secrets.DEV_DATABASE_PORT }}" >> .env && + echo "DATABASE_NAME=${{ secrets.DEV_DATABASE_NAME }}" >> .env && + echo "DATABASE_PASSWORD=${{ secrets.DEV_DATABASE_PASSWORD }}" >> .env && + echo "DATABASE_USER=${{ secrets.DEV_DATABASE_USER }}" >> .env && + echo "DATABASE_HOST=${{ secrets.DEV_DATABASE_HOST }}" >> .env && + echo "DATABASE_PORT=${{ secrets.DEV_DATABASE_PORT }}" >> .env && -# echo "EMAIL_USER=${{ secrets.EMAIL_USER }}" >> .env && -# echo "EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }}" >> .env && -# echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env && -# echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env && -# echo "SELECTEL_ACCOUNT_ID=${{ secrets.SELECTEL_ACCOUNT_ID }}" >> .env && -# echo "SELECTEL_CONTAINER_NAME=${{ secrets.SELECTEL_CONTAINER_NAME }}" >> .env && -# echo "SELECTEL_CONTAINER_PASSWORD=${{ secrets.SELECTEL_CONTAINER_PASSWORD }}" >> .env && -# echo "SELECTEL_CONTAINER_USERNAME=${{ secrets.SELECTEL_CONTAINER_USERNAME }}" >> .env && + echo "EMAIL_USER=${{ secrets.EMAIL_USER }}" >> .env && + echo "EMAIL_PASSWORD=${{ secrets.EMAIL_PASSWORD }}" >> .env && + echo "EMAIL_HOST=${{ secrets.EMAIL_HOST }}" >> .env && + echo "EMAIL_PORT=${{ secrets.EMAIL_PORT }}" >> .env && + echo "SELECTEL_ACCOUNT_ID=${{ secrets.SELECTEL_ACCOUNT_ID }}" >> .env && + echo "SELECTEL_CONTAINER_NAME=${{ secrets.SELECTEL_CONTAINER_NAME }}" >> .env && + echo "SELECTEL_CONTAINER_PASSWORD=${{ secrets.SELECTEL_CONTAINER_PASSWORD }}" >> .env && + echo "SELECTEL_CONTAINER_USERNAME=${{ secrets.SELECTEL_CONTAINER_USERNAME }}" >> .env && -# echo "CLICKUP_API_TOKEN=${{ secrets.CLICKUP_API_TOKEN }}" >> .env && -# echo "CLICKUP_SPACE_ID=${{ secrets.CLICKUP_SPACE_ID }}" >> .env && + echo "CLICKUP_API_TOKEN=${{ secrets.CLICKUP_API_TOKEN }}" >> .env && + echo "CLICKUP_SPACE_ID=${{ secrets.CLICKUP_SPACE_ID }}" >> .env && -# echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env && + echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env && -# echo "UNISENDER_GO_API_KEY=${{ secrets.UNISENDER_GO_API_KEY }}" >> .env && + echo "UNISENDER_GO_API_KEY=${{ secrets.UNISENDER_GO_API_KEY }}" >> .env && -# docker compose -f docker-compose.dev-ci.yml up -d --build --force-recreate + docker compose -f docker-compose.dev-ci.yml up -d --build --force-recreate From f2ab4fe17e0a1e164fad40bd0b20304337f300a8 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko Date: Tue, 12 Nov 2024 00:49:57 +0300 Subject: [PATCH 11/16] loadout students --- users/admin.py | 111 +++++++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/users/admin.py b/users/admin.py index 4f6ebba8..e5992f48 100644 --- a/users/admin.py +++ b/users/admin.py @@ -277,8 +277,7 @@ def get_export_users_emails(self, users): headers=[ "Имя и фамилия", "Возраст", - "Интересы", - "ВУЗ / Школа", + "ВУЗ", "Специальность", "Эл. почта", ] @@ -286,65 +285,77 @@ def get_export_users_emails(self, users): today = date.today() - date_limit_18 = date(today.year - 18, today.month, today.day) - users = ( - CustomUser.objects.all() - .select_related("v2_speciality") - .prefetch_related( - "collaborations__project", - "collaborations__project__industry", - "skills__skill", - "education", - ) + # date_limit_18 = date(today.year - 18, today.month, today.day) + user_ed = ( + UserEducation.objects + .select_related("user", "user__v2_speciality") + .filter(education_status="Студент") ) - little_mans = users.filter(birthday__lte=date_limit_18) - big_mans = users.exclude(id__in=little_mans.values_list("id", flat=True)) + # users = ( + # CustomUser.objects.all() + # .select_related("v2_speciality") + # ) + # little_mans = users.filter(birthday__lte=date_limit_18) + # big_mans = users.exclude(id__in=little_mans.values_list("id", flat=True)) # whole_quality = users.count() # quantity_little_mans = little_mans.count() # quantity_big_mans = whole_quality - quantity_little_mans - for baby in little_mans: - interests = [ - collab.project.industry.name if collab.project.industry else "" - for collab in baby.collaborations.all() - ] - if not len(interests): - interests = [ - skill_to_obj.skill.name if skill_to_obj.skill else "" - for skill_to_obj in baby.skills.all() - ] - if not len(interests): - interests = baby.key_skills.split(",") if baby.key_skills else [] + for ed in user_ed: response_data.append( - [ - baby.first_name + " " + baby.last_name, - today.year - baby.birthday.year, - ", ".join(interests), - "; ".join(baby.education.values_list("organization_name", flat=True)), - baby.v2_speciality if baby.v2_speciality else baby.speciality, - baby.email, - ] - ) + [ + ed.user.first_name + " " + ed.user.last_name, + (today.year - ed.user.birthday.year) if ed.user.birthday.year else None, + ed.organization_name, + ed.user.speciality_v2 if ed.user.speciality_v2 else ed.user.speciality, + ed.user.email - for big_man in big_mans: - industry_names = [ - collab.project.industry.name if collab.project.industry else "" - for collab in big_man.collaborations.all() ] - response_data.append( - [ - big_man.first_name + " " + big_man.last_name, - today.year - big_man.birthday.year, - ", ".join(industry_names), - "; ".join(big_man.education.values_list("organization_name", flat=True)), - big_man.v2_speciality - if big_man.v2_speciality - else big_man.speciality, - big_man.email, - ] + ) + # for baby in little_mans: + # interests = [ + # collab.project.industry.name if collab.project.industry else "" + # for collab in baby.collaborations.all() + # ] + # if not len(interests): + # interests = [ + # skill_to_obj.skill.name if skill_to_obj.skill else "" + # for skill_to_obj in baby.skills.all() + # ] + # if not len(interests): + # interests = baby.key_skills.split(",") if baby.key_skills else [] + # response_data.append( + # [ + # baby.first_name + " " + baby.last_name, + # today.year - baby.birthday.year, + # ", ".join(interests), + # "; ".join(baby.education.values_list("organization_name", flat=True)), + # baby.v2_speciality if baby.v2_speciality else baby.speciality, + # baby.email, + # ] + # ) + # + # for big_man in big_mans: + # industry_names = [ + # collab.project.industry.name if collab.project.industry else "" + # for collab in big_man.collaborations.all() + # ] + # response_data.append( + # [ + # big_man.first_name + " " + big_man.last_name, + # today.year - big_man.birthday.year, + # ", ".join(industry_names), + # "; ".join(big_man.education.values_list("organization_name", flat=True)), + # big_man.v2_speciality + # if big_man.v2_speciality + # else big_man.speciality, + # big_man.email, + # ] + # ) + # для малолеток указать теги проектов, если нет - навыки # для старших - специальность, вуз, учебное заведение From cafe15e445ea8eea238a7f37fbf180385436ae71 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko Date: Tue, 12 Nov 2024 00:50:54 +0300 Subject: [PATCH 12/16] loadout students --- users/admin.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/users/admin.py b/users/admin.py index e5992f48..e277ef29 100644 --- a/users/admin.py +++ b/users/admin.py @@ -286,11 +286,9 @@ def get_export_users_emails(self, users): today = date.today() # date_limit_18 = date(today.year - 18, today.month, today.day) - user_ed = ( - UserEducation.objects - .select_related("user", "user__v2_speciality") - .filter(education_status="Студент") - ) + user_ed = UserEducation.objects.select_related( + "user", "user__v2_speciality" + ).filter(education_status="Студент") # users = ( # CustomUser.objects.all() # .select_related("v2_speciality") @@ -304,15 +302,17 @@ def get_export_users_emails(self, users): for ed in user_ed: response_data.append( - [ - ed.user.first_name + " " + ed.user.last_name, - (today.year - ed.user.birthday.year) if ed.user.birthday.year else None, - ed.organization_name, - ed.user.speciality_v2 if ed.user.speciality_v2 else ed.user.speciality, - ed.user.email - - ] - + [ + ed.user.first_name + " " + ed.user.last_name, + (today.year - ed.user.birthday.year) + if ed.user.birthday.year + else None, + ed.organization_name, + ed.user.speciality_v2 + if ed.user.speciality_v2 + else ed.user.speciality, + ed.user.email, + ] ) # for baby in little_mans: From 7b4b66ce019e6384378c1a53ae38ff8eac02dbd7 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko Date: Tue, 12 Nov 2024 00:59:08 +0300 Subject: [PATCH 13/16] loadout students --- users/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/users/admin.py b/users/admin.py index e277ef29..e4d8dc74 100644 --- a/users/admin.py +++ b/users/admin.py @@ -308,8 +308,8 @@ def get_export_users_emails(self, users): if ed.user.birthday.year else None, ed.organization_name, - ed.user.speciality_v2 - if ed.user.speciality_v2 + ed.user.v2_speciality + if ed.user.v2_speciality else ed.user.speciality, ed.user.email, ] From b2841ef629d77cc4689003ae72d98113b3f567b1 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko Date: Tue, 12 Nov 2024 01:11:47 +0300 Subject: [PATCH 14/16] loadout students --- users/admin.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/users/admin.py b/users/admin.py index e4d8dc74..d6df5cbb 100644 --- a/users/admin.py +++ b/users/admin.py @@ -277,7 +277,7 @@ def get_export_users_emails(self, users): headers=[ "Имя и фамилия", "Возраст", - "ВУЗ", + "Город", "Специальность", "Эл. почта", ] @@ -285,14 +285,19 @@ def get_export_users_emails(self, users): today = date.today() - # date_limit_18 = date(today.year - 18, today.month, today.day) - user_ed = UserEducation.objects.select_related( - "user", "user__v2_speciality" - ).filter(education_status="Студент") - # users = ( - # CustomUser.objects.all() - # .select_related("v2_speciality") - # ) + date_limit_18 = date(today.year - 18, today.month, today.day) + date_limit_22 = date(today.year - 22, today.month, today.day) + # user_ed = UserEducation.objects.select_related( + # "user", "user__v2_speciality" + # ).filter(education_status="Студент") + users = ( + CustomUser.objects.all() + .select_related("v2_speciality") + .filter( + birthday__gte=date_limit_18, + birthday__lte=date_limit_22 + ) + ) # little_mans = users.filter(birthday__lte=date_limit_18) # big_mans = users.exclude(id__in=little_mans.values_list("id", flat=True)) @@ -300,18 +305,18 @@ def get_export_users_emails(self, users): # quantity_little_mans = little_mans.count() # quantity_big_mans = whole_quality - quantity_little_mans - for ed in user_ed: + for user in users: response_data.append( [ - ed.user.first_name + " " + ed.user.last_name, - (today.year - ed.user.birthday.year) - if ed.user.birthday.year + user.first_name + " " + user.last_name, + (today.year - user.birthday.year) + if user.birthday.year else None, - ed.organization_name, - ed.user.v2_speciality - if ed.user.v2_speciality - else ed.user.speciality, - ed.user.email, + user.city, + user.v2_speciality + if user.v2_speciality + else user.speciality, + user.email, ] ) From 4edb4b1a5da0ff93231d767547e09d428a815408 Mon Sep 17 00:00:00 2001 From: Alexey Kudelko Date: Tue, 12 Nov 2024 01:18:16 +0300 Subject: [PATCH 15/16] loadout students --- users/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/users/admin.py b/users/admin.py index d6df5cbb..6a4ca8c4 100644 --- a/users/admin.py +++ b/users/admin.py @@ -294,8 +294,8 @@ def get_export_users_emails(self, users): CustomUser.objects.all() .select_related("v2_speciality") .filter( - birthday__gte=date_limit_18, - birthday__lte=date_limit_22 + birthday__lte=date_limit_18, + birthday__gte=date_limit_22 ) ) # little_mans = users.filter(birthday__lte=date_limit_18) From 029033658fe8e9279a45a2869481b58db01595c2 Mon Sep 17 00:00:00 2001 From: f1xgun Date: Sat, 12 Oct 2024 16:18:00 +0300 Subject: [PATCH 16/16] add flake8-plugins to CI and fix linter warnings --- .flake8 | 7 +- .pre-commit-config.yaml | 10 +- chats/models.py | 6 +- chats/utils.py | 4 +- chats/websockets_settings.py | 2 +- core/serializers.py | 2 +- core/services.py | 9 +- feed/views.py | 8 +- files/admin.py | 5 +- files/service.py | 65 ++++++------ mailing/{typing.py => definitions.py} | 0 mailing/utils.py | 2 +- metrics/views.py | 2 +- news/managers.py | 1 + partner_programs/admin.py | 29 ++++-- partner_programs/models.py | 1 + partner_programs/services.py | 72 +++++++++---- poetry.lock | 107 +++++++++++++++++--- procollab/settings.py | 11 +- project_rates/{typing.py => definitions.py} | 4 +- project_rates/models.py | 4 +- project_rates/serializers.py | 2 +- projects/helpers.py | 12 +-- projects/permissions.py | 22 ++-- projects/serializers.py | 1 - projects/views.py | 7 +- pyproject.toml | 7 +- users/constants.py | 4 +- users/{typing.py => definitions.py} | 0 users/models.py | 30 +++--- users/schema.py | 4 +- users/serializers.py | 35 +++---- users/services/cv_data_prepare.py | 32 ++++-- users/signals.py | 4 +- users/urls.py | 5 +- users/utils.py | 4 +- users/validators.py | 4 +- vacancy/admin.py | 2 +- vacancy/serializers.py | 1 - vacancy/tasks.py | 2 +- 40 files changed, 349 insertions(+), 180 deletions(-) rename mailing/{typing.py => definitions.py} (100%) rename project_rates/{typing.py => definitions.py} (78%) rename users/{typing.py => definitions.py} (100%) diff --git a/.flake8 b/.flake8 index 431c78ac..9f37736d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,11 +1,6 @@ [flake8] max-line-length = 120 -ignore = - VNE002 - VNE003 - W503 - exclude = ./.cache, ./.git, @@ -20,4 +15,4 @@ exclude = ./.vscode, *migrations*, - enable-extensions = print, VNE + enable-extensions = print, VNE, B, A, C4, BLK diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 462fe4a1..d8177e6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,14 @@ repos: exclude: ^.*\b(migrations)\b.*$ language_version: python3 - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + rev: 6.1.0 hooks: - id: flake8 + additional_dependencies: + - flake8-print + - flake8-variables-names + - flake8-bugbear + - flake8-builtins + - flake8-comprehensions + - flake8-broken-line + - flake8-mutable \ No newline at end of file diff --git a/chats/models.py b/chats/models.py index 3a84ddbd..cbbc3d51 100644 --- a/chats/models.py +++ b/chats/models.py @@ -85,7 +85,7 @@ class ProjectChat(BaseChat): created_at: A DateTimeField indicating date of creation. """ - id = models.PositiveIntegerField(primary_key=True, unique=True) + id = models.PositiveIntegerField(primary_key=True, unique=True) # noqa A003 VNE003 project = models.ForeignKey( Project, on_delete=models.CASCADE, related_name="project_chats" ) @@ -126,7 +126,7 @@ class DirectChat(BaseChat): get_users: returns list of users, who are in chat """ - id = models.CharField(primary_key=True, max_length=64) + id = models.CharField(primary_key=True, max_length=64) # noqa A003 VNE003 users = models.ManyToManyField(User, related_name="direct_chats") def get_users(self): @@ -287,7 +287,7 @@ class Meta: class FileToMessage(models.Model): - file = models.OneToOneField( + file = models.OneToOneField( # noqa VNE002 UserFile, on_delete=models.CASCADE, related_name="file_to_message", diff --git a/chats/utils.py b/chats/utils.py index 02f36b06..189410b5 100644 --- a/chats/utils.py +++ b/chats/utils.py @@ -108,12 +108,12 @@ async def create_file_to_message( async def match_files_and_messages(file_urls, messages): for url in file_urls: - file = await sync_to_async(UserFile.objects.get)(pk=url) + user_file = await sync_to_async(UserFile.objects.get)(pk=url) # implicitly matches a file and a message await create_file_to_message( direct_message=messages["direct_message"], project_message=messages["project_message"], - file=file, + file=user_file, ) diff --git a/chats/websockets_settings.py b/chats/websockets_settings.py index d03205e7..9ce65038 100644 --- a/chats/websockets_settings.py +++ b/chats/websockets_settings.py @@ -27,5 +27,5 @@ class EventGroupType(str, Enum): @dataclass(frozen=True) class Event: - type: EventType + type: EventType # noqa: A003, VNE003 content: dict diff --git a/core/serializers.py b/core/serializers.py index 6fff7685..cdccc536 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -29,7 +29,7 @@ class Meta: class SkillToObjectSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(source="skill.id") + id = serializers.IntegerField(source="skill.id") # noqa A003 VNE003 name = serializers.CharField(source="skill.name") category = SkillCategorySerializer(source="skill.category") diff --git a/core/services.py b/core/services.py index 21369186..6c87a587 100644 --- a/core/services.py +++ b/core/services.py @@ -131,6 +131,7 @@ def get_links(obj): class Base64ImageEncoder: """Encode image to base64.""" + BASE_QUALITY: int = 85 def get_encoded_base64_from_url(self, url: str, max_width: int = 300) -> str: @@ -146,10 +147,12 @@ def get_encoded_base64_from_url(self, url: str, max_width: int = 300) -> str: def get_encoded_base64_from_local_path(self, image_path: str) -> str: """Returns the full prepared base64 string pron url path.""" with open(image_path, "rb") as image_file: - base64_image = base64.b64encode(image_file.read()).decode('utf-8') + base64_image = base64.b64encode(image_file.read()).decode("utf-8") return self._base64_full_string(base64_image, image_path.split(".")[-1]) - def _base64_full_string(self, base_64_string: str, image_extension: str = "jpeg") -> str: + def _base64_full_string( + self, base_64_string: str, image_extension: str = "jpeg" + ) -> str: return f"data:image/{image_extension};base64,{base_64_string}" def _get_compressed_image(self, image_data, max_width: int) -> str: @@ -158,5 +161,5 @@ def _get_compressed_image(self, image_data, max_width: int) -> str: image.thumbnail((max_width, max_width)) buffer = BytesIO() image.save(buffer, format="JPEG", quality=self.BASE_QUALITY) - base64_image = base64.b64encode(buffer.getvalue()).decode('utf-8') + base64_image = base64.b64encode(buffer.getvalue()).decode("utf-8") return base64_image diff --git a/feed/views.py b/feed/views.py index 01c4450e..fd2ec5b9 100644 --- a/feed/views.py +++ b/feed/views.py @@ -29,11 +29,9 @@ def _get_filter_data(self) -> list[str]: def _get_excluded_projects_ids(self) -> list[int]: """IDs for exclude projects which in Partner Program.""" - excluded_projects = ( - PartnerProgramUserProfile.objects - .values_list("project_id", flat=True) - .exclude(project_id__isnull=True) - ) + excluded_projects = PartnerProgramUserProfile.objects.values_list( + "project_id", flat=True + ).exclude(project_id__isnull=True) return excluded_projects def get_queryset(self) -> QuerySet[News]: diff --git a/files/admin.py b/files/admin.py index 75d70239..c402537a 100644 --- a/files/admin.py +++ b/files/admin.py @@ -9,7 +9,7 @@ class UserFileForm(ModelForm): - file = FileField(required=True) + file = FileField(required=True) # noqa VNE002 class Meta: model = UserFile @@ -51,7 +51,8 @@ def filename(self, obj): @admin.display(empty_value="Empty link") def short_link(self, obj): - return reprlib.repr(obj.link.lstrip("https://")).strip("'") + link = obj.link.removeprefix("https://") + return reprlib.repr(link).removesuffix("'").removeprefix("'") def get_fieldsets(self, request, obj=None): fieldsets = super().get_fieldsets(request, obj) diff --git a/files/service.py b/files/service.py index fafe5698..ba1a6e96 100644 --- a/files/service.py +++ b/files/service.py @@ -18,33 +18,36 @@ class File: def __init__( - self, file: TemporaryUploadedFile | InMemoryUploadedFile, quality: int = 70 + self, + file_obj: TemporaryUploadedFile | InMemoryUploadedFile, + quality: int = 70, ): - self.size = file.size - self.name = File._get_name(file) - self.extension = File._get_extension(file) - self.buffer = file.open(mode="rb") - self.content_type = file.content_type + self.size = file_obj.size + self.name = File._get_name(file_obj) + self.extension = File._get_extension(file_obj) + self.buffer = file_obj.open(mode="rb") + self.content_type = file_obj.content_type # we can compress given type of image if self.content_type in SUPPORTED_IMAGES_TYPES: - webp_image = convert_image_to_webp(file, quality) + webp_image = convert_image_to_webp(file_obj, quality) self.buffer = webp_image.buffer() self.size = webp_image.size self.content_type = "image/webp" self.extension = "webp" @staticmethod - def _get_name(file) -> str: - name_parts = file.name.split(".") - if len(name_parts) == 1: - return name_parts[0] - return ".".join(name_parts[:-1]) + def _get_name(file_obj) -> str: + file_name_parts = file_obj.name.split(".") + if len(file_name_parts) == 1: + return file_name_parts[0] + return ".".join(file_name_parts[:-1]) @staticmethod - def _get_extension(file) -> str: - if len(file.name.split(".")) > 1: - return file.name.split(".")[-1] + def _get_extension(file_obj) -> str: + file_name_parts = file_obj.name.split(".") + if len(file_name_parts) > 1: + return file_name_parts[-1] return "" @@ -54,7 +57,7 @@ def delete(self, url: str) -> Response: pass @abstractmethod - def upload(self, file: File, user: User) -> FileInfo: + def upload(self, file_obj: File, user: User) -> FileInfo: pass @@ -63,32 +66,32 @@ def delete(self, url: str) -> Response: token = self._get_auth_token() return requests.delete(url, headers={"X-Auth-Token": token}) - def upload(self, file: File, user: User) -> FileInfo: - url = self._upload(file, user) + def upload(self, file_obj: File, user: User) -> FileInfo: + url = self._upload(file_obj, user) return FileInfo( url=url, - name=file.name, - extension=file.extension, - mime_type=file.content_type, - size=file.size, + name=file_obj.name, + extension=file_obj.extension, + mime_type=file_obj.content_type, + size=file_obj.size, ) - def _upload(self, file: File, user: User) -> str: + def _upload(self, file_obj: File, user: User) -> str: token = self._get_auth_token() - url = self._generate_url(file, user) + url = self._generate_url(file_obj, user) requests.put( url, headers={ "X-Auth-Token": token, - "Content-Type": file.content_type, + "Content-Type": file_obj.content_type, }, - data=file.buffer, + data=file_obj.buffer, ) return url - def _generate_url(self, file: File, user: User) -> str: + def _generate_url(self, file_obj: File, user: User) -> str: """ Generates url for selcdn Returns: @@ -97,9 +100,9 @@ def _generate_url(self, file: File, user: User) -> str: return ( f"{SELECTEL_SWIFT_URL}" f"{abs(hash(user.email))}" - f"/{abs(hash(file.name))}" + f"/{abs(hash(file_obj.name))}" f"_{abs(hash(time.time()))}" - f".{file.extension}" + f".{file_obj.extension}" ) @staticmethod @@ -138,8 +141,8 @@ def delete(self, url: str) -> Response: def upload( self, - file: TemporaryUploadedFile | InMemoryUploadedFile, + file_obj: TemporaryUploadedFile | InMemoryUploadedFile, user: User, quality: int = 70, ) -> FileInfo: - return self.storage.upload(File(file, quality), user) + return self.storage.upload(File(file_obj, quality), user) diff --git a/mailing/typing.py b/mailing/definitions.py similarity index 100% rename from mailing/typing.py rename to mailing/definitions.py diff --git a/mailing/utils.py b/mailing/utils.py index e7ead6a0..8886dd16 100644 --- a/mailing/utils.py +++ b/mailing/utils.py @@ -12,7 +12,7 @@ from django.core.mail import EmailMultiAlternatives from django.template import Context, Template -from .typing import MailDataDict, EmailDataToPrepare +from .definitions import MailDataDict, EmailDataToPrepare User = get_user_model() diff --git a/metrics/views.py b/metrics/views.py index 8bb1311c..8d2d8cd3 100644 --- a/metrics/views.py +++ b/metrics/views.py @@ -20,7 +20,7 @@ class MetricsView(APIView): permission_classes = [permissions.IsAdminUser] - def get(self, request, format=None): + def get(self, request): data = {} models = [User, Expert, Investor, Member, Mentor, Project, Vacancy] diff --git a/news/managers.py b/news/managers.py index 21b3ec7b..587e899e 100644 --- a/news/managers.py +++ b/news/managers.py @@ -2,6 +2,7 @@ from django.db import models from django.db.models.query import QuerySet import typing + if typing.TYPE_CHECKING: from news.models import News diff --git a/partner_programs/admin.py b/partner_programs/admin.py index 4b7465f7..5c302efa 100644 --- a/partner_programs/admin.py +++ b/partner_programs/admin.py @@ -155,7 +155,9 @@ def get_export_file(self, partner_program: PartnerProgram): return response def get_export_rates_view(self, request, object_id): - rates_data_to_write: list[dict] = self._get_prepared_rates_data_for_export(object_id) + rates_data_to_write: list[dict] = self._get_prepared_rates_data_for_export( + object_id + ) xlsx_file_writer = XlsxFileToExport() xlsx_file_writer.write_data_to_xlsx(rates_data_to_write) @@ -170,7 +172,9 @@ def get_export_rates_view(self, request, object_id): binary_data_to_export, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) - response["Content-Disposition"] = f'attachment; filename*=UTF-8\'\'{encoded_file_name}' + response[ + "Content-Disposition" + ] = f"attachment; filename*=UTF-8''{encoded_file_name}" return response def _get_prepared_rates_data_for_export(self, program_id: int) -> list[dict]: @@ -179,19 +183,20 @@ def _get_prepared_rates_data_for_export(self, program_id: int) -> list[dict]: Columns example: ФИО|Email|Регион_РФ|Учебное_заведение|Название_учебного_заведения|Класс_курс|Фамилия эксперта|**criteria """ - criterias = Criteria.objects.filter(partner_program__id=program_id).select_related("partner_program") + criterias = Criteria.objects.filter( + partner_program__id=program_id + ).select_related("partner_program") scores = ( - ProjectScore.objects - .filter(criteria__in=criterias) + ProjectScore.objects.filter(criteria__in=criterias) .select_related("user", "criteria", "project") .order_by("project", "criteria") ) - user_programm_profiles = ( - PartnerProgramUserProfile.objects - .filter(partner_program__id=program_id) - .select_related("user") + user_programm_profiles = PartnerProgramUserProfile.objects.filter( + partner_program__id=program_id + ).select_related("user") + projects = ( + Project.objects.filter(scores__in=scores).select_related("leader").distinct() ) - projects = Project.objects.filter(scores__in=scores).select_related("leader").distinct() # To reduce the number of DB requests. user_profiles_dict: dict[int, PartnerProgramUserProfile] = { @@ -203,7 +208,9 @@ def _get_prepared_rates_data_for_export(self, program_id: int) -> list[dict]: prepared_projects_rates_data: list[dict] = [] for project in projects: - project_data_preparer = ProjectScoreDataPreparer(user_profiles_dict, scores_dict, project.id, program_id) + project_data_preparer = ProjectScoreDataPreparer( + user_profiles_dict, scores_dict, project.id, program_id + ) full_project_rates_data: dict = { **project_data_preparer.get_project_user_info(), **project_data_preparer.get_project_expert_info(), diff --git a/partner_programs/models.py b/partner_programs/models.py index cd7d3550..9ea510fe 100644 --- a/partner_programs/models.py +++ b/partner_programs/models.py @@ -26,6 +26,7 @@ class PartnerProgram(models.Model): datetime_created: A DateTimeField indicating date of creation. datetime_updated: A DateTimeField indicating date of update. """ + PROJECTS_AVAILABILITY_CHOISES = [ ("all_users", "Всем пользователям"), ("experts_only", "Только экспертам"), diff --git a/partner_programs/services.py b/partner_programs/services.py index a374a741..4dd0b014 100644 --- a/partner_programs/services.py +++ b/partner_programs/services.py @@ -23,16 +23,14 @@ class ProjectScoreDataPreparer: "Класс_курс": "ОШИБКА", } - EXPERT_ERROR_FIELDS = { - "Фамилия эксперта": "ОШИБКА" - } + EXPERT_ERROR_FIELDS = {"Фамилия эксперта": "ОШИБКА"} def __init__( self, user_profiles: dict[int, PartnerProgramUserProfile], scores: dict[int, list[ProjectScore]], project_id: int, - program_id: int + program_id: int, ): self._project_id = project_id self._user_profiles = user_profiles @@ -41,35 +39,54 @@ def __init__( def get_project_user_info(self) -> dict[str, str]: try: - user_program_profile: PartnerProgramUserProfile = self._user_profiles.get(self._project_id) - user_program_profile_json: dict = user_program_profile.partner_program_data if user_program_profile else {} + user_program_profile: PartnerProgramUserProfile = self._user_profiles.get( + self._project_id + ) + user_program_profile_json: dict = ( + user_program_profile.partner_program_data if user_program_profile else {} + ) user_info: dict[str, str] = { - "Фамилия": user_program_profile.user.last_name if user_program_profile else '', - "Имя": user_program_profile.user.first_name if user_program_profile else '', - "Отчество": user_program_profile.user.patronymic if user_program_profile else '', + "Фамилия": user_program_profile.user.last_name + if user_program_profile + else "", + "Имя": user_program_profile.user.first_name + if user_program_profile + else "", + "Отчество": user_program_profile.user.patronymic + if user_program_profile + else "", "Email": ( - user_program_profile_json.get('email') if user_program_profile_json.get('email') + user_program_profile_json.get("email") + if user_program_profile_json.get("email") else user_program_profile.user.email ), - "Регион_РФ": user_program_profile_json.get('region', ''), - "Учебное_заведение": user_program_profile_json.get('education_type', ''), - "Название_учебного_заведения": user_program_profile_json.get('institution_name', ''), - "Класс_курс": user_program_profile_json.get('class_course', ''), + "Регион_РФ": user_program_profile_json.get("region", ""), + "Учебное_заведение": user_program_profile_json.get("education_type", ""), + "Название_учебного_заведения": user_program_profile_json.get( + "institution_name", "" + ), + "Класс_курс": user_program_profile_json.get("class_course", ""), } return user_info except Exception as e: - logger.error(f"Prepare export rates data about user error: {str(e)}", exc_info=True) + logger.error( + f"Prepare export rates data about user error: {str(e)}", exc_info=True + ) return self.USER_ERROR_FIELDS def get_project_expert_info(self) -> dict[str, str]: try: project_scores: list[ProjectScore] = self._scores.get(self._project_id, []) first_score = project_scores[0] if project_scores else None - expert_last_name: dict[str, str] = {"Фамилия эксперта": first_score.user.last_name if first_score else ''} + expert_last_name: dict[str, str] = { + "Фамилия эксперта": first_score.user.last_name if first_score else "" + } return expert_last_name except Exception as e: - logger.error(f"Prepare export rates data about expert error: {str(e)}", exc_info=True) + logger.error( + f"Prepare export rates data about expert error: {str(e)}", exc_info=True + ) return self.EXPERT_ERROR_FIELDS def get_project_scores_info(self) -> dict[str, str]: @@ -78,16 +95,29 @@ def get_project_scores_info(self) -> dict[str, str]: project_scores: list[ProjectScore] = self._scores.get(self._project_id, []) score_info_with_out_comment: dict[str, str] = { score.criteria.name: score.value - for score in project_scores if score.criteria.name != "Комментарий" + for score in project_scores + if score.criteria.name != "Комментарий" } project_scores_dict.update(score_info_with_out_comment) - comment = next((score for score in project_scores if score.criteria.name == "Комментарий"), None) + comment = next( + ( + score + for score in project_scores + if score.criteria.name == "Комментарий" + ), + None, + ) if comment is not None: project_scores_dict["Комментарий"] = comment.value return project_scores_dict except Exception as e: - logger.error(f"Prepare export rates data about project_scores error: {str(e)}", exc_info=True) + logger.error( + f"Prepare export rates data about project_scores error: {str(e)}", + exc_info=True, + ) return { criteria.name: "ОШИБКА" - for criteria in Criteria.objects.filter(partner_program__id=self._program_id) + for criteria in Criteria.objects.filter( + partner_program__id=self._program_id + ) } diff --git a/poetry.lock b/poetry.lock index 87c2f2aa..c29bfe3d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1284,19 +1284,96 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "flake8" -version = "5.0.4" +version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8.1" files = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "flake8-broken-line" +version = "1.0.0" +description = "Flake8 plugin to forbid backslashes for line breaks" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "flake8_broken_line-1.0.0-py3-none-any.whl", hash = "sha256:96c964336024a5030dc536a9f6fb02aa679e2d2a6b35b80a558b5136c35832a9"}, + {file = "flake8_broken_line-1.0.0.tar.gz", hash = "sha256:e2c6a17f8d9a129e99c1320fce89b33843e2963871025c4c2bb7b8b8d8732a85"}, +] + +[package.dependencies] +flake8 = ">5" + +[[package]] +name = "flake8-bugbear" +version = "24.8.19" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8_bugbear-24.8.19-py3-none-any.whl", hash = "sha256:25bc3867f7338ee3b3e0916bf8b8a0b743f53a9a5175782ddc4325ed4f386b89"}, + {file = "flake8_bugbear-24.8.19.tar.gz", hash = "sha256:9b77627eceda28c51c27af94560a72b5b2c97c016651bdce45d8f56c180d2d32"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=6.0.0" + +[package.extras] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] + +[[package]] +name = "flake8-builtins" +version = "2.5.0" +description = "Check for python builtins being used as variables or parameters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "flake8_builtins-2.5.0-py3-none-any.whl", hash = "sha256:8cac7c52c6f0708c0902b46b385bc7e368a9068965083796f1431c0d2e6550cf"}, + {file = "flake8_builtins-2.5.0.tar.gz", hash = "sha256:bdaa3dd823e4f5308c5e712d19fa5f69daa52781ea874f5ea9c3637bcf56faa6"}, +] + +[package.dependencies] +flake8 = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "flake8-comprehensions" +version = "3.15.0" +description = "A flake8 plugin to help you write better list/set/dict comprehensions." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flake8_comprehensions-3.15.0-py3-none-any.whl", hash = "sha256:b7e027bbb52be2ceb779ee12484cdeef52b0ad3c1fcb8846292bdb86d3034681"}, + {file = "flake8_comprehensions-3.15.0.tar.gz", hash = "sha256:923c22603e0310376a6b55b03efebdc09753c69f2d977755cba8bb73458a5d4d"}, +] + +[package.dependencies] +flake8 = ">=3,<3.2 || >3.2" + +[[package]] +name = "flake8-mutable" +version = "1.2.0" +description = "mutable defaults flake8 extension" +optional = false +python-versions = "*" +files = [ + {file = "flake8-mutable-1.2.0.tar.gz", hash = "sha256:ee9b77111b867d845177bbc289d87d541445ffcc6029a0c5c65865b42b18c6a6"}, + {file = "flake8_mutable-1.2.0-py2-none-any.whl", hash = "sha256:38fd9dadcbcda6550a916197bc40ed76908119dabb37fbcca30873666c31d2d5"}, +] + +[package.dependencies] +flake8 = "*" [[package]] name = "flake8-print" @@ -2371,13 +2448,13 @@ pyasn1 = ">=0.4.6,<0.7.0" [[package]] name = "pycodestyle" -version = "2.9.1" +version = "2.11.1" description = "Python style guide checker" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, ] [[package]] @@ -2408,13 +2485,13 @@ test = ["pillow", "pytest", "ruff"] [[package]] name = "pyflakes" -version = "2.5.0" +version = "3.1.0" description = "passive checker of Python programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, ] [[package]] @@ -3317,4 +3394,4 @@ test = ["pytest"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "a52dc21926aeed34ad4fcc5585869e6b1002d2577796a331becb908f569bc94b" +content-hash = "220efc2b962f6abeb8e96999bfbc115df80abf71ed899a68b8e1b2ac47f8ba6b" diff --git a/procollab/settings.py b/procollab/settings.py index f41c2b7a..daa826f5 100644 --- a/procollab/settings.py +++ b/procollab/settings.py @@ -249,8 +249,9 @@ AUTH_PASSWORD_VALIDATORS = [ { - "NAME": "django.contrib.auth.password_validation.\ -UserAttributeSimilarityValidator", + "NAME": ( + "django.contrib.auth.password_validation." "UserAttributeSimilarityValidator" + ), }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", @@ -304,8 +305,9 @@ "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", "USER_ID_FIELD": "id", "USER_ID_CLAIM": "user_id", - "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.\ -default_user_authentication_rule", + "USER_AUTHENTICATION_RULE": ( + "rest_framework_simplejwt.authentication." "default_user_authentication_rule", + ), "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), "TOKEN_TYPE_CLAIM": "token_type", "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", @@ -395,7 +397,6 @@ DATA_UPLOAD_MAX_NUMBER_FIELDS = None # for mailing - CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" CELERY_BROKER_URL = "redis://redis:6379/0" diff --git a/project_rates/typing.py b/project_rates/definitions.py similarity index 78% rename from project_rates/typing.py rename to project_rates/definitions.py index 162e25af..165f879e 100644 --- a/project_rates/typing.py +++ b/project_rates/definitions.py @@ -2,10 +2,10 @@ class CriteriasResponse(TypedDict): - id: int + id: int # noqa A003 VNE003 name: str description: str - type: str + type: str # noqa A003 VNE003 min_value: int | float | None max_value: int | float | None diff --git a/project_rates/models.py b/project_rates/models.py index f2743779..878b2437 100644 --- a/project_rates/models.py +++ b/project_rates/models.py @@ -25,7 +25,9 @@ class Criteria(models.Model): name = models.CharField(verbose_name="Название", max_length=50) description = models.TextField(verbose_name="Описание", null=True, blank=True) - type = models.CharField(verbose_name="Тип", max_length=8, choices=VERBOSE_TYPES) + type = models.CharField( # noqa A003 VNE003 + verbose_name="Тип", max_length=8, choices=VERBOSE_TYPES + ) min_value = models.FloatField( verbose_name="Минимально допустимое числовое значение", diff --git a/project_rates/serializers.py b/project_rates/serializers.py index 361a8587..5b3fca53 100644 --- a/project_rates/serializers.py +++ b/project_rates/serializers.py @@ -3,7 +3,7 @@ from core.services import get_views_count from projects.models import Project from .models import Criteria, ProjectScore -from .typing import CriteriasResponse, ProjectScoresResponse +from .definitions import CriteriasResponse, ProjectScoresResponse from .validators import ProjectScoreValidator diff --git a/projects/helpers.py b/projects/helpers.py index 36e7a50b..e8a5feb1 100644 --- a/projects/helpers.py +++ b/projects/helpers.py @@ -110,11 +110,12 @@ def update_partner_program( clear_project_existing_from_profile(user, instance) else: partner_program = PartnerProgram.objects.get(pk=program_id) - existing_program_id: int | None = clear_project_existing_from_profile(user, instance) + existing_program_id: int | None = clear_project_existing_from_profile( + user, instance + ) - if ( - partner_program.datetime_finished < timezone.now() - and (existing_program_id != program_id) + if partner_program.datetime_finished < timezone.now() and ( + existing_program_id != program_id ): raise ValidationError({"error": "Cannot select a completed program."}) @@ -129,8 +130,7 @@ def update_partner_program( def clear_project_existing_from_profile(user, instance) -> None | int: """Remove project from `PartnerProgramUserProfile` instance.""" existing_program_profile = ( - PartnerProgramUserProfile.objects - .select_related("partner_program") + PartnerProgramUserProfile.objects.select_related("partner_program") .filter(user=user, project=instance) .first() ) diff --git a/projects/permissions.py b/projects/permissions.py index 4a18a68a..308f614f 100644 --- a/projects/permissions.py +++ b/projects/permissions.py @@ -79,6 +79,7 @@ class TimingAfterEndsProgramPermission(BasePermission): for `_SECONDS_AFTER_CANT_EDIT` seconds -> days from the end of the program. If the project is not in program or the request in `SAFE_METHODS` -> allowed. """ + _SECONDS_AFTER_CANT_EDIT: int = 60 * 60 * 24 * 30 # Now 30 days. def has_object_permission(self, request, view, obj) -> bool: @@ -86,22 +87,29 @@ def has_object_permission(self, request, view, obj) -> bool: return True program_profile = ( - PartnerProgramUserProfile.objects - .filter(user=request.user, project=obj) + PartnerProgramUserProfile.objects.filter(user=request.user, project=obj) .select_related("partner_program") .first() ) moscow_time: datetime = timezone.localtime(timezone.now()) if program_profile: - date_from_end_program: timedelta = (moscow_time - program_profile.partner_program.datetime_finished) + date_from_end_program: timedelta = ( + moscow_time - program_profile.partner_program.datetime_finished + ) days_from_end_program: int = date_from_end_program.days seconds_from_end_program: int = date_from_end_program.total_seconds() if 0 <= seconds_from_end_program <= self._SECONDS_AFTER_CANT_EDIT: - raise PermissionDenied(detail=self._prepare_exception_detail(days_from_end_program, program_profile)) + raise PermissionDenied( + detail=self._prepare_exception_detail( + days_from_end_program, program_profile + ) + ) return True - def _prepare_exception_detail(self, days_from_end_program: int, program_profile: PartnerProgramUserProfile): + def _prepare_exception_detail( + self, days_from_end_program: int, program_profile: PartnerProgramUserProfile + ): """ Prepare response body when `PermissionDenied` exception raised: program_name: str -> Program title @@ -112,7 +120,9 @@ def _prepare_exception_detail(self, days_from_end_program: int, program_profile: when_can_edit: datetime = timezone.localtime( datetime_finished + timedelta(seconds=self._SECONDS_AFTER_CANT_EDIT) ) - days_until_resolution: int = int(self._SECONDS_AFTER_CANT_EDIT / 60 / 60 / 24) - days_from_end_program - 1 + days_until_resolution: int = ( + int(self._SECONDS_AFTER_CANT_EDIT / 60 / 60 / 24) - days_from_end_program - 1 + ) return { "program_name": program_profile.partner_program.name, "when_can_edit": when_can_edit, diff --git a/projects/serializers.py b/projects/serializers.py index a68cadb4..5f8db5ae 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -176,7 +176,6 @@ def validate(self, data): class ProjectIndustrySerializer(serializers.ModelSerializer): - id = serializers.IntegerField() name = serializers.CharField(read_only=True) class Meta: diff --git a/projects/views.py b/projects/views.py index c7e9a8c8..a4e396c4 100644 --- a/projects/views.py +++ b/projects/views.py @@ -130,7 +130,10 @@ def post(self, request, *args, **kwargs): class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Project.objects.get_projects_for_detail_view() - permission_classes = [HasInvolvementInProjectOrReadOnly, TimingAfterEndsProgramPermission] + permission_classes = [ + HasInvolvementInProjectOrReadOnly, + TimingAfterEndsProgramPermission, + ] serializer_class = ProjectDetailSerializer def retrieve(self, request, *args, **kwargs): @@ -308,7 +311,7 @@ def _collabs_queryset(project_id: int, requested_id: int, leader_id: int) -> Que class ProjectSteps(APIView): permission_classes = [IsStaffOrReadOnly] - def get(self, request, format=None): + def get(self, request): """ Return a tuple of project steps. """ diff --git a/pyproject.toml b/pyproject.toml index a7185ae6..0973a893 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ packages = [] [tool.poetry.dependencies] python = "^3.11" djangorestframework = "^3.14.0" -flake8 = "^5.0.4" +flake8 = "^6.1.0" python-decouple = "^3.6" psycopg2-binary = "^2.9.4" django-cleanup = "^6.0.0" @@ -71,6 +71,11 @@ django-celery-beat = "^2.6.0" weasyprint = "^62.3" django-anymail = "^12.0" phonenumbers = "^8.13.47" +flake8-bugbear = "^24.8.19" +flake8-builtins = "^2.5.0" +flake8-comprehensions = "^3.15.0" +flake8-broken-line = "^1.0.0" +flake8-mutable = "^1.2.0" [build-system] diff --git a/users/constants.py b/users/constants.py index 4ba1bb46..38e98f8e 100644 --- a/users/constants.py +++ b/users/constants.py @@ -71,7 +71,9 @@ def choices(cls): return [(item.value, item.value) for item in cls] -USER_EXPERIENCE_YEAR_VALIDATION_MESSAGE: str = "Год начала не может быть больше года завершения" +USER_EXPERIENCE_YEAR_VALIDATION_MESSAGE: str = ( + "Год начала не может быть больше года завершения" +) # Working with user skills: USER_MAX_SKILL_QUANTITY: int = 20 diff --git a/users/typing.py b/users/definitions.py similarity index 100% rename from users/typing.py rename to users/definitions.py diff --git a/users/models.py b/users/models.py index b21af20a..c375a336 100644 --- a/users/models.py +++ b/users/models.py @@ -102,7 +102,7 @@ class CustomUser(AbstractUser): null=True, blank=True, verbose_name="Номер телефона", - help_text="Пример: +7 XXX XX-XX-XX | +7XXXXXXXXX | +7 (XXX) XX-XX-XX" + help_text="Пример: +7 XXX XX-XX-XX | +7XXXXXXXXX | +7 (XXX) XX-XX-XX", ) v2_speciality = models.ForeignKey( on_delete=models.SET_NULL, @@ -138,7 +138,7 @@ class CustomUser(AbstractUser): blank=True, default=False, verbose_name="Временная мера для переноса навыка", - help_text="Yes если оба поля `v2_speciality` и `skills` есть, No если поля не перенеслись" + help_text="Yes если оба поля `v2_speciality` и `skills` есть, No если поля не перенеслись", ) USERNAME_FIELD = "email" @@ -442,6 +442,7 @@ class Meta(TypedModelMeta): class AbstractUserExperience(models.Model): """Abstact help model for user work|education experience.""" + organization_name = models.CharField( max_length=255, verbose_name="Наименование организации", @@ -469,9 +470,7 @@ class Meta: abstract = True def __str__(self) -> str: - return ( - f"id: {self.id} - ({self.user.first_name} {self.user.last_name} user_id: {self.user.id})" - ) + return f"id: {self.id} - ({self.user.first_name} {self.user.last_name} user_id: {self.user.id})" def clean(self) -> None: """Validate both years `entry` <`completion`""" @@ -541,6 +540,7 @@ class UserWorkExperience(AbstractUserExperience): entry_year: PositiveSmallIntegerField Year of admission. completion_year: PositiveSmallIntegerField Year of dismissal. """ + user = models.ForeignKey( to=CustomUser, on_delete=models.CASCADE, @@ -570,6 +570,7 @@ class UserLanguages(models.Model): language: CharField(choise) languages. language_level: CharField(choise) language level. """ + user = models.ForeignKey( to=CustomUser, on_delete=models.CASCADE, @@ -604,7 +605,9 @@ def clean(self) -> None: """ super().clean() user_languages = self.user.user_languages.values_list("language", flat=True) - if (self.language not in user_languages) and len(user_languages) == constants.USER_MAX_LANGUAGES_COUNT: + if (self.language not in user_languages) and len( + user_languages + ) == constants.USER_MAX_LANGUAGES_COUNT: raise ValidationError(constants.COUNT_LANGUAGES_VALIDATION_MESSAGE) def save(self, *args, **kwargs): @@ -612,9 +615,7 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) def __str__(self) -> str: - return ( - f"id: {self.id} - ({self.user.first_name} {self.user.last_name} user_id: {self.user.id})" - ) + return f"id: {self.id} - ({self.user.first_name} {self.user.last_name} user_id: {self.user.id})" class UserSkillConfirmation(models.Model): @@ -626,15 +627,12 @@ class UserSkillConfirmation(models.Model): confirmed_by: FK CustomUser. confirmed_at: DateTimeField. """ + skill_to_object = models.ForeignKey( - "core.SkillToObject", - on_delete=models.CASCADE, - related_name="confirmations" + "core.SkillToObject", on_delete=models.CASCADE, related_name="confirmations" ) confirmed_by = models.ForeignKey( - CustomUser, - on_delete=models.CASCADE, - related_name="skill_confirmations" + CustomUser, on_delete=models.CASCADE, related_name="skill_confirmations" ) confirmed_at = models.DateTimeField(auto_now_add=True) @@ -642,7 +640,7 @@ class Meta: constraints = [ models.UniqueConstraint( fields=["skill_to_object", "confirmed_by"], - name="unique_skill_confirmed_by" + name="unique_skill_confirmed_by", ) ] verbose_name = "Подтверждение навыка" diff --git a/users/schema.py b/users/schema.py index b151a044..91505f84 100644 --- a/users/schema.py +++ b/users/schema.py @@ -5,12 +5,12 @@ "user_pk", openapi.IN_PATH, description="Id user to confirmed", - type=openapi.TYPE_INTEGER + type=openapi.TYPE_INTEGER, ) SKILL_PK_PARAM = openapi.Parameter( "skill_pk", openapi.IN_PATH, description="Id skill user to confirmed", - type=openapi.TYPE_INTEGER + type=openapi.TYPE_INTEGER, ) diff --git a/users/serializers.py b/users/serializers.py index 52ab8b07..49eb088b 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -107,6 +107,7 @@ class Meta: class UserDataConfirmationSerializer(serializers.ModelSerializer): """Information about the User to add to the skill confirmation information.""" + v2_speciality = SpecializationSerializer() class Meta: @@ -154,6 +155,7 @@ def to_representation(self, instance): class UserApproveSkillResponse(serializers.Serializer): """For swagger response presentation.""" + confirmed_by = UserDataConfirmationSerializer(read_only=True) @@ -173,14 +175,14 @@ class Meta: def get_approves(self, obj): """Adds information about confirm to the skill.""" - confirmations = ( - UserSkillConfirmation.objects - .filter(skill_to_object=obj) - .select_related('confirmed_by') - ) + confirmations = UserSkillConfirmation.objects.filter( + skill_to_object=obj + ).select_related("confirmed_by") return [ { - "confirmed_by": UserDataConfirmationSerializer(confirmation.confirmed_by).data, + "confirmed_by": UserDataConfirmationSerializer( + confirmation.confirmed_by + ).data, } for confirmation in confirmations ] @@ -277,7 +279,7 @@ class Meta: class SubscriptionSerializer(serializers.Serializer): - id = serializers.IntegerField() + id = serializers.IntegerField() # noqa A003 VNE003 name = serializers.CharField() price = serializers.IntegerField() features_list = serializers.ListField(child=serializers.CharField()) @@ -307,7 +309,6 @@ def validate(self, attrs): class UserEducationSerializer(UserExperienceMixin, serializers.ModelSerializer): - class Meta: model = UserEducation fields = [ @@ -321,7 +322,6 @@ class Meta: class UserWorkExperienceSerializer(UserExperienceMixin, serializers.ModelSerializer): - class Meta: model = UserWorkExperience fields = [ @@ -334,7 +334,6 @@ class Meta: class UserLanguagesSerializer(serializers.ModelSerializer): - class Meta: model = UserLanguages fields = [ @@ -391,11 +390,9 @@ def get_projects(self, user: CustomUser): ).data def get_programs(self, user: CustomUser): - user_program_profiles = ( - user.partner_program_profiles - .select_related('partner_program') - .filter(partner_program__draft=False) - ) + user_program_profiles = user.partner_program_profiles.select_related( + "partner_program" + ).filter(partner_program__draft=False) return UserProgramsSerializer( [profile.partner_program for profile in user_program_profiles], context={"request": self.context.get("request"), "user": user}, @@ -556,13 +553,17 @@ def _update_user_education(self, instance: CustomUser, data: list[dict]) -> None serializer.save(user=instance) @transaction.atomic - def _update_user_work_experience(self, instance: CustomUser, data: list[dict]) -> None: + def _update_user_work_experience( + self, instance: CustomUser, data: list[dict] + ) -> None: """ Update user work experience. `PUT`/ `PATCH` methods require full data about education. """ instance.work_experience.all().delete() - serializer = UserWorkExperienceSerializer(data=data, many=True, context=self.context) + serializer = UserWorkExperienceSerializer( + data=data, many=True, context=self.context + ) if serializer.is_valid(raise_exception=True): serializer.save(user=instance) diff --git a/users/services/cv_data_prepare.py b/users/services/cv_data_prepare.py index 0dddd257..0c765701 100644 --- a/users/services/cv_data_prepare.py +++ b/users/services/cv_data_prepare.py @@ -106,7 +106,9 @@ def _get_user_age(self, user_instance: CustomUser) -> str | None: def _get_encoded_image_from_local_path(self, path_to_image: str) -> str: """Local files also need to be converted to base64 for rendering.""" os_image_path: str = self._get_os_file_path(path_to_image) - encoded_image = Base64ImageEncoder().get_encoded_base64_from_local_path(os_image_path) + encoded_image = Base64ImageEncoder().get_encoded_base64_from_local_path( + os_image_path + ) return encoded_image def _get_encoded_user_avatar(self, user_instance: CustomUser) -> str: @@ -115,19 +117,33 @@ def _get_encoded_user_avatar(self, user_instance: CustomUser) -> str: If user dont have avatar, take local `avatar-placeholder`. """ if user_instance.avatar: - encoded_base64_avatar: str = Base64ImageEncoder().get_encoded_base64_from_url(user_instance.avatar) + encoded_base64_avatar: str = Base64ImageEncoder().get_encoded_base64_from_url( + user_instance.avatar + ) else: - user_avatar_placeholder_path: str = self._get_os_file_path(self.__EMPTY_USER_AVATAR_FILENAME) - encoded_base64_avatar: str = self._get_encoded_image_from_local_path(user_avatar_placeholder_path) + user_avatar_placeholder_path: str = self._get_os_file_path( + self.__EMPTY_USER_AVATAR_FILENAME + ) + encoded_base64_avatar: str = self._get_encoded_image_from_local_path( + user_avatar_placeholder_path + ) return encoded_base64_avatar def _get_fonts_absolute_paths_for_render(self) -> dict[str, str]: """Prepare absolute paths for fonts (needs for correct render).""" fonts: dict[str, str] = { - "font_semibold_path": self._get_os_file_path(self.__FONT_SEMIBOLD_FILENAME).replace("\\", "/"), - "font_heavy_path": self._get_os_file_path(self.__FONT_HEAVY_FILENAME).replace("\\", "/"), - "font_extralight_path": self._get_os_file_path(self.__FONT_EXTRALIGHT_FILENAME).replace("\\", "/"), - "font_regular_path": self._get_os_file_path(self.__FONT_REGULAR_FILENAME).replace("\\", "/"), + "font_semibold_path": self._get_os_file_path( + self.__FONT_SEMIBOLD_FILENAME + ).replace("\\", "/"), + "font_heavy_path": self._get_os_file_path(self.__FONT_HEAVY_FILENAME).replace( + "\\", "/" + ), + "font_extralight_path": self._get_os_file_path( + self.__FONT_EXTRALIGHT_FILENAME + ).replace("\\", "/"), + "font_regular_path": self._get_os_file_path( + self.__FONT_REGULAR_FILENAME + ).replace("\\", "/"), } return fonts diff --git a/users/signals.py b/users/signals.py index 73b42547..aed77697 100644 --- a/users/signals.py +++ b/users/signals.py @@ -32,7 +32,9 @@ def update_dataset_migration_applied(sender, instance, **kwargs): """Update the `dataset_migration_applied` attribute based on the presence of `v2_speciality` and `skills`.""" def update_migration(): - dataset_migration_applied = bool(instance.v2_speciality and instance.skills.exists()) + dataset_migration_applied = bool( + instance.v2_speciality and instance.skills.exists() + ) if instance.dataset_migration_applied != dataset_migration_applied: CustomUser.objects.filter(pk=instance.pk).update( dataset_migration_applied=dataset_migration_applied diff --git a/users/urls.py b/users/urls.py index 28e5ea89..e12e279d 100644 --- a/users/urls.py +++ b/users/urls.py @@ -54,7 +54,10 @@ path("users//news//", NewsDetail.as_view()), path("users//news//set_viewed/", NewsDetailSetViewed.as_view()), path("users//news//set_liked/", NewsDetailSetLiked.as_view()), - path("users//approve_skill//", UserSkillsApproveDeclineView.as_view()), + path( + "users//approve_skill//", + UserSkillsApproveDeclineView.as_view(), + ), path("users/current/", CurrentUser.as_view()), # todo: change password view path("users/current/programs/", CurrentUserPrograms.as_view()), diff --git a/users/utils.py b/users/utils.py index 230a288a..5352a7fd 100644 --- a/users/utils.py +++ b/users/utils.py @@ -31,7 +31,9 @@ def normalize_user_phone(phone_num: str): try: phone_number = phonenumbers.parse(phone_num, None) if phonenumbers.is_valid_number(phone_number): - return phonenumbers.format_number(phone_number, phonenumbers.PhoneNumberFormat.INTERNATIONAL) + return phonenumbers.format_number( + phone_number, phonenumbers.PhoneNumberFormat.INTERNATIONAL + ) raise ValidationError(NOT_VALID_NUMBER_MESSAGE) except phonenumbers.phonenumberutil.NumberParseException: raise ValidationError(NOT_VALID_NUMBER_MESSAGE) diff --git a/users/validators.py b/users/validators.py index 41929397..e93c04fb 100644 --- a/users/validators.py +++ b/users/validators.py @@ -49,7 +49,9 @@ def user_experience_years_range_validator(value: int): (2000 - `now.year`) """ if value not in range(2000, timezone.now().year + 1): - raise DjangoValidationError(f"Год должен быть в диапазоне 2000 - {timezone.now().year}") + raise DjangoValidationError( + f"Год должен быть в диапазоне 2000 - {timezone.now().year}" + ) def user_phone_number_validation(value: str): diff --git a/vacancy/admin.py b/vacancy/admin.py index de42048d..5b42b67b 100644 --- a/vacancy/admin.py +++ b/vacancy/admin.py @@ -24,7 +24,7 @@ class VacancyAdmin(admin.ModelAdmin): inlines = [ VacancySkillToObjectInline, ] - readonly_fields = ('datetime_closed',) + readonly_fields = ("datetime_closed",) list_display_links = ["role"] change_list_template = "vacancies/vacancies_change_list.html" diff --git a/vacancy/serializers.py b/vacancy/serializers.py index 56648c7b..f4efbd0b 100644 --- a/vacancy/serializers.py +++ b/vacancy/serializers.py @@ -192,7 +192,6 @@ class ProjectVacancyCreateListSerializer( AbstractVacancyEnumFields, RequiredSkillsWriteSerializerMixin[Vacancy], ): - def create(self, validated_data): project = validated_data["project"] if project.leader != self.context["request"].user: diff --git a/vacancy/tasks.py b/vacancy/tasks.py index 384a71f7..7e5c2daf 100644 --- a/vacancy/tasks.py +++ b/vacancy/tasks.py @@ -1,6 +1,6 @@ import datetime -from mailing.typing import EmailDataToPrepare, ContextDataDict, MailDataDict +from mailing.definitions import EmailDataToPrepare, ContextDataDict, MailDataDict from mailing.utils import send_mass_mail, prepare_mail_data from procollab.celery import app from vacancy.mapping import (