From 8d47eb1bd572a20c7a0177da643c8c246484e644 Mon Sep 17 00:00:00 2001 From: Tudor Amariei Date: Fri, 27 Sep 2024 12:35:31 +0300 Subject: [PATCH 1/2] Add NGO Hub login for NGO Users --- backend/donations/admin.py | 2 +- .../management/commands/generate_donations.py | 2 +- .../commands/registration_numbers_cleanup.py | 40 +- ...s_ngo_is_social_service_viable_and_more.py | 40 ++ backend/donations/models/main.py | 58 ++- .../tests/test_ngo_cif_validation.py | 2 +- backend/donations/views/account_management.py | 10 +- backend/donations/views/api.py | 2 +- backend/donations/views/donations_download.py | 16 +- backend/donations/views/my_account.py | 2 +- backend/donations/views/ngo.py | 2 +- backend/donations/workers/__init__.py | 0 .../donations/workers/update_organization.py | 195 +++++++++ backend/importer/tasks/processor.py | 5 +- backend/locale/en/LC_MESSAGES/django.po | 323 +++++++++----- backend/locale/ro/LC_MESSAGES/django.po | 303 ++++++++----- backend/redirectioneaza/settings.py | 4 + backend/redirectioneaza/social_adapters.py | 169 ++++++-- backend/redirectioneaza/urls.py | 16 + backend/redirectioneaza/views.py | 10 + backend/requirements-dev.txt | 29 +- backend/requirements.in | 4 +- backend/requirements.txt | 23 +- .../v1/components/ngo-details-form.html | 2 +- backend/templates/v1/index.html | 410 +++++++++--------- .../v2/account/errors/app_missing.html | 11 + backend/templates/v2/account/errors/base.html | 18 + .../v2/account/errors/multiple_ngos.html | 8 + .../v2/account/errors/unknown_role.html | 8 + .../v2/account/snippets/third-party.html | 2 +- backend/users/models.py | 13 + 31 files changed, 1206 insertions(+), 523 deletions(-) create mode 100644 backend/donations/migrations/0013_rename_has_special_status_ngo_is_social_service_viable_and_more.py create mode 100644 backend/donations/workers/__init__.py create mode 100644 backend/donations/workers/update_organization.py create mode 100644 backend/redirectioneaza/views.py create mode 100644 backend/templates/v2/account/errors/app_missing.html create mode 100644 backend/templates/v2/account/errors/base.html create mode 100644 backend/templates/v2/account/errors/multiple_ngos.html create mode 100644 backend/templates/v2/account/errors/unknown_role.html diff --git a/backend/donations/admin.py b/backend/donations/admin.py index d1eb95ce..7ddb73cd 100644 --- a/backend/donations/admin.py +++ b/backend/donations/admin.py @@ -106,7 +106,7 @@ class NgoAdmin(ModelAdmin): ), ( _("Activity"), - {"fields": ("is_verified", "is_active", "is_accepting_forms", "has_special_status")}, + {"fields": ("is_verified", "is_active", "is_accepting_forms", "is_social_service_viable")}, ), ( _("Logo"), diff --git a/backend/donations/management/commands/generate_donations.py b/backend/donations/management/commands/generate_donations.py index 4b5e72c8..68284e70 100644 --- a/backend/donations/management/commands/generate_donations.py +++ b/backend/donations/management/commands/generate_donations.py @@ -107,7 +107,7 @@ def handle(self, *args, **options): "cif": ngo.registration_number, "account": ngo.bank_account.upper(), "years_checkmark": False, - "special_status": ngo.has_special_status, + "special_status": ngo.is_social_service_viable, } donor.pdf_file = File(create_pdf(donor_dict, ngo_dict)) diff --git a/backend/donations/management/commands/registration_numbers_cleanup.py b/backend/donations/management/commands/registration_numbers_cleanup.py index ce64ce4c..69a73f50 100644 --- a/backend/donations/management/commands/registration_numbers_cleanup.py +++ b/backend/donations/management/commands/registration_numbers_cleanup.py @@ -4,19 +4,28 @@ from django.core.exceptions import ValidationError from django.core.management import BaseCommand -from donations.models.main import Ngo, ngo_id_number_validator +from donations.models.main import ( + Ngo, + REGISTRATION_NUMBER_REGEX, + REGISTRATION_NUMBER_REGEX_SANS_VAT, + ngo_id_number_validator, +) class Command(BaseCommand): help = "Clean up registration numbers for all NGOs." - registration_number_pattern = r"^(RO|)\d{1,10}$" - registration_number_with_vat_id_pattern = r"^RO\d{1,10}$" - def handle(self, *args, **options): errors: List[str] = [] - # for ngo_id in Ngo.objects.filter(registration_number_valid=None).values_list("pk", flat=True): - for ngo_id in Ngo.objects.filter(registration_number_valid=False).values_list("pk", flat=True): + target_ngos = Ngo.objects.filter(registration_number_valid=None) + if target_ngos.count() == 0: + target_ngos = Ngo.objects.filter(registration_number_valid=False) + if target_ngos.count() == 0: + self.stdout.write(self.style.SUCCESS("No NGOs to clean registration numbers for.")) + + return + + for ngo_id in target_ngos.values_list("pk", flat=True): result = self.clean_ngo(ngo_id) if result["state"] != "success": @@ -47,7 +56,7 @@ def clean_ngo_registration_number(self, ngo: Ngo) -> Dict[str, str]: initial_registration_number = ngo.registration_number cleaned_registration_number = self._clean_registration_number(initial_registration_number) - if not re.match(self.registration_number_pattern, cleaned_registration_number): + if not re.match(REGISTRATION_NUMBER_REGEX, cleaned_registration_number): self.stdout.write( self.style.ERROR(f"NGO {ngo.pk} has an invalid registration number: {ngo.registration_number}") ) @@ -97,8 +106,9 @@ def clean_ngo_registration_number(self, ngo: Ngo) -> Dict[str, str]: ), } - def _clean_registration_number(self, reg_num: str) -> Optional[str]: - if re.match(self.registration_number_pattern, reg_num): + @staticmethod + def _clean_registration_number(reg_num: str) -> Optional[str]: + if re.match(REGISTRATION_NUMBER_REGEX, reg_num): return reg_num # uppercase the string and strip of any whitespace @@ -109,16 +119,18 @@ def _clean_registration_number(self, reg_num: str) -> Optional[str]: return reg_num - def _extract_vat_id(self, reg_num: str) -> Dict[str, str]: + @staticmethod + def _extract_vat_id(reg_num: str) -> Dict[str, str]: result = { "vat_id": "", "registration_number": reg_num, } - # if the registration number matches the RO########## pattern separate the VAT ID from the CUI - if re.match(self.registration_number_with_vat_id_pattern, reg_num): - result["vat_id"] = reg_num[:2] - result["registration_number"] = reg_num[2:] + if re.match(REGISTRATION_NUMBER_REGEX_SANS_VAT, reg_num): + return result + + result["vat_id"] = reg_num[:2] + result["registration_number"] = reg_num[2:] return result diff --git a/backend/donations/migrations/0013_rename_has_special_status_ngo_is_social_service_viable_and_more.py b/backend/donations/migrations/0013_rename_has_special_status_ngo_is_social_service_viable_and_more.py new file mode 100644 index 00000000..81ac299b --- /dev/null +++ b/backend/donations/migrations/0013_rename_has_special_status_ngo_is_social_service_viable_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.1.1 on 2024-09-27 09:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("donations", "0012_remove_ngo_form_url_remove_ngo_image_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="ngo", + old_name="has_special_status", + new_name="is_social_service_viable", + ), + migrations.AddField( + model_name="ngo", + name="filename_cache", + field=models.JSONField(default=dict, editable=False, verbose_name="Filename cache"), + ), + migrations.AddField( + model_name="ngo", + name="ngohub_last_update_ended", + field=models.DateTimeField(blank=True, editable=False, null=True, verbose_name="Last NGO Hub update"), + ), + migrations.AddField( + model_name="ngo", + name="ngohub_last_update_started", + field=models.DateTimeField(blank=True, editable=False, null=True, verbose_name="Last NGO Hub update"), + ), + migrations.AddField( + model_name="ngo", + name="ngohub_org_id", + field=models.IntegerField( + blank=True, db_index=True, null=True, unique=True, verbose_name="NGO Hub organization ID" + ), + ), + ] diff --git a/backend/donations/models/main.py b/backend/donations/models/main.py index db913956..472a1f4f 100644 --- a/backend/donations/models/main.py +++ b/backend/donations/models/main.py @@ -1,6 +1,7 @@ import hashlib import json import logging +import re from datetime import datetime from functools import partial from typing import Dict, List, Tuple @@ -19,6 +20,10 @@ ALL_NGO_IDS_CACHE_KEY = "ALL_NGO_IDS" FRONTPAGE_NGOS_KEY = "FRONTPAGE_NGOS" +REGISTRATION_NUMBER_REGEX = r"^([A-Z]{2}|)\d{2,10}$" +REGISTRATION_NUMBER_REGEX_SANS_VAT = r"^\d{2,10}$" +REGISTRATION_NUMBER_REGEX_WITH_VAT = r"^[A-Z]{2}\d{2,10}$" + logger = logging.getLogger(__name__) @@ -77,18 +82,21 @@ def ngo_slug_validator(value): def ngo_id_number_validator(value): - cif = "".join([char for char in value.upper() if char.isalnum()]) + reg_num: str = "".join([char for char in value.upper() if char.isalnum()]) - if cif == len(cif) * "0": + if reg_num == len(reg_num) * "0": raise ValidationError(_("The ID number cannot be all zeros")) - if value.startswith("RO"): - cif = value[2:] + if not re.match(REGISTRATION_NUMBER_REGEX, reg_num): + raise ValidationError(_("The ID number format is not valid")) + + if re.match(REGISTRATION_NUMBER_REGEX_WITH_VAT, reg_num): + reg_num = value[2:] - if not cif.isdigit(): + if not reg_num.isdigit(): raise ValidationError(_("The ID number must contain only digits")) - if 2 > len(cif) or len(cif) > 10: + if 2 > len(reg_num) or len(reg_num) > 10: raise ValidationError(_("The ID number must be between 2 and 10 digits long")) if not settings.RUN_FULL_CUI_VALIDATION: @@ -97,7 +105,7 @@ def ngo_id_number_validator(value): control_key: str = "753217532" reversed_key: List[int] = [int(digit) for digit in control_key[::-1]] - reversed_cif: List[int] = [int(digit) for digit in cif[::-1]] + reversed_cif: List[int] = [int(digit) for digit in reg_num[::-1]] cif_control_digit: int = reversed_cif.pop(0) @@ -133,6 +141,17 @@ class Ngo(models.Model): name = models.CharField(verbose_name=_("Name"), blank=False, null=False, max_length=200, db_index=True) description = models.TextField(verbose_name=_("description")) + # NGO Hub details + ngohub_org_id = models.IntegerField( + verbose_name=_("NGO Hub organization ID"), + blank=True, + null=True, + db_index=True, + unique=True, + ) + ngohub_last_update_started = models.DateTimeField(_("Last NGO Hub update"), null=True, blank=True, editable=False) + ngohub_last_update_ended = models.DateTimeField(_("Last NGO Hub update"), null=True, blank=True, editable=False) + # originally: logo logo = models.ImageField( verbose_name=_("logo"), @@ -198,13 +217,13 @@ class Ngo(models.Model): # originally: special_status # if the ngo has a special status (e.g. social ngo) they are entitled to 3.5% donation, not 2% - has_special_status = models.BooleanField(verbose_name=_("has special status"), db_index=True, default=False) + is_social_service_viable = models.BooleanField(verbose_name=_("has special status"), db_index=True, default=False) # originally: accepts_forms # if the ngo accepts to receive donation forms through email is_accepting_forms = models.BooleanField(verbose_name=_("is accepting forms"), db_index=True, default=False) - # originally: active + # originally: active — the user cannot modify this property, it is set by the admin/by the NGO Hub settings is_active = models.BooleanField(verbose_name=_("is active"), db_index=True, default=True) # url to the form that contains only the ngo's details @@ -216,6 +235,8 @@ class Ngo(models.Model): upload_to=partial(year_ngo_directory_path, "ngo-forms"), ) + filename_cache = models.JSONField(_("Filename cache"), editable=False, default=dict, blank=False, null=False) + date_created = models.DateTimeField(verbose_name=_("date created"), db_index=True, auto_now_add=timezone.now) date_updated = models.DateTimeField(verbose_name=_("date updated"), db_index=True, auto_now=timezone.now) @@ -226,6 +247,12 @@ def save(self, *args, **kwargs): is_new = self.id is None self.slug = self.slug.lower() + if self.registration_number: + uppercase_registration_number = self.registration_number + if re.match(REGISTRATION_NUMBER_REGEX_WITH_VAT, uppercase_registration_number): + self.vat_id = uppercase_registration_number[:2] + self.registration_number = uppercase_registration_number[2:] + super().save(*args, **kwargs) if is_new and settings.ENABLE_CACHE: @@ -248,6 +275,19 @@ def get_full_form_url(self): else: return "" + def activate(self): + if self.is_active: + return + self.is_active = True + self.save() + + def deactivate(self): + if not self.is_active: + return + + self.is_active = False + self.save() + @staticmethod def delete_prefilled_form(ngo_id): try: diff --git a/backend/donations/tests/test_ngo_cif_validation.py b/backend/donations/tests/test_ngo_cif_validation.py index bcf4a271..fcae5cf1 100644 --- a/backend/donations/tests/test_ngo_cif_validation.py +++ b/backend/donations/tests/test_ngo_cif_validation.py @@ -26,7 +26,7 @@ def test_validation_works_with_good_cif(cif): "6040117043197", "5010418324902", # some random test data - "XY36317167", + "XYZ36317167", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", ], ) diff --git a/backend/donations/views/account_management.py b/backend/donations/views/account_management.py index baffc9dd..6029aa48 100644 --- a/backend/donations/views/account_management.py +++ b/backend/donations/views/account_management.py @@ -106,7 +106,7 @@ def post(self, request: HttpRequest): user = authenticate(email=email, password=password) if user is not None: - login(request, user) + login(request, user, backend="django.contrib.auth.backends.ModelBackend") if user.has_perm("users.can_view_old_dashboard"): return redirect(reverse("admin-index")) @@ -122,7 +122,7 @@ def post(self, request: HttpRequest): if user and user.check_old_password(password): user.set_password(password) user.save() - login(request, user) + login(request, user, backend="django.contrib.auth.backends.ModelBackend") if user.has_perm("users.can_view_old_dashboard"): return redirect(reverse("admin-index")) return redirect(reverse("contul-meu")) @@ -161,7 +161,7 @@ def post(self, request, *args, **kwargs): user.clear_token(commit=False) user.save() - login(request, user) + login(request, user, backend="django.contrib.auth.backends.ModelBackend") return redirect(reverse("contul-meu")) @@ -237,7 +237,7 @@ def post(self, request, *args, **kwargs): ) # login the user after signup - login(request, user) + login(request, user, backend="django.contrib.auth.backends.ModelBackend") # redirect to my account return redirect(reverse("contul-meu")) @@ -271,7 +271,7 @@ def get(self, request, *args, **kwargs): # user.clear_token() pass - login(request, user) + login(request, user, backend="django.contrib.auth.backends.ModelBackend") if verification_type == "v": user.is_verified = True diff --git a/backend/donations/views/api.py b/backend/donations/views/api.py index 8038960a..87a66e13 100644 --- a/backend/donations/views/api.py +++ b/backend/donations/views/api.py @@ -129,7 +129,7 @@ def get(self, request, ngo_url, *args, **kwargs): # do not add any checkmark on this form regarding the number of years "years_checkmark": False, # "two_years": False, - "special_status": ngo.has_special_status, + "special_status": ngo.is_social_service_viable, } donor = { # we assume that ngos are looking for people with income from wages diff --git a/backend/donations/views/donations_download.py b/backend/donations/views/donations_download.py index 026459ce..08f0b146 100644 --- a/backend/donations/views/donations_download.py +++ b/backend/donations/views/donations_download.py @@ -2,13 +2,14 @@ import csv import io import logging -import math import os +import re import tempfile from datetime import datetime from typing import Any, Dict, List, Tuple from zipfile import ZIP_DEFLATED, ZipFile +import math import requests from django.conf import settings from django.core.files import File @@ -20,7 +21,7 @@ from localflavor.ro.ro_counties import COUNTIES_CHOICES from donations.models.jobs import Job, JobDownloadError, JobStatusChoices -from donations.models.main import Donor, Ngo +from donations.models.main import Donor, Ngo, REGISTRATION_NUMBER_REGEX_SANS_VAT from redirectioneaza.common.messaging import send_email logger = logging.getLogger(__name__) @@ -368,10 +369,13 @@ def _build_xml( handler.write(xml_str.encode()) -def _clean_registration_number(cif: str) -> str: - # The CIF should be added without the "RO" prefix - cif: str = cif.upper() - return cif if not cif.startswith("RO") else cif[2:] +def _clean_registration_number(reg_num: str) -> str: + # The registration number should be added without the country prefix + reg_num: str = reg_num.upper() + if re.match(REGISTRATION_NUMBER_REGEX_SANS_VAT, reg_num): + return reg_num + + return reg_num[2:] def _build_xml_header(ngo, xml_idx, zip_timestamp) -> str: diff --git a/backend/donations/views/my_account.py b/backend/donations/views/my_account.py index 25dd2526..5c0b943b 100644 --- a/backend/donations/views/my_account.py +++ b/backend/donations/views/my_account.py @@ -253,7 +253,7 @@ def post(self, request, *args, **kwargs): ngo.address = post.get("ong-adresa", "").strip() ngo.county = post.get("ong-judet", "").strip() ngo.active_region = post.get("ong-activitate", "").strip() - ngo.has_special_status = True if post.get("special-status") == "on" else False + ngo.is_social_service_viable = True if post.get("special-status") == "on" else False ngo.is_accepting_forms = True if post.get("accepts-forms") == "on" else False errors: List[str] = [] diff --git a/backend/donations/views/ngo.py b/backend/donations/views/ngo.py index 17f7201e..02ec86eb 100644 --- a/backend/donations/views/ngo.py +++ b/backend/donations/views/ngo.py @@ -332,7 +332,7 @@ def get_post_value(arg, add_to_error_list=True): "account": ngo.bank_account.upper(), "cif": ngo.registration_number, "two_years": two_years, - "special_status": ngo.has_special_status, + "special_status": ngo.is_social_service_viable, "percent": "3,5%", } diff --git a/backend/donations/workers/__init__.py b/backend/donations/workers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/donations/workers/update_organization.py b/backend/donations/workers/update_organization.py new file mode 100644 index 00000000..a468a441 --- /dev/null +++ b/backend/donations/workers/update_organization.py @@ -0,0 +1,195 @@ +import logging +import mimetypes +import tempfile +from typing import Dict, List, Optional, Union + +import requests +from django.conf import settings +from django.core.files import File +from django.utils import timezone +from django_q.tasks import async_task +from ngohub import NGOHub +from ngohub.models.organization import Organization, OrganizationGeneral +from pycognito import Cognito +from requests import Response + +from donations.models.main import Ngo +from redirectioneaza.common.cache import cache_decorator + +logger = logging.getLogger(__name__) + + +def remove_signature(s3_url: str) -> str: + """ + Extract the S3 file name without the URL signature and the directory path + """ + if s3_url: + return s3_url.split("?")[0].split("/")[-1] + else: + return "" + + +def copy_file_to_organization(ngo: Ngo, signed_file_url: str, file_type: str): + if not hasattr(ngo, file_type): + raise AttributeError(f"Organization has no attribute '{file_type}'") + + filename: str = remove_signature(signed_file_url) + if not filename and getattr(ngo, file_type): + getattr(ngo, file_type).delete() + error_message = f"ERROR: {file_type.upper()} file URL is empty, deleting the existing file." + logger.warning(error_message) + return error_message + + if not filename: + error_message = f"ERROR: {file_type.upper()} file URL is empty, but is a required field." + logger.warning(error_message) + return error_message + + if filename == ngo.filename_cache.get(file_type, ""): + logger.info(f"{file_type.upper()} file is already up to date.") + return None + + r: Response = requests.get(signed_file_url) + if r.status_code != requests.codes.ok: + logger.info(f"{file_type.upper()} file request status = {r.status_code}") + error_message = f"ERROR: Could not download {file_type} file from NGO Hub, error status {r.status_code}." + logger.warning(error_message) + return error_message + + extension: str = mimetypes.guess_extension(r.headers["content-type"]) + + # TODO: mimetypes thinks that some S3 documents are .bin files, which is useless + if extension == ".bin": + extension = "" + with tempfile.TemporaryFile() as fp: + fp.write(r.content) + fp.seek(0) + getattr(ngo, file_type).save(f"{file_type}{extension}", File(fp)) + + ngo.filename_cache[file_type] = filename + + +@cache_decorator(timeout=settings.TIMEOUT_CACHE_NORMAL, cache_key="authenticate_with_ngohub") +def authenticate_with_ngohub() -> str: + u = Cognito( + user_pool_id=settings.AWS_COGNITO_USER_POOL_ID, + client_id=settings.AWS_COGNITO_CLIENT_ID, + client_secret=settings.AWS_COGNITO_CLIENT_SECRET, + username=settings.NGOHUB_API_ACCOUNT, + user_pool_region=settings.AWS_COGNITO_REGION, + ) + u.authenticate(password=settings.NGOHUB_API_KEY) + + return u.id_token + + +def get_ngo_hub_data(ngohub_org_id: int, token: str = "") -> Organization: + hub = NGOHub(settings.NGOHUB_API_HOST) + + # if a token is already provided, use it for the profile endpoint + if token: + return hub.get_organization_profile(ngo_token=token) + + # if no token is provided, attempt to authenticate as an admin for the organization endpoint + token: str = authenticate_with_ngohub() + return hub.get_organization(organization_id=ngohub_org_id, admin_token=token) + + +def update_local_ngo_with_ngohub_data(ngo: Ngo, ngohub_ngo: Organization) -> Dict[str, Union[int, List[str]]]: + errors: List[str] = [] + + if not ngo.filename_cache: + ngo.filename_cache = {} + + ngohub_general_data: OrganizationGeneral = ngohub_ngo.general_data + + ngo.name = ngohub_general_data.alias or ngohub_general_data.name + + if not ngo.slug: + ngo.slug = ngo.name.lower().replace(" ", "-") + + if ngo.description is None: + ngo.description = ngohub_general_data.description or "" + + ngo.registration_number = ngohub_general_data.cui + + # Import the organization logo + logo_url: str = ngohub_general_data.logo + logo_url_error: Optional[str] = copy_file_to_organization(ngo, logo_url, "logo") + if logo_url_error: + errors.append(logo_url_error) + + # TODO: the county and active region have different formats here + ngo.address = ngohub_general_data.address + ngo.county = ngohub_general_data.county.name + + active_region: str = ngohub_ngo.activity_data.area + if ngohub_ngo.activity_data.area == "Regional": + regions: List[str] = [region.name for region in ngohub_ngo.activity_data.regions] + active_region = f"{ngohub_ngo.activity_data.area} ({','.join(regions)})" + elif ngohub_ngo.activity_data.area == "Local": + counties: List[str] = [city.county.name for city in ngohub_ngo.activity_data.cities] + active_region = f"{ngohub_ngo.activity_data.area} ({','.join(counties)})" + ngo.active_region = active_region + + if not ngo.phone: + ngo.phone = ngohub_general_data.contact.phone + + ngo.email = ngohub_general_data.contact.email + ngo.website = ngohub_general_data.website + + ngo.is_social_service_viable = ngohub_ngo.activity_data.is_social_service_viable + ngo.is_verified = True + + ngo.ngohub_last_update_ended = timezone.now() + ngo.save() + + task_result: Dict = { + "ngo_id": ngo.id, + "errors": errors, + } + + return task_result + + +def update_organization_process(organization_id: int, token: str = "") -> Dict[str, Union[int, List[str]]]: + """ + Update the organization with the given ID. + """ + ngo: Ngo = Ngo.objects.get(pk=organization_id) + + ngo.ngohub_last_update_started = timezone.now() + ngo.save() + + ngohub_id: int = ngo.ngohub_org_id + ngohub_org_data: Organization = get_ngo_hub_data(ngohub_id, token) + + task_result = update_local_ngo_with_ngohub_data(ngo, ngohub_org_data) + + return task_result + + +def update_organization(organization_id: int, token: str = ""): + """ + Update the organization with the given ID asynchronously. + """ + if settings.UPDATE_ORGANIZATION_METHOD == "async": + async_task(update_organization_process, organization_id, token) + else: + update_organization_process(organization_id, token) + + +def create_organization_for_user(user, ngohub_org_data: Organization) -> Ngo: + """ + Create a blank organization for the given user. + The data regarding the organization will be added from NGO Hub. + """ + ngo = Ngo(registration_number=ngohub_org_data.general_data.cui, ngohub_org_id=ngohub_org_data.id) + ngo.save() + + update_local_ngo_with_ngohub_data(ngo, ngohub_org_data) + + user.ngo = ngo + user.save() + + return ngo diff --git a/backend/importer/tasks/processor.py b/backend/importer/tasks/processor.py index 365ba6cd..058bed25 100644 --- a/backend/importer/tasks/processor.py +++ b/backend/importer/tasks/processor.py @@ -1,5 +1,6 @@ import csv import logging +import re from datetime import datetime from typing import Any, Dict, List, Optional, TypedDict, Union @@ -10,7 +11,7 @@ from django.db.models.base import ModelBase from django.utils.timezone import make_aware -from donations.models.main import Ngo +from donations.models.main import Ngo, REGISTRATION_NUMBER_REGEX from importer.models import ImportJob, ImportModelTypeChoices, ImportStatusChoices logger = logging.getLogger(__name__) @@ -142,7 +143,7 @@ def clean_bank_account(value: str) -> str: def clean_registration(value: str) -> str: value = "".join(value.split()).strip().upper() - if (value.startswith("RO") and len(value) != 10) or (not value.startswith("RO") and len(value)) != 8: + if not re.match(REGISTRATION_NUMBER_REGEX, value): logger.warning(f"Invalid registration number: {value}") return value diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 0af9a7ee..635bb881 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-09-12 13:11+0300\n" +"POT-Creation-Date: 2024-09-30 13:06+0300\n" "PO-Revision-Date: 2024-02-28 15:45+0000\n" "Last-Translator: Tudor Amariei \n" "Language-Team: English \n" "Language-Team: Romanian None: @@ -30,19 +39,32 @@ def handle_new_login(sociallogin: SocialLogin, **kwargs) -> None: common_user_init(sociallogin=sociallogin) +@receiver(social_account_updated) +def handle_existing_login(sociallogin: SocialLogin, **kwargs) -> None: + """ + Handler for the social-account-update signal, which is sent for all logins after the initial login. + + We already have a User, but we must be sure that the User also has + an Organization and schedule its data update from NGO Hub. + """ + + common_user_init(sociallogin=sociallogin) + + class UserOrgAdapter(DefaultSocialAccountAdapter): """ Authentication adapter which makes sure that each new `User` also has an `NGO` """ - def save_user(self, request: HttpRequest, sociallogin: SocialLogin, form=None) -> settings.AUTH_USER_MODEL: + def save_user(self, request: HttpRequest, sociallogin: SocialLogin, form=None) -> UserModel: """ Besides the default user creation, also mark this user as coming from NGO Hub, create a blank Organization for them, and schedule a data update from NGO Hub. """ - user: settings.AUTH_USER_MODEL = super().save_user(request, sociallogin, form) + user: UserModel = super().save_user(request, sociallogin, form) user.is_ngohub_user = True + user.verify_email = True user.save() common_user_init(sociallogin=sociallogin) @@ -50,57 +72,130 @@ def save_user(self, request: HttpRequest, sociallogin: SocialLogin, form=None) - return user -def common_user_init(sociallogin: SocialLogin) -> settings.AUTH_USER_MODEL: +def common_user_init(sociallogin: SocialLogin) -> UserModel: user = sociallogin.user if user.is_superuser: return user token: str = sociallogin.token.token - hub = NGOHub(settings.NGOHUB_API_HOST) + ngohub = NGOHub(settings.NGOHUB_API_HOST) try: - user_profile: Dict = hub.get_profile(user_token=token) + user_profile: Optional[UserProfile] = ngohub.get_profile(token) except HubHTTPException: - user_profile = {} + user.deactivate() + raise ImmediateHttpResponse(redirect(reverse("error-app-missing"))) + + user_role: str = user_profile.role + ngohub_org_id: int = user_profile.organization.id - user_role: str = user_profile.get("role", "") + if not user.first_name: + user.first_name = user_profile.name + user.save() # Check the user role from NGO Hub if user_role == settings.NGOHUB_ROLE_SUPER_ADMIN: # A super admin from NGO Hub will become a Django admin - user.first_name = user_profile.get("name", "") user.save() user.groups.add(Group.objects.get(name=RESTRICTED_ADMIN)) + elif user_role == settings.NGOHUB_ROLE_NGO_ADMIN: + if not ngohub.check_user_organization_has_application(ngo_token=token, login_link=settings.BASE_WEBSITE): + _deactivate_ngo_and_users(ngohub_org_id) + raise ImmediateHttpResponse(redirect(reverse("error-app-missing"))) + + # Add the user to the NGO admin group + user.groups.add(Group.objects.get(name=NGO_ADMIN)) - return None - - # TODO: Implement the following roles when we have the create/update organization tasks - # elif user_role == settings.NGOHUB_ROLE_NGO_ADMIN: - # if not hub.check_user_organization_has_application(ngo_token=token, login_link=settings.BASE_WEBSITE): - # if user.is_active: - # user.is_active = False - # user.save() - # - # raise ImmediateHttpResponse(redirect(reverse("error-app-missing"))) - # elif not user.is_active: - # user.is_active = True - # user.save() - # - # # Add the user to the NGO group - # user.groups.add(Group.objects.get(name=NGO_ADMIN)) - # - # org = Ngo.objects.filter(user=user).first() - # if not org: - # org = create_blank_org(user) - # - # return org - # - # elif user_role == settings.NGOHUB_ROLE_NGO_EMPLOYEE: - # # Employees cannot have organizations - # raise ImmediateHttpResponse(redirect(reverse("error-user-role"))) + _connect_user_and_ngo(user, ngohub_org_id, token) + elif user_role == settings.NGOHUB_ROLE_NGO_EMPLOYEE: + if not ngohub.check_user_organization_has_application(ngo_token=token, login_link=settings.BASE_WEBSITE): + user.deactivate() + raise ImmediateHttpResponse(redirect(reverse("error-app-missing"))) + # Add the user to the NGO admin group + user.groups.add(Group.objects.get(name=NGO_MEMBER)) + + _connect_user_and_ngo(user, ngohub_org_id, token) else: # Unknown user role raise ImmediateHttpResponse(redirect(reverse("error-unknown-user-role"))) + + return user + + +def _connect_user_and_ngo(user, ngohub_org_id, token): + # Make sure the user is active + user.activate() + + user_ngo: Ngo = _get_or_create_user_ngo(user, ngohub_org_id, token) + + # Make sure the organization is active + user_ngo.activate() + + user.ngo = user_ngo + user.save() + + +def _deactivate_ngo_and_users(ngohub_org_id: int) -> None: + # Deactivate users if their organization does not have the application + try: + user_ngo: Ngo = Ngo.objects.get(ngohub_org_id=ngohub_org_id) + user_ngo.deactivate() + + for user in UserModel.objects.filter(ngo=user_ngo): + user.deactivate() + except Ngo.DoesNotExist: + pass + + +def _get_or_create_user_ngo(user: UserModel, ngohub_org_id: int, token: str) -> Ngo: + """ + If the user does not have an organization, create one with the user as the owner and the data from NGO Hub + """ + try: + # NGO exists and has already been imported from NGO Hub + user_ngo = Ngo.objects.get(ngohub_org_id=ngohub_org_id) + except Ngo.DoesNotExist: + hub: NGOHub = NGOHub(settings.NGOHUB_API_HOST) + + ngohub_org_data: Organization = hub.get_organization_profile(ngo_token=token) + ngo_registration_number: str = ngohub_org_data.general_data.cui + + registration_number_choices = [ngo_registration_number.upper()] + if re.match(r"[A-Z]{2}\d{2,10}", ngo_registration_number): + registration_number_choices.append(ngo_registration_number[2:]) + + # Check if the NGO already exists in the database by its registration number + user_ngo_queryset = Ngo.objects.filter(registration_number__in=registration_number_choices) + if user_ngo_queryset.exists(): + if user_ngo_queryset.count() > 1: + # If there are multiple NGOs with the same registration number, raise an error and notify admins + user_email = user.email + user_pk = user.pk + + _raise_error_multiple_ngos(ngo_registration_number, ngohub_org_id, user_email, user_pk) + + # If there is only one NGO with the registration number, connect the user to it + user_ngo: Ngo = user_ngo_queryset.first() + user_ngo.ngohub_org_id = ngohub_org_id + user_ngo.save() + else: + # If the NGO does not exist in the database, create it + user_ngo: Ngo = create_organization_for_user(user, ngohub_org_data) + + return user_ngo + + +def _raise_error_multiple_ngos(ngo_registration_number, ngohub_org_id, user_email, user_pk): + error_message = ( + f"Multiple organizations with the same registration {ngo_registration_number} found " + f"for organization with ngohub_org_id {ngohub_org_id} " + f"and user {user_email} – {user_pk}" + ) + logger.error(error_message) + if settings.ENABLE_SENTRY: + capture_message(error_message, level="error") + + raise ImmediateHttpResponse(redirect(reverse("error-multiple-organizations"))) diff --git a/backend/redirectioneaza/urls.py b/backend/redirectioneaza/urls.py index 643b479e..b5cec324 100644 --- a/backend/redirectioneaza/urls.py +++ b/backend/redirectioneaza/urls.py @@ -60,6 +60,7 @@ PolicyHandler, TermsHandler, ) +from redirectioneaza.views import StaticPageView admin.site.site_header = f"Admin | {settings.VERSION_SUFFIX}" @@ -103,6 +104,21 @@ path("organizatia/", NgoDetailsView.as_view(), name="organization"), path("asociatia/", RedirectView.as_view(pattern_name="organization", permanent=True)), path("date-cont/", MyAccountDetailsView.as_view(), name="date-contul-meu"), + path( + "contul-meu/eroare/aplicatie-lipsa/", + StaticPageView.as_view(template_name="account/errors/app_missing.html"), + name="error-app-missing", + ), + path( + "contul-meu/eroare/sincronizare-ong/", + StaticPageView.as_view(template_name="account/errors/multiple_ngos.html"), + name="error-multiple-organizations", + ), + path( + "contul-meu/eroare/rol-necunoscut/", + StaticPageView.as_view(template_name="account/errors/unknown_role.html"), + name="error-unknown-user-role", + ), # APIs path("api/ngo/check-url//", CheckNgoUrl.as_view(), name="api-ngo-check-url"), path("api/ngos/", NgosApi.as_view(), name="api-ngos"), diff --git a/backend/redirectioneaza/views.py b/backend/redirectioneaza/views.py new file mode 100644 index 00000000..14f40049 --- /dev/null +++ b/backend/redirectioneaza/views.py @@ -0,0 +1,10 @@ +from django.views.generic.base import TemplateView + + +class StaticPageView(TemplateView): + template_name = "" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + return context diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 5d4dc242..e61eca0f 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.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-dev.txt --strip-extras requirements-dev.in @@ -12,11 +12,12 @@ black==24.8.0 # via -r requirements-dev.in blessed==1.20.0 # via -r requirements.txt -boto3==1.35.27 +boto3==1.35.28 # via # -r requirements.txt # django-ses -botocore==1.35.27 + # pycognito +botocore==1.35.28 # via # -r requirements.txt # boto3 @@ -49,7 +50,9 @@ coverage==7.6.1 croniter==3.0.3 # via -r requirements.txt cryptography==43.0.1 - # via -r requirements.txt + # via + # -r requirements.txt + # pyjwt cssselect2==0.7.0 # via # -r requirements.txt @@ -89,6 +92,10 @@ django-storages==1.14.4 # via -r requirements.txt django-unfold==0.39.0 # via -r requirements.txt +envs==1.4 + # via + # -r requirements.txt + # pycognito execnet==2.1.1 # via pytest-xdist faker==28.4.1 @@ -124,7 +131,7 @@ markupsafe==2.1.5 # jinja2 mypy-extensions==1.0.0 # via black -ngohub==0.0.7 +ngohub==0.0.8 # via -r requirements.txt packaging==24.1 # via @@ -149,10 +156,16 @@ psutil==6.0.0 # via -r requirements.txt psycopg2-binary==2.9.9 # via -r requirements.txt +pycognito==2024.5.1 + # via -r requirements.txt pycparser==2.22 # via # -r requirements.txt # cffi +pyjwt==2.9.0 + # via + # -r requirements.txt + # pycognito pymysql==1.1.1 # via -r requirements.txt pypdf==4.3.1 @@ -196,8 +209,10 @@ reportlab==4.2.2 # -r requirements.txt # svglib requests==2.32.3 - # via -r requirements.txt -ruff==0.6.7 + # via + # -r requirements.txt + # pycognito +ruff==0.6.8 # via -r requirements-dev.in s3transfer==0.10.2 # via diff --git a/backend/requirements.in b/backend/requirements.in index f175f7f7..89dc4e9c 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -4,11 +4,13 @@ django-ipware~=7.0.1 django-unfold~=0.39.0 django-allauth~=64.2.1 +pycognito~=2024.5.1 + # captcha django-recaptcha~=4.0.0 # NGO Hub integration -ngohub~=0.0.6 +ngohub~=0.0.8 # encrypting data cryptography~=43.0.1 diff --git a/backend/requirements.txt b/backend/requirements.txt index f1f19ce8..ed40d2b9 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,11 +8,12 @@ asgiref==3.8.1 # via django blessed==1.20.0 # via -r requirements.in -boto3==1.35.27 +boto3==1.35.28 # via # django-ses # django-storages -botocore==1.35.27 + # pycognito +botocore==1.35.28 # via # boto3 # s3transfer @@ -29,7 +30,9 @@ charset-normalizer==3.3.2 croniter==3.0.3 # via -r requirements.in cryptography==43.0.1 - # via -r requirements.in + # via + # -r requirements.in + # pyjwt cssselect2==0.7.0 # via svglib django==5.1.1 @@ -64,6 +67,8 @@ django-storages==1.14.4 # via -r requirements.in django-unfold==0.39.0 # via -r requirements.in +envs==1.4 + # via pycognito gevent==24.2.1 # via -r requirements.in greenlet==3.1.1 @@ -82,7 +87,7 @@ lxml==5.3.0 # via svglib markupsafe==2.1.5 # via jinja2 -ngohub==0.0.7 +ngohub==0.0.8 # via -r requirements.in packaging==24.1 # via gunicorn @@ -92,8 +97,12 @@ psutil==6.0.0 # via -r requirements.in psycopg2-binary==2.9.9 # via -r requirements.in +pycognito==2024.5.1 + # via -r requirements.in pycparser==2.22 # via cffi +pyjwt==2.9.0 + # via pycognito pymysql==1.1.1 # via -r requirements.in pypdf==4.3.1 @@ -113,7 +122,9 @@ reportlab==4.2.2 # -r requirements.in # svglib requests==2.32.3 - # via -r requirements.in + # via + # -r requirements.in + # pycognito s3transfer==0.10.2 # via boto3 sentry-sdk==2.14.0 diff --git a/backend/templates/v1/components/ngo-details-form.html b/backend/templates/v1/components/ngo-details-form.html index 38f354d5..a81c4fb0 100644 --- a/backend/templates/v1/components/ngo-details-form.html +++ b/backend/templates/v1/components/ngo-details-form.html @@ -80,7 +80,7 @@
- +
diff --git a/backend/templates/v1/index.html b/backend/templates/v1/index.html index 9989e697..613f472c 100644 --- a/backend/templates/v1/index.html +++ b/backend/templates/v1/index.html @@ -1,49 +1,49 @@ {% extends "base.html" %} {% block assets %} - + {% endblock %} {% block header %} - {% if not custom_subdomain %} - {{ super() }} - {% endif %} + {% if not custom_subdomain %} + {{ super() }} + {% endif %} {% endblock %} {% block content %} -
- -
-
- Logo of the platform -
- {% if company_name %} - {# we have a custom header for some companies #} - {% if custom_header %} -

- Redirecționează 3.5% din impozitul tău
- către o cauză la alegere -

- {% else %} -

- Redirecționează 3.5% din impozitul tău
- către unul dintre ONG-urile susținute de {{ company_name }}. -

- {% endif %} - {% else %} -

- Nu te costă nimic să faci bine! -
- Redirecționează 3,5% din impozitul tău către ONG-ul în care crezi. -

- {% endif %} - -

+
+ +
+
+ Logo of the platform +
+ {% if company_name %} + {# we have a custom header for some companies #} + {% if custom_header %} +

+ Redirecționează 3.5% din impozitul tău
+ către o cauză la alegere +

+ {% else %} +

+ Redirecționează 3.5% din impozitul tău
+ către unul dintre ONG-urile susținute de {{ company_name }}. +

+ {% endif %} + {% else %} +

+ Nu te costă nimic să faci bine! +
+ Redirecționează 3,5% din impozitul tău către ONG-ul în care crezi. +

+ {% endif %} + +

Cu doar câteva minute din timpul tău, poți fi parte din transformarea României într-o societate mai solidară. @@ -54,190 +54,192 @@

Poți depune Declarația 230 până pe {{ limit.day }} {{ month_limit }} {{ limit.year }}. -

- {% if custom_note %} -

- Dacă ONG-ul tău preferat nu se află pe această pagină,
- consultă lista completă de organizații pe redirectioneaza.ro. -

- {% endif %} - - {% if not custom_subdomain %} -
-
- -
-
- {% endif %} -

- -
- {% for ngo in ngos %} - - {% endfor %} - {% if not custom_subdomain %} - - {% endif %} +

+ {% if custom_note %} +

+ Dacă ONG-ul tău preferat nu se află pe această pagină,
+ consultă lista completă de organizații pe redirectioneaza.ro. +

+ {% endif %} + + {% if not custom_subdomain %} +
+
+ +
+ {% endif %} +
- {% if stats %} -
-
-

{{ stats["ngos"] }}

- organizații înscrise în platformă -
-
-

{{ stats["forms"] }}

- formulare completate în {{ current_year }} -
-
-

- peste 2.000.000 EUR -

- redirecționați spre ONG-uri în {{ current_year }} +
+ {% for ngo in ngos %} + {% if ngo.slug %} + {% endif %} - -
-
-

Cum funcționează redirectioneaza.ro?

-
    -
  1. -
    -
    - -
    -
    -

    - Completează Declarația 230 de pe pagina unui - ONG, - apoi alege una dintre opțiunile disponibile: -

    - -

    - Fie o semnezi online, în browser, apoi declarația este trimisă din platformă - direct pe adresa de e-mail a organizației. -
    - Fie o descarci, printezi, semnezi și o trimiți pe e-mail, prin curier sau poștă, - la adresa ONG-ului. Apoi ei se ocupă de depunerea declarației. Adresele le vei - găsi în paginile fiecărui ONG. -

    -
    -
    -
  2. -
  3. -
    -
    - -
    -
    -

    - „Nu pot redirecționa 3,5% din impozit. Pot ajuta altfel?” -

    -

    - Poți susține ONG-urile și povestind familiei și prietenilor despre redirecționare. - Descarcă Declarația 230 precompletată de pe pagina ONG-ului pe care vrei să îl - susții, printează în mai multe exemplare și transmite-o colegilor, prietenilor și - familiei. -

    -
    -
    -
  4. -
-
+ {% endfor %} + {% if not custom_subdomain %} + + {% endif %} +
-
+ {% if stats %} +
+
+

{{ stats["ngos"] }}

+ organizații înscrise în platformă +
+
+

{{ stats["forms"] }}

+ formulare completate în {{ current_year }} +
+
+

+ peste 2.000.000 EUR +

+ redirecționați spre ONG-uri în {{ current_year }} +
+
+ {% endif %} -
-
-

- Nu subestima puterea pe care o ai! -
- Poți influența direcția în care se dezvoltă România. +
+
+

Cum funcționează redirectioneaza.ro?

+
    +
  1. +
    +
    + +
    +
    +

    + Completează Declarația 230 de pe pagina unui + ONG, + apoi alege una dintre opțiunile disponibile:

    -
    -
    - -
    -
    -

    Ce este Declarația 230?

    -

    - Declarația 230 este metoda prin care orice persoană poate redirecționa până la 3,5% din - impozitul anual către un ONG. Astfel suma respectivă nu mai ajunge în bugetul statului, ci - este direcționată către contul ONG-ului. Cu doar o semnătură și o declarație depusă la timp, - poți ajuta ajuta o cauză în care crezi. -

    -

    - Distribuirea sumei poate fi solicitată pentru aceiași beneficiari pentru o perioadă de unul - sau doi ani. Declarația poate fi reînnoită după expirarea perioadei respective. -

    -
    -
    + +

    + Fie o semnezi online, în browser, apoi declarația este trimisă din platformă + direct pe adresa de e-mail a organizației. +
    + Fie o descarci, printezi, semnezi și o trimiți pe e-mail, prin curier sau poștă, + la adresa ONG-ului. Apoi ei se ocupă de depunerea declarației. Adresele le vei + găsi în paginile fiecărui ONG. +

    +
    -
    -
    -
    - -
    -
    -

    De ce este important să depui declarația?

    - -

    - - este dreptul tău de a decide în ce mod se pot utiliza acești bani; -
    - - îți oferă posibilitatea de a susține recurent o cauză în care crezi; -
    - - ONG-urile se bazează pe ajutorul comunității și au nevoie de tine ca să își poată susține - activitatea în beneficiul societății; -
    - - 2 milioane de euro anual s-ar putea strânge pentru a sprijini binele făcut de - organizațiile neguvernamentale. -

    -
    -
    +
  2. +
  3. +
    +
    + +
    +
    +

    + „Nu pot redirecționa 3,5% din impozit. Pot ajuta altfel?” +

    +

    + Poți susține ONG-urile și povestind familiei și prietenilor despre redirecționare. + Descarcă Declarația 230 precompletată de pe pagina ONG-ului pe care vrei să îl + susții, printează în mai multe exemplare și transmite-o colegilor, prietenilor și + familiei. +

    +
    +
  4. +
+
+
+ +
+ +
+
+

+ Nu subestima puterea pe care o ai! +
+ Poți influența direcția în care se dezvoltă România. +

+
+
+ +
+
+

Ce este Declarația 230?

+

+ Declarația 230 este metoda prin care orice persoană poate redirecționa până la 3,5% din + impozitul anual către un ONG. Astfel suma respectivă nu mai ajunge în bugetul statului, ci + este direcționată către contul ONG-ului. Cu doar o semnătură și o declarație depusă la timp, + poți ajuta ajuta o cauză în care crezi. +

+

+ Distribuirea sumei poate fi solicitată pentru aceiași beneficiari pentru o perioadă de unul + sau doi ani. Declarația poate fi reînnoită după expirarea perioadei respective. +

+
+
+
+
+
+ +
+
+

De ce este important să depui declarația?

+ +

+ - este dreptul tău de a decide în ce mod se pot utiliza acești bani; +
+ - îți oferă posibilitatea de a susține recurent o cauză în care crezi; +
+ - ONG-urile se bazează pe ajutorul comunității și au nevoie de tine ca să își poată susține + activitatea în beneficiul societății; +
+ - 2 milioane de euro anual s-ar putea strânge pentru a sprijini binele făcut de + organizațiile neguvernamentale. +

+
+
+
+
-
+
- {% if not custom_subdomain %} - {% include "components/for-ngos.html" %} - {% endif %} + {% if not custom_subdomain %} + {% include "components/for-ngos.html" %} + {% endif %} -

+
{% endblock %} {% block scripts %} - - + + {% endblock %} diff --git a/backend/templates/v2/account/errors/app_missing.html b/backend/templates/v2/account/errors/app_missing.html new file mode 100644 index 00000000..3085c40d --- /dev/null +++ b/backend/templates/v2/account/errors/app_missing.html @@ -0,0 +1,11 @@ +{% extends 'account/errors/base.html' %} +{% load i18n %} + +{% block content %} +

+ {% trans 'Application not activated' %} +

+

+ {% trans 'The application is not activated for this user/organization.' %} +

+{% endblock %} diff --git a/backend/templates/v2/account/errors/base.html b/backend/templates/v2/account/errors/base.html new file mode 100644 index 00000000..8ffd2e1f --- /dev/null +++ b/backend/templates/v2/account/errors/base.html @@ -0,0 +1,18 @@ +{% extends 'redirect/base.html' %} +{% load static %} +{% load i18n %} + + +{% block title %}{% trans 'Sign In Error' %}{% endblock %} + +{% block left-side-view %} +
+

+ {% trans 'Sign In Error' %} +

+ + {% block error_message %} +

+ {% endblock %} +
+{% endblock %} diff --git a/backend/templates/v2/account/errors/multiple_ngos.html b/backend/templates/v2/account/errors/multiple_ngos.html new file mode 100644 index 00000000..efa18094 --- /dev/null +++ b/backend/templates/v2/account/errors/multiple_ngos.html @@ -0,0 +1,8 @@ +{% extends 'account/errors/base.html' %} +{% load i18n %} + +{% block error_message %} +

+ {% trans 'Multiple NGOs have been found during synchronization. Admins have been notified and will resolve the issue shortly.'%} +

+{% endblock %} diff --git a/backend/templates/v2/account/errors/unknown_role.html b/backend/templates/v2/account/errors/unknown_role.html new file mode 100644 index 00000000..9778226b --- /dev/null +++ b/backend/templates/v2/account/errors/unknown_role.html @@ -0,0 +1,8 @@ +{% extends 'account/errors/base.html' %} +{% load i18n %} + +{% block error_message %} +

+ {% trans "Your current role isn't supported." %} +

+{% endblock %} diff --git a/backend/templates/v2/account/snippets/third-party.html b/backend/templates/v2/account/snippets/third-party.html index ba2b5367..0ca1f38b 100644 --- a/backend/templates/v2/account/snippets/third-party.html +++ b/backend/templates/v2/account/snippets/third-party.html @@ -23,7 +23,7 @@

{% endif %}

diff --git a/backend/users/models.py b/backend/users/models.py index 7327f858..e0d6e675 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -126,6 +126,19 @@ def clear_token(self, commit=True): if commit: self.save() + def activate(self): + if self.is_active: + return + self.is_active = True + self.save() + + def deactivate(self): + if not self.is_active: + return + + self.is_active = False + self.save() + @staticmethod def old_hash_password(password, method, salt=None, pepper=None): """ From 2ab88a3cab03ab917ca5bb85e959f393cbc0f9ee Mon Sep 17 00:00:00 2001 From: Tudor Amariei Date: Tue, 1 Oct 2024 10:54:27 +0300 Subject: [PATCH 2/2] fix bugs & tweak stuff --- backend/donations/models/main.py | 14 ++++++++++---- backend/donations/views/account_management.py | 16 +++++++++++----- backend/redirectioneaza/social_adapters.py | 2 -- backend/users/models.py | 13 +++++++++---- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/backend/donations/models/main.py b/backend/donations/models/main.py index 472a1f4f..45af20e4 100644 --- a/backend/donations/models/main.py +++ b/backend/donations/models/main.py @@ -165,6 +165,7 @@ class Ngo(models.Model): bank_account = models.CharField(verbose_name=_("bank account"), max_length=100) # originally: cif + # TODO: the number's length should be between 2 and 10 (or 8) registration_number = models.CharField( verbose_name=_("registration number"), max_length=100, @@ -275,18 +276,23 @@ def get_full_form_url(self): else: return "" - def activate(self): + def activate(self, commit: bool = True): if self.is_active: return + self.is_active = True - self.save() - def deactivate(self): + if commit: + self.save() + + def deactivate(self, commit: bool = True): if not self.is_active: return self.is_active = False - self.save() + + if commit: + self.save() @staticmethod def delete_prefilled_form(ngo_id): diff --git a/backend/donations/views/account_management.py b/backend/donations/views/account_management.py index 6029aa48..01db07b9 100644 --- a/backend/donations/views/account_management.py +++ b/backend/donations/views/account_management.py @@ -18,6 +18,10 @@ logger = logging.getLogger(__name__) +def django_login(request, user) -> None: + login(request, user, backend="django.contrib.auth.backends.ModelBackend") + + class ForgotPasswordView(BaseAccountView): template_name = "resetare-parola.html" @@ -106,7 +110,7 @@ def post(self, request: HttpRequest): user = authenticate(email=email, password=password) if user is not None: - login(request, user, backend="django.contrib.auth.backends.ModelBackend") + django_login(request, user) if user.has_perm("users.can_view_old_dashboard"): return redirect(reverse("admin-index")) @@ -122,7 +126,7 @@ def post(self, request: HttpRequest): if user and user.check_old_password(password): user.set_password(password) user.save() - login(request, user, backend="django.contrib.auth.backends.ModelBackend") + django_login(request, user) if user.has_perm("users.can_view_old_dashboard"): return redirect(reverse("admin-index")) return redirect(reverse("contul-meu")) @@ -161,7 +165,7 @@ def post(self, request, *args, **kwargs): user.clear_token(commit=False) user.save() - login(request, user, backend="django.contrib.auth.backends.ModelBackend") + django_login(request, user) return redirect(reverse("contul-meu")) @@ -237,7 +241,8 @@ def post(self, request, *args, **kwargs): ) # login the user after signup - login(request, user, backend="django.contrib.auth.backends.ModelBackend") + django_login(request, user) + # redirect to my account return redirect(reverse("contul-meu")) @@ -271,7 +276,7 @@ def get(self, request, *args, **kwargs): # user.clear_token() pass - login(request, user, backend="django.contrib.auth.backends.ModelBackend") + django_login(request, user) if verification_type == "v": user.is_verified = True @@ -285,4 +290,5 @@ def get(self, request, *args, **kwargs): return render(request, self.template_name, context) logger.info("verification type not supported") + raise Http404 diff --git a/backend/redirectioneaza/social_adapters.py b/backend/redirectioneaza/social_adapters.py index a0bf0926..a9081640 100644 --- a/backend/redirectioneaza/social_adapters.py +++ b/backend/redirectioneaza/social_adapters.py @@ -97,8 +97,6 @@ def common_user_init(sociallogin: SocialLogin) -> UserModel: # Check the user role from NGO Hub if user_role == settings.NGOHUB_ROLE_SUPER_ADMIN: # A super admin from NGO Hub will become a Django admin - user.save() - user.groups.add(Group.objects.get(name=RESTRICTED_ADMIN)) elif user_role == settings.NGOHUB_ROLE_NGO_ADMIN: if not ngohub.check_user_organization_has_application(ngo_token=token, login_link=settings.BASE_WEBSITE): diff --git a/backend/users/models.py b/backend/users/models.py index e0d6e675..3df58805 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -126,18 +126,23 @@ def clear_token(self, commit=True): if commit: self.save() - def activate(self): + def activate(self, commit: bool = True): if self.is_active: return + self.is_active = True - self.save() - def deactivate(self): + if commit: + self.save() + + def deactivate(self, commit: bool = True): if not self.is_active: return self.is_active = False - self.save() + + if commit: + self.save() @staticmethod def old_hash_password(password, method, salt=None, pepper=None):