From 5fb3cb87934c513589014f743588ddd875a3c602 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 12:56:08 +0200 Subject: [PATCH 01/16] Update requirements --- backend/requirements-dev.txt | 146 +++++++++++----------- backend/requirements.txt | 10 +- docker/dockerfiles/Dockerfile.backend | 4 +- docker/dockerfiles/Dockerfile.backend.dev | 4 +- 4 files changed, 81 insertions(+), 83 deletions(-) diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index c793fbd6..8fb09065 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --output-file=requirements-dev.txt --strip-extras requirements-dev.in # asgiref==3.8.1 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django asttokens==2.4.1 # via stack-data @@ -15,58 +15,58 @@ attrs==24.2.0 black==24.10.0 # via -r requirements-dev.in blessed==1.20.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt -boto3==1.35.60 + # via -r /var/www/votong/backend/requirements.txt +boto3==1.35.68 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-ses # pycognito -botocore==1.35.60 +botocore==1.35.68 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # boto3 # s3transfer brotlipy==0.7.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt build==1.2.2.post1 # via pip-tools certifi==2024.8.30 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # requests # sentry-sdk cffi==1.17.1 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # brotlipy # cryptography charset-normalizer==3.4.0 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # requests click==8.1.7 # via # black # pip-tools -coverage==7.6.4 +coverage==7.6.7 # via pytest-cov croniter==3.0.4 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt cryptography==43.0.3 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # pyjwt -debugpy==1.8.8 +debugpy==1.8.9 # via -r requirements-dev.in decorator==5.1.1 # via ipython diff-match-patch==20241021 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-import-export django==4.2.16 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-admin-autocomplete-filter # django-allauth # django-appconf @@ -84,60 +84,60 @@ django==4.2.16 # django-storages # django-tinymce django-admin-autocomplete-filter==0.7.1 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-admin-rangefilter==0.13.2 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-allauth==65.0.2 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-appconf==1.0.6 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-avatar django-auditlog==3.0.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-avatar==8.0.1 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-crispy-forms==2.3 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-debug-toolbar==4.4.6 # via -r requirements-dev.in django-environ==0.11.2 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-extensions==3.2.3 # via -r requirements-dev.in django-file-resubmit==0.5.2 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-guardian==2.4.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-impersonate==1.9.4 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-import-export==4.1.1 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-model-utils==5.0.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-picklefield==3.2 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-q2 django-q2==1.7.4 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-recaptcha==4.0.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-ses==4.2.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-spurl==0.6.8 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-storages==1.14.4 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt django-tinymce==4.1.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt dnspython==2.7.0 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-avatar envs==1.4 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # pycognito execnet==2.1.1 # via pytest-xdist @@ -148,16 +148,16 @@ faker==30.6.0 fancycompleter==0.9.1 # via pdbpp gevent==24.10.3 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt greenlet==3.1.1 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # gevent gunicorn==23.0.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt idna==3.10 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # requests iniconfig==2.0.0 # via pytest @@ -167,7 +167,7 @@ jedi==0.19.2 # via ipython jmespath==1.0.1 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # boto3 # botocore markupsafe==3.0.2 @@ -178,11 +178,11 @@ mypy-extensions==1.0.0 # via black oauthlib==3.2.2 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # requests-oauthlib packaging==24.2 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # black # build # gunicorn @@ -197,7 +197,7 @@ pexpect==4.9.0 # via ipython pillow==10.4.0 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-avatar pip-tools==7.4.1 # via -r requirements-dev.in @@ -208,26 +208,26 @@ pluggy==1.5.0 prompt-toolkit==3.0.48 # via ipython psutil==6.1.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt psycopg2-binary==2.9.10 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data pycognito==2024.5.1 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt pycparser==2.22 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # cffi pygments==2.18.0 # via # ipython # pdbpp -pyjwt==2.9.0 +pyjwt==2.10.0 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # pycognito pyproject-hooks==1.2.0 # via @@ -249,86 +249,84 @@ pytest-xdist==3.6.1 # via -r requirements-dev.in python-dateutil==2.9.0.post0 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # botocore # croniter # django-auditlog # faker pytz==2024.2 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # croniter requests==2.32.3 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # pycognito # requests-oauthlib requests-oauthlib==2.0.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt -ruff==0.7.3 + # via -r /var/www/votong/backend/requirements.txt +ruff==0.7.4 # via -r requirements-dev.in -s3transfer==0.10.3 +s3transfer==0.10.4 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # boto3 sentry-sdk==2.17.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt six==1.16.0 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # asttokens # blessed # django-spurl # python-dateutil sqlparse==0.5.2 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django # django-debug-toolbar stack-data==0.6.3 # via ipython tablib==3.5.0 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-import-export traitlets==5.14.3 # via # ipython # matplotlib-inline typing-extensions==4.12.2 - # via - # faker - # ipython + # via faker urllib3==2.2.3 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # botocore # requests # sentry-sdk urlobject==2.4.3 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # django-spurl wcwidth==0.2.13 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # blessed # prompt-toolkit werkzeug==3.0.6 # via -r requirements-dev.in -wheel==0.45.0 +wheel==0.45.1 # via pip-tools whitenoise==6.7.0 - # via -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # via -r /var/www/votong/backend/requirements.txt wmctrl==0.5 # via pdbpp zope-event==5.0 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # gevent zope-interface==7.1.1 # via - # -r /Users/tudoramariei/Work/c4ro/ngohub/votong/backend/requirements.txt + # -r /var/www/votong/backend/requirements.txt # gevent # The following packages are considered to be unsafe in a requirements file: diff --git a/backend/requirements.txt b/backend/requirements.txt index 2f97a52e..2ff87033 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --output-file=requirements.txt --strip-extras requirements.in @@ -8,12 +8,12 @@ asgiref==3.8.1 # via django blessed==1.20.0 # via -r requirements.in -boto3==1.35.60 +boto3==1.35.68 # via # -r requirements.in # django-ses # pycognito -botocore==1.35.60 +botocore==1.35.68 # via # boto3 # s3transfer @@ -125,7 +125,7 @@ pycognito==2024.5.1 # via -r requirements.in pycparser==2.22 # via cffi -pyjwt==2.9.0 +pyjwt==2.10.0 # via # django-allauth # pycognito @@ -144,7 +144,7 @@ requests==2.32.3 # requests-oauthlib requests-oauthlib==2.0.0 # via django-allauth -s3transfer==0.10.3 +s3transfer==0.10.4 # via boto3 sentry-sdk==2.17.0 # via -r requirements.in diff --git a/docker/dockerfiles/Dockerfile.backend b/docker/dockerfiles/Dockerfile.backend index 35acd6c8..3c8d949c 100644 --- a/docker/dockerfiles/Dockerfile.backend +++ b/docker/dockerfiles/Dockerfile.backend @@ -1,4 +1,4 @@ -ARG PYTHON_VERSION=3.12.4 +ARG PYTHON_VERSION=3.12.7 FROM python:${PYTHON_VERSION}-slim-bookworm as build @@ -47,7 +47,7 @@ RUN apt-get update && \ nginx gcc xz-utils gettext build-essential postgresql-client libpq-dev -ARG S6_OVERLAY_VERSION=3.2.0.0 +ARG S6_OVERLAY_VERSION=3.2.0.2 ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME 0 ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp diff --git a/docker/dockerfiles/Dockerfile.backend.dev b/docker/dockerfiles/Dockerfile.backend.dev index 05e62077..19ff9c08 100644 --- a/docker/dockerfiles/Dockerfile.backend.dev +++ b/docker/dockerfiles/Dockerfile.backend.dev @@ -1,4 +1,4 @@ -ARG PYTHON_VERSION=3.12.4 +ARG PYTHON_VERSION=3.12.7 FROM python:${PYTHON_VERSION}-slim-bookworm @@ -28,7 +28,7 @@ RUN apt-get update && \ apt-get clean -ARG S6_OVERLAY_VERSION=3.2.0.0 +ARG S6_OVERLAY_VERSION=3.2.0.2 ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME 0 ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp From 1cd6a409f73e63346ff5b05f0cb9446f35f4c409 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 13:11:31 +0200 Subject: [PATCH 02/16] Fix typo --- backend/hub/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/hub/admin.py b/backend/hub/admin.py index b20346e1..d66b2883 100644 --- a/backend/hub/admin.py +++ b/backend/hub/admin.py @@ -218,7 +218,7 @@ def _set_candidates_status( committee_emails = Group.objects.get(name=COMMITTEE_GROUP).user_set.all().values_list("email", flat=True) for candidate in queryset: - # only take action if there is a chance in the status + # only take action if there is a change in the status if candidate.status != status: CandidateConfirmation.objects.filter(candidate=candidate).delete() From 62bb30e07a23efdebbb02403248ee10f23429d52 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 13:31:36 +0200 Subject: [PATCH 03/16] Fix the HttpRequest type hint --- backend/hub/admin.py | 10 +++++----- backend/hub/context_processors.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/hub/admin.py b/backend/hub/admin.py index d66b2883..21589f90 100644 --- a/backend/hub/admin.py +++ b/backend/hub/admin.py @@ -8,8 +8,8 @@ from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.auth.models import Group from django.contrib.sites.shortcuts import get_current_site -from django.core.handlers.wsgi import WSGIRequest from django.db.models import Count, QuerySet +from django.http import HttpRequest from django.shortcuts import redirect, render from django.urls import path, reverse from django.utils.http import urlencode @@ -210,7 +210,7 @@ def send_confirm_email_to_committee(request, candidate, to_email): def _set_candidates_status( - request: WSGIRequest, + request: HttpRequest, queryset: QuerySet[Candidate], status: str, send_committee_confirmation: bool = True, @@ -231,21 +231,21 @@ def _set_candidates_status( queryset.update(status=status) -def reject_candidates(_, request: WSGIRequest, queryset: QuerySet[Candidate]): +def reject_candidates(_, request: HttpRequest, queryset: QuerySet[Candidate]): _set_candidates_status(request, queryset, Candidate.STATUS.rejected) reject_candidates.short_description = _("Set selected candidates status to REJECTED") -def accept_candidates(_, request: WSGIRequest, queryset: QuerySet[Candidate]): +def accept_candidates(_, request: HttpRequest, queryset: QuerySet[Candidate]): _set_candidates_status(request, queryset, Candidate.STATUS.accepted) accept_candidates.short_description = _("Set selected candidates status to ACCEPTED") -def pending_candidates(_, request: WSGIRequest, queryset: QuerySet[Candidate]): +def pending_candidates(_, request: HttpRequest, queryset: QuerySet[Candidate]): _set_candidates_status(request, queryset, Candidate.STATUS.pending, send_committee_confirmation=False) diff --git a/backend/hub/context_processors.py b/backend/hub/context_processors.py index a8d7ec69..cf536627 100644 --- a/backend/hub/context_processors.py +++ b/backend/hub/context_processors.py @@ -1,13 +1,13 @@ from typing import Any, Dict from django.conf import settings -from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpRequest from django.urls import reverse from hub.models import FLAG_CHOICES, FeatureFlag, SETTINGS_CHOICES -def hub_settings(_: WSGIRequest) -> Dict[str, Any]: +def hub_settings(_: HttpRequest) -> Dict[str, Any]: flags = {k: v for k, v in FeatureFlag.objects.all().values_list("flag", "is_enabled")} register_url = settings.NGOHUB_APP_BASE From 1961c876c272c8a13e942b0e0916d30d8acf914a Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 14:23:41 +0200 Subject: [PATCH 04/16] Add a separate Admin section for the electoral commision users --- backend/accounts/admin.py | 37 ++++++++++++++++++++++++- backend/accounts/models.py | 7 +++++ backend/locale/en/LC_MESSAGES/django.po | 26 ++++++++++++----- backend/locale/ro/LC_MESSAGES/django.po | 26 ++++++++++++----- 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/backend/accounts/admin.py b/backend/accounts/admin.py index d7b4a5bd..09d0817e 100644 --- a/backend/accounts/admin.py +++ b/backend/accounts/admin.py @@ -2,6 +2,8 @@ from django.contrib.admin.sites import NotRegistered from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin from django.contrib.auth.models import Group as BaseGroup +from django.db.models import QuerySet +from django.http import HttpRequest from django.urls import reverse_lazy from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -9,7 +11,16 @@ from civil_society_vote.common.admin import BasePermissionsAdmin -from .models import GroupProxy, User +from .models import ( + GroupProxy, + User, + CommissionUser, + COMMITTEE_GROUP, + COMMITTEE_GROUP_READ_ONLY, + STAFF_GROUP, + SUPPORT_GROUP, +) + # Remove the default admins for User and Group try: @@ -45,6 +56,8 @@ class UserAdmin(UserAdminImpersonateMixin, BasePermissionsAdmin): search_fields = ("email", "first_name", "last_name") + actions = ("request_confirmations_deletion",) + readonly_fields = ( "email", "password", @@ -101,6 +114,10 @@ class UserAdmin(UserAdminImpersonateMixin, BasePermissionsAdmin): ), ) + @admin.action(description=_("Request confirmations deletion")) + def request_confirmations_deletion(self, request: HttpRequest, queryset: QuerySet[User]): + print("TODO request_confirmations_deletion") + def change_view(self, request, object_id, form_url="", extra_context=None): extra_context = extra_context or {} extra_context["user_id"] = object_id @@ -133,5 +150,23 @@ def get_groups(self, obj: User): get_groups.short_description = _("Groups") +@admin.register(CommissionUser) +class ComissionAdmin(UserAdmin): + """ + User admin which only handles electoral commission users + """ + + # TODO: Use only relevant filters + # TODO: Move request_confirmations_deletion to be available only to this admin + + def get_queryset(self, request) -> QuerySet: + return ( + super() + .get_queryset(request) + .filter(groups__name__in=[COMMITTEE_GROUP, COMMITTEE_GROUP_READ_ONLY]) + .exclude(groups__name__in=[STAFF_GROUP, SUPPORT_GROUP]) + ) + + @admin.register(GroupProxy) class GroupAdmin(BaseGroupAdmin, BasePermissionsAdmin): ... diff --git a/backend/accounts/models.py b/backend/accounts/models.py index 1696daaf..b9daaf80 100644 --- a/backend/accounts/models.py +++ b/backend/accounts/models.py @@ -97,5 +97,12 @@ class Meta: verbose_name_plural = _("Groups") +class CommissionUser(User): + class Meta: + proxy = True + verbose_name = _("Commission user") + verbose_name_plural = _("Commission users") + + auditlog.register(User, exclude_fields=["password"]) auditlog.register(GroupProxy) diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 439a57a7..0231454d 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-22 16:58+0200\n" +"POT-Creation-Date: 2024-11-23 14:09+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,27 +17,31 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: accounts/admin.py:63 hub/admin.py:292 hub/admin.py:407 +#: accounts/admin.py:69 hub/admin.py:292 hub/admin.py:407 msgid "Identification" msgstr "" -#: accounts/admin.py:75 +#: accounts/admin.py:81 msgid "Flags" msgstr "" -#: accounts/admin.py:85 +#: accounts/admin.py:91 msgid "Permissions" msgstr "" -#: accounts/admin.py:94 +#: accounts/admin.py:100 msgid "Important dates" msgstr "" -#: accounts/admin.py:120 hub/models.py:440 +#: accounts/admin.py:110 +msgid "Request confirmations deletion" +msgstr "" + +#: accounts/admin.py:130 hub/models.py:440 msgid "Organization" msgstr "" -#: accounts/admin.py:133 accounts/models.py:97 +#: accounts/admin.py:143 accounts/models.py:97 msgid "Groups" msgstr "" @@ -81,6 +85,14 @@ msgstr "" msgid "Group" msgstr "" +#: accounts/models.py:103 +msgid "Commission user" +msgstr "" + +#: accounts/models.py:104 +msgid "Commission users" +msgstr "" + #: accounts/templates/accounts/avatar/change.html:47 msgid "You haven't uploaded an avatar yet. Please upload one now." msgstr "" diff --git a/backend/locale/ro/LC_MESSAGES/django.po b/backend/locale/ro/LC_MESSAGES/django.po index f65156c5..486092a0 100644 --- a/backend/locale/ro/LC_MESSAGES/django.po +++ b/backend/locale/ro/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-22 16:58+0200\n" +"POT-Creation-Date: 2024-11-23 14:09+0200\n" "PO-Revision-Date: 2020-04-23 17:54+0300\n" "Last-Translator: \n" "Language-Team: \n" @@ -19,27 +19,31 @@ msgstr "" "2:1));\n" "X-Generator: Poedit 2.3\n" -#: accounts/admin.py:63 hub/admin.py:292 hub/admin.py:407 +#: accounts/admin.py:69 hub/admin.py:292 hub/admin.py:407 msgid "Identification" msgstr "Identificare" -#: accounts/admin.py:75 +#: accounts/admin.py:81 msgid "Flags" msgstr "Acțiuni" -#: accounts/admin.py:85 +#: accounts/admin.py:91 msgid "Permissions" msgstr "Permisiuni" -#: accounts/admin.py:94 +#: accounts/admin.py:100 msgid "Important dates" msgstr "Date importante" -#: accounts/admin.py:120 hub/models.py:440 +#: accounts/admin.py:110 +msgid "Request confirmations deletion" +msgstr "Inițiază ștergerea confirmărilor" + +#: accounts/admin.py:130 hub/models.py:440 msgid "Organization" msgstr "Organizație" -#: accounts/admin.py:133 accounts/models.py:97 +#: accounts/admin.py:143 accounts/models.py:97 msgid "Groups" msgstr "Grupuri" @@ -83,6 +87,14 @@ msgstr "Utilizatori" msgid "Group" msgstr "Grup" +#: accounts/models.py:103 +msgid "Commission user" +msgstr "Utilizator Comisie Electorală" + +#: accounts/models.py:104 +msgid "Commission users" +msgstr "Utilizatori Comisie Electorală" + #: accounts/templates/accounts/avatar/change.html:47 msgid "You haven't uploaded an avatar yet. Please upload one now." msgstr "Nu ai ales niciun avatar. Te rugăm să îți alegi unul." From 836b6d5eae9613b0291edab416fbbade476213b6 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 15:49:44 +0200 Subject: [PATCH 05/16] Fix candidates listing title --- .../templates/hub/committee/candidates.html | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/hub/templates/hub/committee/candidates.html b/backend/hub/templates/hub/committee/candidates.html index 278e0158..51fac53b 100644 --- a/backend/hub/templates/hub/committee/candidates.html +++ b/backend/hub/templates/hub/committee/candidates.html @@ -17,9 +17,25 @@

{% if filtering == "pending" %} + {% trans 'Pending candidates' %} ({{ counters.candidates_pending }}) - {% else %} + + {% elif filtering == "confirmed" %} + + {% trans 'Validated candidates' %} ({{ counters.candidates_confirmed }}) + + {% elif filtering == "rejected" %} + + {% trans 'Rejected candidates' %} ({{ counters.candidates_rejected }}) + + {% elif filtering == "accepted" %} + {% trans 'Accepted candidates' %} ({{ counters.candidates_accepted }}) + + {% else %} + +   + {% endif %}

From ac805f65f78fe3787a3a88d2b2c7cabdc75909fe Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 16:41:27 +0200 Subject: [PATCH 06/16] Define the reset_approve_candidate permission --- .../migrations/0012_commissionuser.py | 29 +++++++++++++++++++ backend/hub/management/commands/init.py | 1 + .../0076_alter_candidate_options.py | 28 ++++++++++++++++++ backend/hub/models.py | 1 + 4 files changed, 59 insertions(+) create mode 100644 backend/accounts/migrations/0012_commissionuser.py create mode 100644 backend/hub/migrations/0076_alter_candidate_options.py diff --git a/backend/accounts/migrations/0012_commissionuser.py b/backend/accounts/migrations/0012_commissionuser.py new file mode 100644 index 00000000..5030621c --- /dev/null +++ b/backend/accounts/migrations/0012_commissionuser.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.16 on 2024-11-23 13:58 + +import django.contrib.auth.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0011_groupproxy_user_created_user_modified_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="CommissionUser", + fields=[], + options={ + "verbose_name": "Utilizator Comisie Electorală", + "verbose_name_plural": "Utilizatori Comisie Electorală", + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("accounts.user",), + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/backend/hub/management/commands/init.py b/backend/hub/management/commands/init.py index ed908391..b112d5a5 100644 --- a/backend/hub/management/commands/init.py +++ b/backend/hub/management/commands/init.py @@ -35,6 +35,7 @@ def _initialize_groups_permissions(self): committee_group: Group = Group.objects.get_or_create(name=COMMITTEE_GROUP)[0] assign_perm("hub.approve_candidate", committee_group) + assign_perm("hub.reset_approve_candidate", committee_group) assign_perm("hub.view_data_candidate", committee_group) committee_group_read_only: Group = Group.objects.get_or_create(name=COMMITTEE_GROUP_READ_ONLY)[0] diff --git a/backend/hub/migrations/0076_alter_candidate_options.py b/backend/hub/migrations/0076_alter_candidate_options.py new file mode 100644 index 00000000..fed9e84f --- /dev/null +++ b/backend/hub/migrations/0076_alter_candidate_options.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.16 on 2024-11-23 13:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("hub", "0075_alter_candidate_statement"), + ] + + operations = [ + migrations.AlterModelOptions( + name="candidate", + options={ + "ordering": ["name"], + "permissions": ( + ("view_data_candidate", "View data candidate"), + ("approve_candidate", "Approve candidate"), + ("reset_approve_candidate", "Reset candidate approval"), + ("support_candidate", "Support candidate"), + ("vote_candidate", "Vote candidate"), + ), + "verbose_name": "Candidate", + "verbose_name_plural": "Candidates", + }, + ), + ] diff --git a/backend/hub/models.py b/backend/hub/models.py index 97cd942d..a5e47458 100644 --- a/backend/hub/models.py +++ b/backend/hub/models.py @@ -830,6 +830,7 @@ class Meta: permissions = ( ("view_data_candidate", "View data candidate"), ("approve_candidate", "Approve candidate"), + ("reset_approve_candidate", "Reset candidate approval"), ("support_candidate", "Support candidate"), ("vote_candidate", "Vote candidate"), ) From 60ae9ce8b73af8469d6b93f0300cb6895e0135c7 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 17:52:21 +0200 Subject: [PATCH 07/16] Emails and endpoint for confirmations reset --- backend/accounts/admin.py | 33 ++++- .../hub/emails/08_delete_confirmations.html | 10 ++ .../hub/emails/08_delete_confirmations.txt | 3 + backend/hub/urls.py | 2 + backend/hub/views.py | 21 ++- backend/locale/en/LC_MESSAGES/django.po | 104 ++++++++------- backend/locale/ro/LC_MESSAGES/django.po | 120 +++++++++--------- 7 files changed, 177 insertions(+), 116 deletions(-) create mode 100644 backend/hub/templates/hub/emails/08_delete_confirmations.html create mode 100644 backend/hub/templates/hub/emails/08_delete_confirmations.txt diff --git a/backend/accounts/admin.py b/backend/accounts/admin.py index 09d0817e..6a95dae5 100644 --- a/backend/accounts/admin.py +++ b/backend/accounts/admin.py @@ -2,23 +2,25 @@ from django.contrib.admin.sites import NotRegistered from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin from django.contrib.auth.models import Group as BaseGroup +from django.contrib.sites.shortcuts import get_current_site from django.db.models import QuerySet from django.http import HttpRequest -from django.urls import reverse_lazy +from django.urls import reverse_lazy, reverse from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from impersonate.admin import UserAdminImpersonateMixin from civil_society_vote.common.admin import BasePermissionsAdmin +from civil_society_vote.common.messaging import send_email from .models import ( - GroupProxy, - User, CommissionUser, - COMMITTEE_GROUP, COMMITTEE_GROUP_READ_ONLY, + COMMITTEE_GROUP, + GroupProxy, STAFF_GROUP, SUPPORT_GROUP, + User, ) @@ -116,7 +118,28 @@ class UserAdmin(UserAdminImpersonateMixin, BasePermissionsAdmin): @admin.action(description=_("Request confirmations deletion")) def request_confirmations_deletion(self, request: HttpRequest, queryset: QuerySet[User]): - print("TODO request_confirmations_deletion") + + current_site = get_current_site(request) + protocol = "https" if request.is_secure() else "http" + + + for user in queryset: + if not user.in_commission_groups(): + continue + + deletion_link_path = reverse("reset-candidate-confirmations") + deletion_link = f"{protocol}://{current_site.domain}{deletion_link_path}" + + send_email( + subject="[VOTONG] Resetare confirmari candidati", + context={ + "deletion_link": deletion_link, + }, + to_emails=[user.email], + text_template="hub/emails/08_delete_confirmations.txt", + html_template="hub/emails/08_delete_confirmations.html", + ) + def change_view(self, request, object_id, form_url="", extra_context=None): extra_context = extra_context or {} diff --git a/backend/hub/templates/hub/emails/08_delete_confirmations.html b/backend/hub/templates/hub/emails/08_delete_confirmations.html new file mode 100644 index 00000000..7a107576 --- /dev/null +++ b/backend/hub/templates/hub/emails/08_delete_confirmations.html @@ -0,0 +1,10 @@ +{% extends "emails/base_email.html" %} + +{% block content %} +

Administratorul site-ului votong.ro a marcat pentru ștergere confirmările date de tine candidaților.

+ +

+ Urmează acest link pentru a aproba acțiunea: + {{ deletion_link }} +

+{% endblock %} diff --git a/backend/hub/templates/hub/emails/08_delete_confirmations.txt b/backend/hub/templates/hub/emails/08_delete_confirmations.txt new file mode 100644 index 00000000..59081942 --- /dev/null +++ b/backend/hub/templates/hub/emails/08_delete_confirmations.txt @@ -0,0 +1,3 @@ +Administratorul site-ului votong.ro a marcat pentru ștergere confirmările date de tine candidaților. + +Urmează acest link pentru a aproba acțiunea: {{ deletion_link }} diff --git a/backend/hub/urls.py b/backend/hub/urls.py index db9b0848..e59134f4 100644 --- a/backend/hub/urls.py +++ b/backend/hub/urls.py @@ -26,6 +26,7 @@ candidate_vote, organization_vote, update_organization_information, + reset_candidate_confirmations, ) urlpatterns = [ @@ -43,6 +44,7 @@ path(_("candidates//revoke/"), candidate_revoke, name="candidate-revoke"), path(_("candidates//status-confirm/"), candidate_status_confirm, name="candidate-status-confirm"), path(_("candidates//update/"), CandidateUpdateView.as_view(), name="candidate-update"), + path(_("candidates/reset-confirmations/"), reset_candidate_confirmations, name="reset-candidate-confirmations"), path(_("candidates/votes/"), ElectorCandidatesListView.as_view(), name="votes"), path(_("candidates/results/"), CandidateResultsView.as_view(), name="results"), path( diff --git a/backend/hub/views.py b/backend/hub/views.py index cd61f9a0..4fe0e073 100644 --- a/backend/hub/views.py +++ b/backend/hub/views.py @@ -15,7 +15,7 @@ from django.db import transaction from django.db.models import Count, Q, QuerySet from django.db.utils import IntegrityError -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils import timezone @@ -1061,6 +1061,25 @@ def candidate_status_confirm(request, pk): return redirect("candidate-detail", pk=pk) +@login_required +@permission_required_or_403("hub.approve_candidate") +def reset_candidate_confirmations(request): + if ( + FeatureFlag.flag_enabled(PHASE_CHOICES.enable_candidate_registration) + or FeatureFlag.flag_enabled(PHASE_CHOICES.enable_candidate_editing) + or FeatureFlag.flag_enabled(PHASE_CHOICES.enable_candidate_supporting) + or FeatureFlag.flag_enabled(PHASE_CHOICES.enable_candidate_voting) + ): + raise PermissionDenied + + if request.method == "POST": + CandidateConfirmation.objects.filter(user=request.user).delete() + return redirect(reverse("candidates")) + + return HttpResponse("TODO") + + + @login_required @permission_required_or_403("hub.change_organization") def update_organization_information(request, pk): diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 0231454d..78a3eeed 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-23 14:09+0200\n" +"POT-Creation-Date: 2024-11-23 17:24+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,31 +17,31 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: accounts/admin.py:69 hub/admin.py:292 hub/admin.py:407 +#: accounts/admin.py:78 hub/admin.py:292 hub/admin.py:407 msgid "Identification" msgstr "" -#: accounts/admin.py:81 +#: accounts/admin.py:90 msgid "Flags" msgstr "" -#: accounts/admin.py:91 +#: accounts/admin.py:100 msgid "Permissions" msgstr "" -#: accounts/admin.py:100 +#: accounts/admin.py:109 msgid "Important dates" msgstr "" -#: accounts/admin.py:110 +#: accounts/admin.py:119 msgid "Request confirmations deletion" msgstr "" -#: accounts/admin.py:130 hub/models.py:440 +#: accounts/admin.py:163 hub/models.py:440 msgid "Organization" msgstr "" -#: accounts/admin.py:143 accounts/models.py:97 +#: accounts/admin.py:176 accounts/models.py:97 msgid "Groups" msgstr "" @@ -1064,31 +1064,31 @@ msgstr "" msgid "Candidate" msgstr "" -#: hub/models.py:909 +#: hub/models.py:910 msgid "Cannot update candidate after votes have been cast." msgstr "" -#: hub/models.py:957 +#: hub/models.py:958 msgid "Candidate votes" msgstr "" -#: hub/models.py:958 +#: hub/models.py:959 msgid "Candidate vote" msgstr "" -#: hub/models.py:978 +#: hub/models.py:979 msgid "Canditate supporters" msgstr "Candidate supporters" -#: hub/models.py:979 +#: hub/models.py:980 msgid "Candidate supporter" msgstr "" -#: hub/models.py:990 +#: hub/models.py:991 msgid "Candidate confirmations" msgstr "" -#: hub/models.py:991 +#: hub/models.py:992 msgid "Candidate confirmation" msgstr "" @@ -1160,7 +1160,7 @@ msgstr "" #: hub/templates/hub/candidate/components/listing_detail.html:39 #: hub/templates/hub/candidate/results.html:65 -#: hub/templates/hub/committee/candidates.html:51 +#: hub/templates/hub/committee/candidates.html:67 #: hub/templates/hub/ngo/votes.html:84 msgid "View details" msgstr "" @@ -1170,7 +1170,7 @@ msgid "Section: " msgstr "" #: hub/templates/hub/candidate/detail.html:83 -#: hub/templates/hub/committee/candidates.html:44 +#: hub/templates/hub/committee/candidates.html:60 msgid "Supporters:" msgstr "" @@ -1246,17 +1246,27 @@ msgstr "" msgid "CES elections" msgstr "" -#: hub/templates/hub/committee/candidates.html:20 +#: hub/templates/hub/committee/candidates.html:21 #: hub/templates/hub/committee/partials/nav.html:22 msgid "Pending candidates" msgstr "" -#: hub/templates/hub/committee/candidates.html:22 +#: hub/templates/hub/committee/candidates.html:25 +#: hub/templates/hub/committee/partials/nav.html:32 +msgid "Validated candidates" +msgstr "" + +#: hub/templates/hub/committee/candidates.html:29 +#: hub/templates/hub/committee/partials/nav.html:37 +msgid "Rejected candidates" +msgstr "" + +#: hub/templates/hub/committee/candidates.html:33 #: hub/templates/hub/committee/partials/nav.html:27 msgid "Accepted candidates" msgstr "" -#: hub/templates/hub/committee/candidates.html:67 +#: hub/templates/hub/committee/candidates.html:83 #: hub/templates/hub/committee/list.html:67 hub/templates/hub/ngo/list.html:66 #: hub/templates/hub/ngo/votes.html:100 msgid "No results matching query" @@ -1277,14 +1287,6 @@ msgstr "" msgid "Rejected Organizations" msgstr "" -#: hub/templates/hub/committee/partials/nav.html:32 -msgid "Validated candidates" -msgstr "" - -#: hub/templates/hub/committee/partials/nav.html:37 -msgid "Rejected candidates" -msgstr "" - #: hub/templates/hub/developed-by.html:7 msgid "A project developed by" msgstr "" @@ -1605,83 +1607,87 @@ msgstr "" msgid "published" msgstr "" -#: hub/urls.py:33 +#: hub/urls.py:34 msgid "health/" msgstr "" -#: hub/urls.py:34 +#: hub/urls.py:35 msgid "candidates/" msgstr "" -#: hub/urls.py:36 +#: hub/urls.py:37 msgid "candidates/register/" msgstr "" -#: hub/urls.py:40 +#: hub/urls.py:41 msgid "candidates//" msgstr "" -#: hub/urls.py:41 +#: hub/urls.py:42 msgid "candidates//vote/" msgstr "" -#: hub/urls.py:42 +#: hub/urls.py:43 msgid "candidates//support/" msgstr "" -#: hub/urls.py:43 +#: hub/urls.py:44 msgid "candidates//revoke/" msgstr "" -#: hub/urls.py:44 +#: hub/urls.py:45 msgid "candidates//status-confirm/" msgstr "" -#: hub/urls.py:45 +#: hub/urls.py:46 msgid "candidates//update/" msgstr "" -#: hub/urls.py:46 +#: hub/urls.py:47 +msgid "candidates/reset-confirmations/" +msgstr "" + +#: hub/urls.py:48 msgid "candidates/votes/" msgstr "" -#: hub/urls.py:47 +#: hub/urls.py:49 msgid "candidates/results/" msgstr "" -#: hub/urls.py:49 +#: hub/urls.py:51 msgid "candidates/ces-results/" msgstr "" -#: hub/urls.py:51 +#: hub/urls.py:53 msgid "committee/ngos/" msgstr "" -#: hub/urls.py:52 +#: hub/urls.py:54 msgid "committee/candidates/" msgstr "" -#: hub/urls.py:53 +#: hub/urls.py:55 msgid "ngos/" msgstr "" -#: hub/urls.py:55 +#: hub/urls.py:57 msgid "ngos/register" msgstr "" -#: hub/urls.py:59 +#: hub/urls.py:61 msgid "ngos/" msgstr "" -#: hub/urls.py:60 +#: hub/urls.py:62 msgid "ngos//vote/" msgstr "" -#: hub/urls.py:61 +#: hub/urls.py:63 msgid "ngos//update" msgstr "" -#: hub/urls.py:62 +#: hub/urls.py:64 msgid "ngo-update/" msgstr "" @@ -1770,7 +1776,7 @@ msgstr "" msgid "Candidate does not belong to user organization" msgstr "" -#: hub/views.py:1084 +#: hub/views.py:1101 #, python-format msgid "Please wait %(minutes_threshold)s minutes before updating again." msgstr "" diff --git a/backend/locale/ro/LC_MESSAGES/django.po b/backend/locale/ro/LC_MESSAGES/django.po index 486092a0..12a9c096 100644 --- a/backend/locale/ro/LC_MESSAGES/django.po +++ b/backend/locale/ro/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-23 14:09+0200\n" +"POT-Creation-Date: 2024-11-23 17:24+0200\n" "PO-Revision-Date: 2020-04-23 17:54+0300\n" "Last-Translator: \n" "Language-Team: \n" @@ -19,31 +19,31 @@ msgstr "" "2:1));\n" "X-Generator: Poedit 2.3\n" -#: accounts/admin.py:69 hub/admin.py:292 hub/admin.py:407 +#: accounts/admin.py:78 hub/admin.py:292 hub/admin.py:407 msgid "Identification" msgstr "Identificare" -#: accounts/admin.py:81 +#: accounts/admin.py:90 msgid "Flags" msgstr "Acțiuni" -#: accounts/admin.py:91 +#: accounts/admin.py:100 msgid "Permissions" msgstr "Permisiuni" -#: accounts/admin.py:100 +#: accounts/admin.py:109 msgid "Important dates" msgstr "Date importante" -#: accounts/admin.py:110 +#: accounts/admin.py:119 msgid "Request confirmations deletion" msgstr "Inițiază ștergerea confirmărilor" -#: accounts/admin.py:130 hub/models.py:440 +#: accounts/admin.py:163 hub/models.py:440 msgid "Organization" msgstr "Organizație" -#: accounts/admin.py:143 accounts/models.py:97 +#: accounts/admin.py:176 accounts/models.py:97 msgid "Groups" msgstr "Grupuri" @@ -1107,31 +1107,31 @@ msgstr "Candidaturi" msgid "Candidate" msgstr "Candidatură" -#: hub/models.py:909 +#: hub/models.py:910 msgid "Cannot update candidate after votes have been cast." msgstr "Nu se poate modifica candidatura după ce s-au înregistrat voturi." -#: hub/models.py:957 +#: hub/models.py:958 msgid "Candidate votes" msgstr "Voturi candidatură" -#: hub/models.py:958 +#: hub/models.py:959 msgid "Candidate vote" msgstr "Vot candidatură" -#: hub/models.py:978 +#: hub/models.py:979 msgid "Canditate supporters" msgstr "Susținători candidatură" -#: hub/models.py:979 +#: hub/models.py:980 msgid "Candidate supporter" msgstr "Susținător candidatură" -#: hub/models.py:990 +#: hub/models.py:991 msgid "Candidate confirmations" msgstr "Confirmări candidatură" -#: hub/models.py:991 +#: hub/models.py:992 msgid "Candidate confirmation" msgstr "Confirmare candidatură" @@ -1205,7 +1205,7 @@ msgstr "Descarcă fișierul actual" #: hub/templates/hub/candidate/components/listing_detail.html:39 #: hub/templates/hub/candidate/results.html:65 -#: hub/templates/hub/committee/candidates.html:51 +#: hub/templates/hub/committee/candidates.html:67 #: hub/templates/hub/ngo/votes.html:84 msgid "View details" msgstr "Vizualizează detalii" @@ -1215,7 +1215,7 @@ msgid "Section: " msgstr "Secțiune: " #: hub/templates/hub/candidate/detail.html:83 -#: hub/templates/hub/committee/candidates.html:44 +#: hub/templates/hub/committee/candidates.html:60 msgid "Supporters:" msgstr "Susținători:" @@ -1291,17 +1291,27 @@ msgstr "Alegeri CE" msgid "CES elections" msgstr "Alegeri CES" -#: hub/templates/hub/committee/candidates.html:20 +#: hub/templates/hub/committee/candidates.html:21 #: hub/templates/hub/committee/partials/nav.html:22 msgid "Pending candidates" msgstr "Candidaturi propuse" -#: hub/templates/hub/committee/candidates.html:22 +#: hub/templates/hub/committee/candidates.html:25 +#: hub/templates/hub/committee/partials/nav.html:32 +msgid "Validated candidates" +msgstr "Candidaturi validate" + +#: hub/templates/hub/committee/candidates.html:29 +#: hub/templates/hub/committee/partials/nav.html:37 +msgid "Rejected candidates" +msgstr "Candidaturi respinse" + +#: hub/templates/hub/committee/candidates.html:33 #: hub/templates/hub/committee/partials/nav.html:27 msgid "Accepted candidates" msgstr "Candidaturi acceptate" -#: hub/templates/hub/committee/candidates.html:67 +#: hub/templates/hub/committee/candidates.html:83 #: hub/templates/hub/committee/list.html:67 hub/templates/hub/ngo/list.html:66 #: hub/templates/hub/ngo/votes.html:100 msgid "No results matching query" @@ -1322,14 +1332,6 @@ msgstr "Organizații validate" msgid "Rejected Organizations" msgstr "Organizații respinse" -#: hub/templates/hub/committee/partials/nav.html:32 -msgid "Validated candidates" -msgstr "Candidaturi validate" - -#: hub/templates/hub/committee/partials/nav.html:37 -msgid "Rejected candidates" -msgstr "Candidaturi respinse" - #: hub/templates/hub/developed-by.html:7 msgid "A project developed by" msgstr "Un proiect dezvoltat de" @@ -1666,83 +1668,87 @@ msgstr "publicat la" msgid "published" msgstr "publicat" -#: hub/urls.py:33 +#: hub/urls.py:34 msgid "health/" msgstr "health/" -#: hub/urls.py:34 +#: hub/urls.py:35 msgid "candidates/" msgstr "candidatura/" -#: hub/urls.py:36 +#: hub/urls.py:37 msgid "candidates/register/" msgstr "candidatura/inregistrare/" -#: hub/urls.py:40 +#: hub/urls.py:41 msgid "candidates//" msgstr "candidatura/" -#: hub/urls.py:41 +#: hub/urls.py:42 msgid "candidates//vote/" msgstr "candidatura//vot/" -#: hub/urls.py:42 +#: hub/urls.py:43 msgid "candidates//support/" msgstr "candidatura//sustinere/" -#: hub/urls.py:43 +#: hub/urls.py:44 msgid "candidates//revoke/" msgstr "candidatura//revocare/" -#: hub/urls.py:44 +#: hub/urls.py:45 msgid "candidates//status-confirm/" msgstr "candidatura//confirma-starea/" -#: hub/urls.py:45 +#: hub/urls.py:46 msgid "candidates//update/" msgstr "candidatura//actualizare/" -#: hub/urls.py:46 +#: hub/urls.py:47 +msgid "candidates/reset-confirmations/" +msgstr "candidates/reseteaza-confirmarile/" + +#: hub/urls.py:48 msgid "candidates/votes/" msgstr "candidatura/voturi/" -#: hub/urls.py:47 +#: hub/urls.py:49 msgid "candidates/results/" msgstr "candidatura/rezultate/" -#: hub/urls.py:49 +#: hub/urls.py:51 msgid "candidates/ces-results/" msgstr "candidatura/ces-rezultate/" -#: hub/urls.py:51 +#: hub/urls.py:53 msgid "committee/ngos/" msgstr "comisie/organizatii/" -#: hub/urls.py:52 +#: hub/urls.py:54 msgid "committee/candidates/" msgstr "comisie/candidatura/" -#: hub/urls.py:53 +#: hub/urls.py:55 msgid "ngos/" msgstr "organizatii/" -#: hub/urls.py:55 +#: hub/urls.py:57 msgid "ngos/register" msgstr "organizatii/inregistrare" -#: hub/urls.py:59 +#: hub/urls.py:61 msgid "ngos/" msgstr "organizatii/" -#: hub/urls.py:60 +#: hub/urls.py:62 msgid "ngos//vote/" msgstr "organizatii//vot/" -#: hub/urls.py:61 +#: hub/urls.py:63 msgid "ngos//update" msgstr "organizatii//actualizare" -#: hub/urls.py:62 +#: hub/urls.py:64 msgid "ngo-update/" msgstr "org-actualizare/" @@ -1826,30 +1832,22 @@ msgid "Authenticated user does not have an organization." msgstr "Utilizatorul autentificat nu are o organizație." #: hub/views.py:936 -#, fuzzy -#| msgid "Candidate registration is closed." msgid "Candidate editing disabled" -msgstr "Înregistrarea candidaților este închisă." +msgstr "Editarea candidaților este închisă" #: hub/views.py:943 -#, fuzzy -#| msgid "Register organization" msgid "No user organization" -msgstr "Înscriere organizație" +msgstr "Utilizator fără organizație" #: hub/views.py:946 -#, fuzzy -#| msgid "candidate" msgid "No candidate" -msgstr "candidatură" +msgstr "Niciun candidat" #: hub/views.py:949 -#, fuzzy -#| msgid "Authenticated user does not have an organization." msgid "Candidate does not belong to user organization" -msgstr "Utilizatorul autentificat nu are o organizație." +msgstr "Candidatul nu ține de organizația utilizatorului" -#: hub/views.py:1084 +#: hub/views.py:1101 #, python-format msgid "Please wait %(minutes_threshold)s minutes before updating again." msgstr "" From 254a03f6af16213ef4f53f80d92be525eba98d87 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 17:54:30 +0200 Subject: [PATCH 08/16] Code format --- backend/accounts/admin.py | 2 -- backend/hub/views.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/accounts/admin.py b/backend/accounts/admin.py index 6a95dae5..44e42154 100644 --- a/backend/accounts/admin.py +++ b/backend/accounts/admin.py @@ -122,7 +122,6 @@ def request_confirmations_deletion(self, request: HttpRequest, queryset: QuerySe current_site = get_current_site(request) protocol = "https" if request.is_secure() else "http" - for user in queryset: if not user.in_commission_groups(): continue @@ -140,7 +139,6 @@ def request_confirmations_deletion(self, request: HttpRequest, queryset: QuerySe html_template="hub/emails/08_delete_confirmations.html", ) - def change_view(self, request, object_id, form_url="", extra_context=None): extra_context = extra_context or {} extra_context["user_id"] = object_id diff --git a/backend/hub/views.py b/backend/hub/views.py index 4fe0e073..793b43b0 100644 --- a/backend/hub/views.py +++ b/backend/hub/views.py @@ -1075,9 +1075,8 @@ def reset_candidate_confirmations(request): if request.method == "POST": CandidateConfirmation.objects.filter(user=request.user).delete() return redirect(reverse("candidates")) - + return HttpResponse("TODO") - @login_required From db5a23f368bbe7be94326037b90057578f0e08d5 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 18:29:06 +0200 Subject: [PATCH 09/16] Add the delete confirmations form --- .../hub/committee/delete_confirmations.html | 19 +++++++++++++++++++ backend/hub/views.py | 16 +++++----------- 2 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 backend/hub/templates/hub/committee/delete_confirmations.html diff --git a/backend/hub/templates/hub/committee/delete_confirmations.html b/backend/hub/templates/hub/committee/delete_confirmations.html new file mode 100644 index 00000000..0b95785f --- /dev/null +++ b/backend/hub/templates/hub/committee/delete_confirmations.html @@ -0,0 +1,19 @@ +{% extends 'hub/ngo/base.html' %} +{% load static i18n %} +{% load spurl %} +{% load hub_tags %} + + +{% block content %} + +
+
+ {% csrf_token %} +
{% trans "Reset candidate confirmations" %}
+

{% trans "Resetting the candidate confirmations is irreversible. Are you sure you want to continue?" %}

+

+
+ +
+ +{% endblock %} diff --git a/backend/hub/views.py b/backend/hub/views.py index 793b43b0..53b86ec4 100644 --- a/backend/hub/views.py +++ b/backend/hub/views.py @@ -15,8 +15,8 @@ from django.db import transaction from django.db.models import Count, Q, QuerySet from django.db.utils import IntegrityError -from django.http import JsonResponse, HttpResponse -from django.shortcuts import get_object_or_404, redirect +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext as _ @@ -1064,19 +1064,13 @@ def candidate_status_confirm(request, pk): @login_required @permission_required_or_403("hub.approve_candidate") def reset_candidate_confirmations(request): - if ( - FeatureFlag.flag_enabled(PHASE_CHOICES.enable_candidate_registration) - or FeatureFlag.flag_enabled(PHASE_CHOICES.enable_candidate_editing) - or FeatureFlag.flag_enabled(PHASE_CHOICES.enable_candidate_supporting) - or FeatureFlag.flag_enabled(PHASE_CHOICES.enable_candidate_voting) - ): - raise PermissionDenied - + # TODO: Do we need to test for other feature flags before allowing the confirmation deletion? if request.method == "POST": CandidateConfirmation.objects.filter(user=request.user).delete() + messages.success(request, _("Confirmations successfuly deleted")) return redirect(reverse("candidates")) - return HttpResponse("TODO") + return render(request, "hub/committee/delete_confirmations.html") @login_required From 2b55b8663d6b1f4b0250d3e49510905e8c8369a8 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sat, 23 Nov 2024 18:50:36 +0200 Subject: [PATCH 10/16] Update translations --- backend/locale/en/LC_MESSAGES/django.po | 26 ++++++++++++++++++++---- backend/locale/ro/LC_MESSAGES/django.po | 27 +++++++++++++++++++++---- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 78a3eeed..6e8b108e 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-23 17:24+0200\n" +"POT-Creation-Date: 2024-11-23 18:47+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -37,11 +37,11 @@ msgstr "" msgid "Request confirmations deletion" msgstr "" -#: accounts/admin.py:163 hub/models.py:440 +#: accounts/admin.py:158 hub/models.py:440 msgid "Organization" msgstr "" -#: accounts/admin.py:176 accounts/models.py:97 +#: accounts/admin.py:171 accounts/models.py:97 msgid "Groups" msgstr "" @@ -1272,6 +1272,20 @@ msgstr "" msgid "No results matching query" msgstr "" +#: hub/templates/hub/committee/delete_confirmations.html:12 +msgid "Reset candidate confirmations" +msgstr "" + +#: hub/templates/hub/committee/delete_confirmations.html:13 +msgid "" +"Resetting the candidate confirmations is irreversible. Are you sure you want " +"to continue?" +msgstr "" + +#: hub/templates/hub/committee/delete_confirmations.html:14 +msgid "Continue" +msgstr "" + #: hub/templates/hub/committee/list.html:23 #: hub/templates/hub/committee/partials/nav.html:7 msgid "Pending Organizations" @@ -1776,7 +1790,11 @@ msgstr "" msgid "Candidate does not belong to user organization" msgstr "" -#: hub/views.py:1101 +#: hub/views.py:1070 +msgid "Confirmations successfuly deleted" +msgstr "" + +#: hub/views.py:1096 #, python-format msgid "Please wait %(minutes_threshold)s minutes before updating again." msgstr "" diff --git a/backend/locale/ro/LC_MESSAGES/django.po b/backend/locale/ro/LC_MESSAGES/django.po index 12a9c096..69cbdabf 100644 --- a/backend/locale/ro/LC_MESSAGES/django.po +++ b/backend/locale/ro/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-23 17:24+0200\n" +"POT-Creation-Date: 2024-11-23 18:47+0200\n" "PO-Revision-Date: 2020-04-23 17:54+0300\n" "Last-Translator: \n" "Language-Team: \n" @@ -39,11 +39,11 @@ msgstr "Date importante" msgid "Request confirmations deletion" msgstr "Inițiază ștergerea confirmărilor" -#: accounts/admin.py:163 hub/models.py:440 +#: accounts/admin.py:158 hub/models.py:440 msgid "Organization" msgstr "Organizație" -#: accounts/admin.py:176 accounts/models.py:97 +#: accounts/admin.py:171 accounts/models.py:97 msgid "Groups" msgstr "Grupuri" @@ -1317,6 +1317,21 @@ msgstr "Candidaturi acceptate" msgid "No results matching query" msgstr "Nu există nici un rezultat" +#: hub/templates/hub/committee/delete_confirmations.html:12 +msgid "Reset candidate confirmations" +msgstr "Șterge confirmările de candidatură" + +#: hub/templates/hub/committee/delete_confirmations.html:13 +msgid "" +"Resetting the candidate confirmations is irreversible. Are you sure you want " +"to continue?" +msgstr "Ștergerea confirmărilor de candidatură este ireversibilă. Sigur vrei " +"să continui?" + +#: hub/templates/hub/committee/delete_confirmations.html:14 +msgid "Continue" +msgstr "Continuă" + #: hub/templates/hub/committee/list.html:23 #: hub/templates/hub/committee/partials/nav.html:7 msgid "Pending Organizations" @@ -1847,7 +1862,11 @@ msgstr "Niciun candidat" msgid "Candidate does not belong to user organization" msgstr "Candidatul nu ține de organizația utilizatorului" -#: hub/views.py:1101 +#: hub/views.py:1070 +msgid "Confirmations successfuly deleted" +msgstr "" + +#: hub/views.py:1096 #, python-format msgid "Please wait %(minutes_threshold)s minutes before updating again." msgstr "" From d557406440860e8881abf40f476a468e2934aced Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Sun, 24 Nov 2024 18:17:47 +0200 Subject: [PATCH 11/16] Create an expiring URL system --- backend/accounts/admin.py | 3 +- backend/civil_society_vote/settings.py | 5 ++ .../hub/emails/08_delete_confirmations.html | 2 +- .../hub/emails/08_delete_confirmations.txt | 2 +- backend/hub/urls.py | 6 +- backend/hub/utils.py | 78 +++++++++++++++++++ backend/hub/views.py | 8 +- 7 files changed, 98 insertions(+), 6 deletions(-) diff --git a/backend/accounts/admin.py b/backend/accounts/admin.py index 44e42154..7dddbbb4 100644 --- a/backend/accounts/admin.py +++ b/backend/accounts/admin.py @@ -12,6 +12,7 @@ from civil_society_vote.common.admin import BasePermissionsAdmin from civil_society_vote.common.messaging import send_email +from hub.utils import create_expiring_url_token from .models import ( CommissionUser, @@ -126,7 +127,7 @@ def request_confirmations_deletion(self, request: HttpRequest, queryset: QuerySe if not user.in_commission_groups(): continue - deletion_link_path = reverse("reset-candidate-confirmations") + deletion_link_path = reverse("reset-candidate-confirmations", args=(create_expiring_url_token(user.pk),)) deletion_link = f"{protocol}://{current_site.domain}{deletion_link_path}" send_email( diff --git a/backend/civil_society_vote/settings.py b/backend/civil_society_vote/settings.py index 1faba0bd..20e3d343 100644 --- a/backend/civil_society_vote/settings.py +++ b/backend/civil_society_vote/settings.py @@ -8,6 +8,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/ """ +import hashlib import os from copy import deepcopy from pathlib import Path @@ -91,6 +92,7 @@ ENABLE_ORG_REGISTRATION_FORM=(bool, False), CURRENT_EDITION_YEAR=(int, 2024), CURRENT_EDITION_TYPE=(str, "ces"), + EXPIRING_URL_DELTA=(int, 72 * 60 * 60), # 72 hours # email settings EMAIL_SEND_METHOD=(str, "async"), EMAIL_BACKEND=(str, "django.core.mail.backends.console.EmailBackend"), @@ -121,6 +123,7 @@ # SECURITY WARNING: keep the secret key used in production secret SECRET_KEY = env("SECRET_KEY") +SECRET_KEY_HASH = hashlib.sha256(f"{SECRET_KEY}".encode()).hexdigest() DEBUG = env("DEBUG") ENVIRONMENT = env.str("ENVIRONMENT") @@ -645,3 +648,5 @@ def show_toolbar(request): # How many previous year reports to require for a candidate proposal PREV_REPORTS_REQUIRED_FOR_PROPOSAL = 3 + +EXPIRING_URL_DELTA = env.int("EXPIRING_URL_DELTA") diff --git a/backend/hub/templates/hub/emails/08_delete_confirmations.html b/backend/hub/templates/hub/emails/08_delete_confirmations.html index 7a107576..aa324993 100644 --- a/backend/hub/templates/hub/emails/08_delete_confirmations.html +++ b/backend/hub/templates/hub/emails/08_delete_confirmations.html @@ -5,6 +5,6 @@

Administratorul site-ului votong.ro a marcat pentru ștergere confirmările

Urmează acest link pentru a aproba acțiunea: - {{ deletion_link }} + {{ deletion_link|safe }}

{% endblock %} diff --git a/backend/hub/templates/hub/emails/08_delete_confirmations.txt b/backend/hub/templates/hub/emails/08_delete_confirmations.txt index 59081942..a541a422 100644 --- a/backend/hub/templates/hub/emails/08_delete_confirmations.txt +++ b/backend/hub/templates/hub/emails/08_delete_confirmations.txt @@ -1,3 +1,3 @@ Administratorul site-ului votong.ro a marcat pentru ștergere confirmările date de tine candidaților. -Urmează acest link pentru a aproba acțiunea: {{ deletion_link }} +Urmează acest link pentru a aproba acțiunea: {{ deletion_link|safe }} diff --git a/backend/hub/urls.py b/backend/hub/urls.py index e59134f4..066608df 100644 --- a/backend/hub/urls.py +++ b/backend/hub/urls.py @@ -44,12 +44,16 @@ path(_("candidates//revoke/"), candidate_revoke, name="candidate-revoke"), path(_("candidates//status-confirm/"), candidate_status_confirm, name="candidate-status-confirm"), path(_("candidates//update/"), CandidateUpdateView.as_view(), name="candidate-update"), - path(_("candidates/reset-confirmations/"), reset_candidate_confirmations, name="reset-candidate-confirmations"), path(_("candidates/votes/"), ElectorCandidatesListView.as_view(), name="votes"), path(_("candidates/results/"), CandidateResultsView.as_view(), name="results"), path( _("candidates/ces-results/"), RedirectView.as_view(pattern_name="results", permanent=True), name="ces-results" ), + path( + _("committee/reset-confirmations//"), + reset_candidate_confirmations, + name="reset-candidate-confirmations", + ), path(_("committee/ngos/"), CommitteeOrganizationListView.as_view(), name="committee-ngos"), path(_("committee/candidates/"), CommitteeCandidatesListView.as_view(), name="committee-candidates"), path(_("ngos/"), OrganizationListView.as_view(), name="ngos"), diff --git a/backend/hub/utils.py b/backend/hub/utils.py index c92e82e3..04a92eb8 100644 --- a/backend/hub/utils.py +++ b/backend/hub/utils.py @@ -1,3 +1,12 @@ +import hashlib +from datetime import datetime + +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.utils import timezone +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode + + def build_full_url(request, obj): """ :param request: django Request object @@ -6,3 +15,72 @@ def build_full_url(request, obj): :return: returns the full URL towards the obj detail page (if any) """ return request.build_absolute_uri(obj.get_absolute_url()) + + +def validate_expiring_url_token(url_token, max_seconds): + + try: + decoded = urlsafe_base64_decode(url_token).decode().split("!!") + except ValueError: + print("Broken token") + return False + + if len(decoded) != 3: + print("Broken token components") + return False + + subject_pk = decoded[0] + iso_ts = decoded[1] + sig = decoded[2] + + try: + ts = datetime.fromisoformat(iso_ts) + except ValueError: + print("DATE VALUE ERROR") + return False + + delta = (timezone.now() - ts).seconds + if delta > max_seconds: + print("Expired") + return False + elif delta < 0: + print("You cannot have a link created in the future") + return False + + current_sig = hashlib.sha256(f"USER ID={subject_pk} T={iso_ts} K={settings.SECRET_KEY_HASH}".encode()).hexdigest() + if sig != current_sig: + print("Signature not valid") + return False + + return True + + +def create_expiring_url_token(subject_pk): + """ + Create an URL safe token composed of three parts: + {subject_id}-{timestamp}-{hash} + """ + iso_ts = timezone.now().isoformat() + sig: str = hashlib.sha256(f"USER ID={subject_pk} T={iso_ts} K={settings.SECRET_KEY_HASH}".encode()).hexdigest() + + unencoded_token = f"{subject_pk}!!{iso_ts}!!{sig}" + return urlsafe_base64_encode(unencoded_token.encode()) + + +def hashed_expiring_url(request): + """ + Validate the `id`, `ts`, and `sign` from an URL GET parameters + """ + pass + + +def expiring_url(max_seconds=settings.EXPIRING_URL_DELTA): + def decorator(function): + def wrapper(request, url_token, *args, **kwargs): + if not validate_expiring_url_token(url_token, max_seconds): + raise PermissionDenied() + return function(request, *args, **kwargs) + + return wrapper + + return decorator diff --git a/backend/hub/views.py b/backend/hub/views.py index 53b86ec4..2eba1a8d 100644 --- a/backend/hub/views.py +++ b/backend/hub/views.py @@ -54,6 +54,8 @@ SETTINGS_CHOICES, ) from hub.workers.update_organization import update_organization +from hub.utils import expiring_url + logger = logging.getLogger(__name__) @@ -1062,9 +1064,11 @@ def candidate_status_confirm(request, pk): @login_required +@expiring_url() @permission_required_or_403("hub.approve_candidate") -def reset_candidate_confirmations(request): - # TODO: Do we need to test for other feature flags before allowing the confirmation deletion? +def reset_candidate_confirmations( + request, +): if request.method == "POST": CandidateConfirmation.objects.filter(user=request.user).delete() messages.success(request, _("Confirmations successfuly deleted")) From d001c73ab9ec3d83a8168f40fdb2f74de460441b Mon Sep 17 00:00:00 2001 From: Tudor Amariei Date: Tue, 26 Nov 2024 10:08:20 +0200 Subject: [PATCH 12/16] Refactor some code --- backend/hub/utils.py | 39 +++++++++++++++++++++++++++++++-------- backend/hub/views.py | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/backend/hub/utils.py b/backend/hub/utils.py index 04a92eb8..dcf53328 100644 --- a/backend/hub/utils.py +++ b/backend/hub/utils.py @@ -17,8 +17,7 @@ def build_full_url(request, obj): return request.build_absolute_uri(obj.get_absolute_url()) -def validate_expiring_url_token(url_token, max_seconds): - +def decode_url_token(url_token): try: decoded = urlsafe_base64_decode(url_token).decode().split("!!") except ValueError: @@ -29,9 +28,28 @@ def validate_expiring_url_token(url_token, max_seconds): print("Broken token components") return False - subject_pk = decoded[0] - iso_ts = decoded[1] - sig = decoded[2] + try: + user_pk = int(decoded[0]) + except ValueError: + return False + + return { + "user_pk": user_pk, + "iso_ts": decoded[1], + "sig": decoded[2], + } + + +def validate_expiring_url_token(url_token, max_seconds): + + decoded_token = decode_url_token(url_token) + + if decoded_token is False: + return decoded_token + + user_pk: int = decoded_token["user_pk"] + iso_ts: str = decoded_token["iso_ts"] + sig: str = decoded_token["sig"] try: ts = datetime.fromisoformat(iso_ts) @@ -47,7 +65,9 @@ def validate_expiring_url_token(url_token, max_seconds): print("You cannot have a link created in the future") return False - current_sig = hashlib.sha256(f"USER ID={subject_pk} T={iso_ts} K={settings.SECRET_KEY_HASH}".encode()).hexdigest() + # noinspection InsecureHash + current_sig = hashlib.sha256(f"USER ID={user_pk} T={iso_ts} K={settings.SECRET_KEY_HASH}".encode()).hexdigest() + if sig != current_sig: print("Signature not valid") return False @@ -57,13 +77,16 @@ def validate_expiring_url_token(url_token, max_seconds): def create_expiring_url_token(subject_pk): """ - Create an URL safe token composed of three parts: + Create a URL-safe token composed of three parts: {subject_id}-{timestamp}-{hash} """ iso_ts = timezone.now().isoformat() + + # noinspection InsecureHash sig: str = hashlib.sha256(f"USER ID={subject_pk} T={iso_ts} K={settings.SECRET_KEY_HASH}".encode()).hexdigest() - unencoded_token = f"{subject_pk}!!{iso_ts}!!{sig}" + unencoded_token = "!!".join([subject_pk, iso_ts, sig]) + return urlsafe_base64_encode(unencoded_token.encode()) diff --git a/backend/hub/views.py b/backend/hub/views.py index 2eba1a8d..50429e3b 100644 --- a/backend/hub/views.py +++ b/backend/hub/views.py @@ -15,7 +15,7 @@ from django.db import transaction from django.db.models import Count, Q, QuerySet from django.db.utils import IntegrityError -from django.http import JsonResponse +from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone @@ -54,7 +54,7 @@ SETTINGS_CHOICES, ) from hub.workers.update_organization import update_organization -from hub.utils import expiring_url +from hub.utils import decode_url_token, expiring_url logger = logging.getLogger(__name__) @@ -1069,12 +1069,35 @@ def candidate_status_confirm(request, pk): def reset_candidate_confirmations( request, ): - if request.method == "POST": - CandidateConfirmation.objects.filter(user=request.user).delete() - messages.success(request, _("Confirmations successfuly deleted")) - return redirect(reverse("candidates")) + if request.method != "POST": + return render(request, "hub/committee/delete_confirmations.html") - return render(request, "hub/committee/delete_confirmations.html") + if not hasattr(request.resolver_match, "captured_kwargs"): + raise Http404 + + url_token = request.resolver_match.captured_kwargs.get("url_token") + + if not url_token: + raise PermissionDenied + + decoded_token = decode_url_token(url_token) + + if decoded_token is False: + raise PermissionDenied + + user_pk = decoded_token.get("user_pk") + if not user_pk: + raise PermissionDenied + + user = get_object_or_404(User, pk=user_pk) + + if request.user != user: + raise PermissionDenied + + CandidateConfirmation.objects.filter(user=request.user).delete() + messages.success(request, _("Confirmations successfully deleted")) + + return redirect(reverse("candidates")) @login_required From 3e080ed2dad53c0c062adbb1c9e478593a280ed2 Mon Sep 17 00:00:00 2001 From: Tudor Amariei Date: Tue, 26 Nov 2024 11:45:37 +0200 Subject: [PATCH 13/16] Remove print statements from the code --- backend/civil_society_vote/settings.py | 2 +- backend/hub/social_adapters.py | 1 - backend/hub/utils.py | 6 ------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/civil_society_vote/settings.py b/backend/civil_society_vote/settings.py index 20e3d343..dd9f10ed 100644 --- a/backend/civil_society_vote/settings.py +++ b/backend/civil_society_vote/settings.py @@ -643,7 +643,7 @@ def show_toolbar(request): CURRENT_EDITION_YEAR = env("CURRENT_EDITION_YEAR") CURRENT_EDITION_TYPE = env("CURRENT_EDITION_TYPE").lower().strip() if CURRENT_EDITION_TYPE not in ("ces", "ce"): - print(f"WARNING: The CURRENT_EDITION_TYPE '{CURRENT_EDITION_TYPE}' has been reset to 'ces'") + # info_log = f"WARNING: The CURRENT_EDITION_TYPE '{CURRENT_EDITION_TYPE}' has been reset to 'ces'" CURRENT_EDITION_TYPE = "ces" # How many previous year reports to require for a candidate proposal diff --git a/backend/hub/social_adapters.py b/backend/hub/social_adapters.py index af13c3f1..7339e2d1 100644 --- a/backend/hub/social_adapters.py +++ b/backend/hub/social_adapters.py @@ -35,7 +35,6 @@ def ngohub_api_get(path: str, token: str): response = requests.get(api_url, headers=auth_headers) if response.status_code != requests.codes.ok: - print(api_url) logger.error("%s while retrieving %s", response.status_code, api_url) raise NGOHubHTTPException diff --git a/backend/hub/utils.py b/backend/hub/utils.py index dcf53328..22d91d9a 100644 --- a/backend/hub/utils.py +++ b/backend/hub/utils.py @@ -21,11 +21,9 @@ def decode_url_token(url_token): try: decoded = urlsafe_base64_decode(url_token).decode().split("!!") except ValueError: - print("Broken token") return False if len(decoded) != 3: - print("Broken token components") return False try: @@ -54,22 +52,18 @@ def validate_expiring_url_token(url_token, max_seconds): try: ts = datetime.fromisoformat(iso_ts) except ValueError: - print("DATE VALUE ERROR") return False delta = (timezone.now() - ts).seconds if delta > max_seconds: - print("Expired") return False elif delta < 0: - print("You cannot have a link created in the future") return False # noinspection InsecureHash current_sig = hashlib.sha256(f"USER ID={user_pk} T={iso_ts} K={settings.SECRET_KEY_HASH}".encode()).hexdigest() if sig != current_sig: - print("Signature not valid") return False return True From 83f7fd2d4839a18591b67b0981e3edf51b6042d9 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Tue, 26 Nov 2024 13:03:12 +0200 Subject: [PATCH 14/16] Improve the expiring url handling --- backend/hub/utils.py | 51 ++++++++++++++++++++++---------------------- backend/hub/views.py | 27 ++++++----------------- 2 files changed, 32 insertions(+), 46 deletions(-) diff --git a/backend/hub/utils.py b/backend/hub/utils.py index 22d91d9a..581bad27 100644 --- a/backend/hub/utils.py +++ b/backend/hub/utils.py @@ -17,37 +17,44 @@ def build_full_url(request, obj): return request.build_absolute_uri(obj.get_absolute_url()) -def decode_url_token(url_token): - try: - decoded = urlsafe_base64_decode(url_token).decode().split("!!") - except ValueError: - return False +def decode_url_token(*, url_token=None, request=None): + if request: + if not hasattr(request.resolver_match, "captured_kwargs"): + return None + url_token = request.resolver_match.captured_kwargs.get("url_token") + + if url_token: + try: + decoded = urlsafe_base64_decode(url_token).decode().split("!!") + except ValueError: + return None if len(decoded) != 3: - return False + return None try: - user_pk = int(decoded[0]) + subject_pk = decoded[0] except ValueError: - return False + return None return { - "user_pk": user_pk, + "subject_pk": subject_pk, "iso_ts": decoded[1], "sig": decoded[2], } -def validate_expiring_url_token(url_token, max_seconds): +def validate_decoded_url_token(decoded_token, max_seconds): - decoded_token = decode_url_token(url_token) + if decoded_token is None: + return False - if decoded_token is False: - return decoded_token + subject_pk: int = decoded_token.get("subject_pk") + iso_ts: str = decoded_token.get("iso_ts") + sig: str = decoded_token.get("sig") - user_pk: int = decoded_token["user_pk"] - iso_ts: str = decoded_token["iso_ts"] - sig: str = decoded_token["sig"] + if not subject_pk or not iso_ts or not sig: + return False try: ts = datetime.fromisoformat(iso_ts) @@ -61,7 +68,7 @@ def validate_expiring_url_token(url_token, max_seconds): return False # noinspection InsecureHash - current_sig = hashlib.sha256(f"USER ID={user_pk} T={iso_ts} K={settings.SECRET_KEY_HASH}".encode()).hexdigest() + current_sig = hashlib.sha256(f"USER ID={subject_pk} T={iso_ts} K={settings.SECRET_KEY_HASH}".encode()).hexdigest() if sig != current_sig: return False @@ -84,17 +91,11 @@ def create_expiring_url_token(subject_pk): return urlsafe_base64_encode(unencoded_token.encode()) -def hashed_expiring_url(request): - """ - Validate the `id`, `ts`, and `sign` from an URL GET parameters - """ - pass - - def expiring_url(max_seconds=settings.EXPIRING_URL_DELTA): def decorator(function): def wrapper(request, url_token, *args, **kwargs): - if not validate_expiring_url_token(url_token, max_seconds): + decoded_token = decode_url_token(url_token=url_token) + if not validate_decoded_url_token(decoded_token, max_seconds): raise PermissionDenied() return function(request, *args, **kwargs) diff --git a/backend/hub/views.py b/backend/hub/views.py index 50429e3b..59b75567 100644 --- a/backend/hub/views.py +++ b/backend/hub/views.py @@ -15,7 +15,7 @@ from django.db import transaction from django.db.models import Count, Q, QuerySet from django.db.utils import IntegrityError -from django.http import Http404, JsonResponse +from django.http import JsonResponse, Http404 from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone @@ -54,7 +54,7 @@ SETTINGS_CHOICES, ) from hub.workers.update_organization import update_organization -from hub.utils import decode_url_token, expiring_url +from hub.utils import expiring_url, decode_url_token logger = logging.getLogger(__name__) @@ -1072,27 +1072,12 @@ def reset_candidate_confirmations( if request.method != "POST": return render(request, "hub/committee/delete_confirmations.html") - if not hasattr(request.resolver_match, "captured_kwargs"): + decoded_token = decode_url_token(request=request) + if not decoded_token: raise Http404 - url_token = request.resolver_match.captured_kwargs.get("url_token") - - if not url_token: - raise PermissionDenied - - decoded_token = decode_url_token(url_token) - - if decoded_token is False: - raise PermissionDenied - - user_pk = decoded_token.get("user_pk") - if not user_pk: - raise PermissionDenied - - user = get_object_or_404(User, pk=user_pk) - - if request.user != user: - raise PermissionDenied + if request.user.pk != decoded_token.get("subject_pk", None): + raise PermissionDenied(_("Cannot delete another user's confirmations")) CandidateConfirmation.objects.filter(user=request.user).delete() messages.success(request, _("Confirmations successfully deleted")) From 07ef3f4357a9dc7ff7796f88eb93fd78ce7d63e1 Mon Sep 17 00:00:00 2001 From: Tudor Amariei Date: Tue, 26 Nov 2024 13:04:01 +0200 Subject: [PATCH 15/16] Update requirements --- backend/requirements-dev.txt | 136 ++++++++++++++++++----------------- backend/requirements.txt | 6 +- 2 files changed, 72 insertions(+), 70 deletions(-) diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 8fb09065..3add03e7 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --output-file=requirements-dev.txt --strip-extras requirements-dev.in # asgiref==3.8.1 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django asttokens==2.4.1 # via stack-data @@ -15,46 +15,46 @@ attrs==24.2.0 black==24.10.0 # via -r requirements-dev.in blessed==1.20.0 - # via -r /var/www/votong/backend/requirements.txt -boto3==1.35.68 + # via -r requirements.txt +boto3==1.35.69 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-ses # pycognito -botocore==1.35.68 +botocore==1.35.69 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # boto3 # s3transfer brotlipy==0.7.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt build==1.2.2.post1 # via pip-tools certifi==2024.8.30 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # requests # sentry-sdk cffi==1.17.1 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # brotlipy # cryptography charset-normalizer==3.4.0 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # requests click==8.1.7 # via # black # pip-tools -coverage==7.6.7 +coverage==7.6.8 # via pytest-cov croniter==3.0.4 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt cryptography==43.0.3 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # pyjwt debugpy==1.8.9 # via -r requirements-dev.in @@ -62,11 +62,11 @@ decorator==5.1.1 # via ipython diff-match-patch==20241021 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-import-export django==4.2.16 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-admin-autocomplete-filter # django-allauth # django-appconf @@ -84,60 +84,60 @@ django==4.2.16 # django-storages # django-tinymce django-admin-autocomplete-filter==0.7.1 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-admin-rangefilter==0.13.2 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-allauth==65.0.2 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-appconf==1.0.6 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-avatar django-auditlog==3.0.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-avatar==8.0.1 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-crispy-forms==2.3 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-debug-toolbar==4.4.6 # via -r requirements-dev.in django-environ==0.11.2 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-extensions==3.2.3 # via -r requirements-dev.in django-file-resubmit==0.5.2 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-guardian==2.4.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-impersonate==1.9.4 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-import-export==4.1.1 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-model-utils==5.0.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-picklefield==3.2 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-q2 django-q2==1.7.4 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-recaptcha==4.0.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-ses==4.2.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-spurl==0.6.8 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-storages==1.14.4 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt django-tinymce==4.1.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt dnspython==2.7.0 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-avatar envs==1.4 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # pycognito execnet==2.1.1 # via pytest-xdist @@ -148,16 +148,16 @@ faker==30.6.0 fancycompleter==0.9.1 # via pdbpp gevent==24.10.3 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt greenlet==3.1.1 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # gevent gunicorn==23.0.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt idna==3.10 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # requests iniconfig==2.0.0 # via pytest @@ -167,7 +167,7 @@ jedi==0.19.2 # via ipython jmespath==1.0.1 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # boto3 # botocore markupsafe==3.0.2 @@ -178,11 +178,11 @@ mypy-extensions==1.0.0 # via black oauthlib==3.2.2 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # requests-oauthlib packaging==24.2 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # black # build # gunicorn @@ -197,7 +197,7 @@ pexpect==4.9.0 # via ipython pillow==10.4.0 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-avatar pip-tools==7.4.1 # via -r requirements-dev.in @@ -208,18 +208,18 @@ pluggy==1.5.0 prompt-toolkit==3.0.48 # via ipython psutil==6.1.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt psycopg2-binary==2.9.10 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data pycognito==2024.5.1 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt pycparser==2.22 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # cffi pygments==2.18.0 # via @@ -227,7 +227,7 @@ pygments==2.18.0 # pdbpp pyjwt==2.10.0 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # pycognito pyproject-hooks==1.2.0 # via @@ -249,67 +249,69 @@ pytest-xdist==3.6.1 # via -r requirements-dev.in python-dateutil==2.9.0.post0 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # botocore # croniter # django-auditlog # faker pytz==2024.2 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # croniter requests==2.32.3 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # pycognito # requests-oauthlib requests-oauthlib==2.0.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt ruff==0.7.4 # via -r requirements-dev.in s3transfer==0.10.4 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # boto3 sentry-sdk==2.17.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt six==1.16.0 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # asttokens # blessed # django-spurl # python-dateutil sqlparse==0.5.2 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django # django-debug-toolbar stack-data==0.6.3 # via ipython tablib==3.5.0 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-import-export traitlets==5.14.3 # via # ipython # matplotlib-inline typing-extensions==4.12.2 - # via faker + # via + # faker + # ipython urllib3==2.2.3 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # botocore # requests # sentry-sdk urlobject==2.4.3 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # django-spurl wcwidth==0.2.13 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # blessed # prompt-toolkit werkzeug==3.0.6 @@ -317,16 +319,16 @@ werkzeug==3.0.6 wheel==0.45.1 # via pip-tools whitenoise==6.7.0 - # via -r /var/www/votong/backend/requirements.txt + # via -r requirements.txt wmctrl==0.5 # via pdbpp zope-event==5.0 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # gevent zope-interface==7.1.1 # via - # -r /var/www/votong/backend/requirements.txt + # -r requirements.txt # gevent # The following packages are considered to be unsafe in a requirements file: diff --git a/backend/requirements.txt b/backend/requirements.txt index 2ff87033..71504288 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --output-file=requirements.txt --strip-extras requirements.in @@ -8,12 +8,12 @@ asgiref==3.8.1 # via django blessed==1.20.0 # via -r requirements.in -boto3==1.35.68 +boto3==1.35.69 # via # -r requirements.in # django-ses # pycognito -botocore==1.35.68 +botocore==1.35.69 # via # boto3 # s3transfer From 1c120b51ef1fa9339f2a3bd3463d566c830fad32 Mon Sep 17 00:00:00 2001 From: Tudor Amariei Date: Tue, 26 Nov 2024 13:17:07 +0200 Subject: [PATCH 16/16] Refactor code --- backend/hub/utils.py | 27 ++++++++++++++++----------- backend/hub/views.py | 11 +++++------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/backend/hub/utils.py b/backend/hub/utils.py index 581bad27..3c82c009 100644 --- a/backend/hub/utils.py +++ b/backend/hub/utils.py @@ -17,17 +17,22 @@ def build_full_url(request, obj): return request.build_absolute_uri(obj.get_absolute_url()) -def decode_url_token(*, url_token=None, request=None): - if request: - if not hasattr(request.resolver_match, "captured_kwargs"): - return None - url_token = request.resolver_match.captured_kwargs.get("url_token") - - if url_token: - try: - decoded = urlsafe_base64_decode(url_token).decode().split("!!") - except ValueError: - return None +def decode_url_token_from_request(request): + if not hasattr(request.resolver_match, "captured_kwargs"): + return None + url_token = request.resolver_match.captured_kwargs.get("url_token") + + return decode_url_token(url_token=url_token) + + +def decode_url_token(url_token=None): + if not url_token: + return None + + try: + decoded = urlsafe_base64_decode(url_token).decode().split("!!") + except ValueError: + return None if len(decoded) != 3: return None diff --git a/backend/hub/views.py b/backend/hub/views.py index 59b75567..7bf041a0 100644 --- a/backend/hub/views.py +++ b/backend/hub/views.py @@ -15,7 +15,7 @@ from django.db import transaction from django.db.models import Count, Q, QuerySet from django.db.utils import IntegrityError -from django.http import JsonResponse, Http404 +from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone @@ -41,6 +41,8 @@ OrganizationUpdateForm, ) from hub.models import ( + PHASE_CHOICES, + SETTINGS_CHOICES, BlogPost, Candidate, CandidateConfirmation, @@ -50,12 +52,9 @@ Domain, FeatureFlag, Organization, - PHASE_CHOICES, - SETTINGS_CHOICES, ) +from hub.utils import decode_url_token_from_request, expiring_url from hub.workers.update_organization import update_organization -from hub.utils import expiring_url, decode_url_token - logger = logging.getLogger(__name__) @@ -1072,7 +1071,7 @@ def reset_candidate_confirmations( if request.method != "POST": return render(request, "hub/committee/delete_confirmations.html") - decoded_token = decode_url_token(request=request) + decoded_token = decode_url_token_from_request(request=request) if not decoded_token: raise Http404