Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NGO Hub login for NGO Users #324

Merged
merged 2 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/donations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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}")
)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
),
),
]
64 changes: 55 additions & 9 deletions backend/donations/models/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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__)


Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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"),
Expand All @@ -146,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,
Expand Down Expand Up @@ -198,13 +218,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
Expand All @@ -216,6 +236,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)

Expand All @@ -226,6 +248,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:
Expand All @@ -248,6 +276,24 @@ def get_full_form_url(self):
else:
return ""

def activate(self, commit: bool = True):
if self.is_active:
return

self.is_active = True

if commit:
self.save()

def deactivate(self, commit: bool = True):
if not self.is_active:
return

self.is_active = False

if commit:
self.save()

@staticmethod
def delete_prefilled_form(ngo_id):
try:
Expand Down
2 changes: 1 addition & 1 deletion backend/donations/tests/test_ngo_cif_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_validation_works_with_good_cif(cif):
"6040117043197",
"5010418324902",
# some random test data
"XY36317167",
"XYZ36317167",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
],
)
Expand Down
16 changes: 11 additions & 5 deletions backend/donations/views/account_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -106,7 +110,7 @@ def post(self, request: HttpRequest):

user = authenticate(email=email, password=password)
if user is not None:
login(request, user)
django_login(request, user)
if user.has_perm("users.can_view_old_dashboard"):
return redirect(reverse("admin-index"))

Expand All @@ -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)
django_login(request, user)
if user.has_perm("users.can_view_old_dashboard"):
return redirect(reverse("admin-index"))
return redirect(reverse("contul-meu"))
Expand Down Expand Up @@ -161,7 +165,7 @@ def post(self, request, *args, **kwargs):
user.clear_token(commit=False)
user.save()

login(request, user)
django_login(request, user)

return redirect(reverse("contul-meu"))

Expand Down Expand Up @@ -237,7 +241,8 @@ def post(self, request, *args, **kwargs):
)

# login the user after signup
login(request, user)
django_login(request, user)

# redirect to my account
return redirect(reverse("contul-meu"))

Expand Down Expand Up @@ -271,7 +276,7 @@ def get(self, request, *args, **kwargs):
# user.clear_token()
pass

login(request, user)
django_login(request, user)

if verification_type == "v":
user.is_verified = True
Expand All @@ -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
2 changes: 1 addition & 1 deletion backend/donations/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading