diff --git a/backend/donations/admin/common.py b/backend/donations/admin/common.py index 6c268de75..659d37f29 100644 --- a/backend/donations/admin/common.py +++ b/backend/donations/admin/common.py @@ -24,7 +24,7 @@ class CommonCauseFields: {"fields": ("ngo",)}, ) - flags_fieldset: tuple[str, dict[str, tuple[str]]] = ( + flags_fieldset = ( _("Flags"), { "fields": ( @@ -36,7 +36,7 @@ class CommonCauseFields: }, ) - form_data_fieldset: tuple[str, dict[str, tuple[str]]] = ( + form_data_fieldset = ( _("Form Data"), { "fields": ( @@ -46,7 +46,7 @@ class CommonCauseFields: }, ) - data_fieldset: tuple[str, dict[str, tuple[str]]] = ( + data_fieldset = ( _("Data"), { "fields": ( @@ -58,7 +58,7 @@ class CommonCauseFields: }, ) - dates_fieldset: tuple[str, dict[str, tuple[str]]] = ( + dates_fieldset = ( _("Date"), { "fields": ( @@ -70,7 +70,7 @@ class CommonCauseFields: readonly_fields = ("ngo", "date_created", "date_updated") - def get_readonly_fields(self, _: HttpRequest, obj=None): + def get_readonly_fields(self, request: HttpRequest, obj=None): if obj and not obj.has_online_tax_account: return self.readonly_fields + ("allow_online_collection",) diff --git a/backend/donations/admin/donors.py b/backend/donations/admin/donors.py index 5251903ff..690e05a44 100644 --- a/backend/donations/admin/donors.py +++ b/backend/donations/admin/donors.py @@ -405,7 +405,7 @@ def run_anonymize_donation_task(selected_donor: Donor) -> dict[str, str]: }, ) - return redirect(reverse_lazy("admin:donations_donor_change", args=[donor.pk])) + # return redirect(reverse_lazy("admin:donations_donor_change", args=[donor.pk])) @action(description=_("Schedule redirections stats"), url_path="schedule-redirections-stats-generator") def run_redirections_stats_generator(self, request): diff --git a/backend/donations/admin/ngos.py b/backend/donations/admin/ngos.py index ca84de53d..f3733617e 100644 --- a/backend/donations/admin/ngos.py +++ b/backend/donations/admin/ngos.py @@ -88,7 +88,7 @@ def has_delete_permission(self, request, obj=...): class NgoPartnerInline(TabularInline): # noinspection PyUnresolvedReferences - model = Ngo.partners.through + model = Ngo.partners.through # type: ignore extra = 1 tab = True @@ -515,7 +515,7 @@ def update_from_ngohub_async(self, request: HttpRequest, queryset: QuerySet[Ngo] def check_cult_registry_sync(self, request: HttpRequest, queryset: QuerySet[Ngo]): show_errors: bool = True - registration_numbers = queryset.values_list("registration_number", flat=True) + registration_numbers: list[str] = queryset.values_list("registration_number", flat=True) # type: ignore task_result = cult_registry_check_organizations(registration_numbers, update_method="sync") message = "ANAF Registry Results: | " @@ -533,7 +533,7 @@ def check_cult_registry_sync(self, request: HttpRequest, queryset: QuerySet[Ngo] @action(description=_("Check in ANAF Cult Registry asynchronously")) def check_cult_registry_async(self, request, queryset: QuerySet[Ngo]): - registration_numbers = queryset.values_list("registration_number", flat=True) + registration_numbers: list[str] = queryset.values_list("registration_number", flat=True) # type: ignore cult_registry_check_organizations(registration_numbers, update_method="async") self.message_user(request, _("NGOs are being searched in ANAF Cult Registry.")) diff --git a/backend/donations/forms/ngo_account.py b/backend/donations/forms/ngo_account.py index 441d7ef06..51f8aabf5 100644 --- a/backend/donations/forms/ngo_account.py +++ b/backend/donations/forms/ngo_account.py @@ -139,7 +139,7 @@ def save(self, commit=True): return cause def clean_slug(self): - slug = self.cleaned_data.get("slug").lower() + slug = self.cleaned_data.get("slug", "").lower() ngo_slug_validator(slug) @@ -155,7 +155,7 @@ def clean_slug(self): return slug def clean_description(self): - return self.cleaned_data.get("description").strip() + return self.cleaned_data.get("description", "").strip() def clean_bank_account(self): bank_account = self.cleaned_data.get("bank_account") diff --git a/backend/donations/management/commands/generate_orgs.py b/backend/donations/management/commands/generate_orgs.py index 1bd7d001d..fd276096f 100644 --- a/backend/donations/management/commands/generate_orgs.py +++ b/backend/donations/management/commands/generate_orgs.py @@ -1,5 +1,4 @@ import random -from typing import Any from django.contrib.auth import get_user_model from django.core.management import BaseCommand @@ -803,7 +802,7 @@ def handle(self, *args, **options): create_user_only = options.get("user_only", None) create_ngohub_id = options.get("ngohub", None) - organizations: list[dict[str, Any]] = [] + organizations: list[Ngo] = [] generated_organization_names: list[str] = [] user_model = get_user_model() diff --git a/backend/donations/management/commands/generate_other_causes.py b/backend/donations/management/commands/generate_other_causes.py index b4d33ea15..92a877604 100644 --- a/backend/donations/management/commands/generate_other_causes.py +++ b/backend/donations/management/commands/generate_other_causes.py @@ -258,10 +258,10 @@ def handle(self, *args, **options): consecutive_identical_names: int = 0 while len(causes) < total_causes: if target_org: - ngo: Ngo = target_org + ngo: Ngo | None = target_org else: - ngo: Ngo = Ngo.active.order_by("?").first() - if not ngo.can_create_causes: + ngo: Ngo | None = Ngo.active.order_by("?").first() + if not ngo or not ngo.can_create_causes: continue cause_title = MOCK_CAUSE_NAMES["titles"][random.randint(0, len(MOCK_CAUSE_NAMES["titles"]) - 1)] diff --git a/backend/donations/management/commands/generate_stats.py b/backend/donations/management/commands/generate_stats.py index 866e55c2f..73bcdf10b 100644 --- a/backend/donations/management/commands/generate_stats.py +++ b/backend/donations/management/commands/generate_stats.py @@ -23,7 +23,7 @@ def add_arguments(self, parser): def handle(self, *args, **options): statistic_type: str = options["statistic"] - for_date_str: str = options.get("date") + for_date_str: str = options.get("date", "") if statistic_type == StatsChoices.REDIRECTIONS_PER_DAY and not for_date_str: self.stderr.write("Error: --date argument is required for REDIRECTIONS_PER_DAY statistic.") diff --git a/backend/donations/models/downloads.py b/backend/donations/models/downloads.py index df4520304..3b9a2947b 100644 --- a/backend/donations/models/downloads.py +++ b/backend/donations/models/downloads.py @@ -23,7 +23,7 @@ class RedirectionsDownloadJob(AsyncJob): null=True, ) - class Meta: + class Meta: # type: ignore verbose_name = _("download job") verbose_name_plural = _("download jobs") diff --git a/backend/donations/models/jobs.py b/backend/donations/models/jobs.py index 74d6fa586..f5a523c18 100644 --- a/backend/donations/models/jobs.py +++ b/backend/donations/models/jobs.py @@ -37,7 +37,7 @@ class Job(AsyncJob): def __str__(self): return f"{self.cause} {self.status}" - class Meta: + class Meta: # type: ignore verbose_name = _("job") verbose_name_plural = _("jobs") diff --git a/backend/donations/models/ngos.py b/backend/donations/models/ngos.py index 7ebf9e790..ecb3b229e 100644 --- a/backend/donations/models/ngos.py +++ b/backend/donations/models/ngos.py @@ -1,7 +1,7 @@ import logging import re from functools import partial -from typing import Any +from typing import TYPE_CHECKING, Any from auditlog.registry import auditlog from django.conf import settings @@ -25,6 +25,11 @@ ngo_id_number_validator, ) +if TYPE_CHECKING: + from donations.models import Job, RedirectionsDownloadJob + from partners.models import Partner + + ALL_NGOS_CACHE_KEY = "ALL_NGOS" ALL_NGO_IDS_CACHE_KEY = "ALL_NGO_IDS" FRONTPAGE_NGOS_KEY = "FRONTPAGE_NGOS" @@ -280,12 +285,20 @@ class Ngo(CommonFilenameCacheModel): date_created = models.DateTimeField(verbose_name=_("date created"), db_index=True, auto_now_add=True) date_updated = models.DateTimeField(verbose_name=_("date updated"), db_index=True, auto_now=True) + # Type hinting for related models + causes: "models.manager.RelatedManager[Cause]" + partners: "models.manager.RelatedManager[Partner]" + jobs: "models.manager.RelatedManager[Job]" + download_jobs: "models.manager.RelatedManager[RedirectionsDownloadJob]" + donor_set: "models.manager.RelatedManager[Donor]" + + # Model managers objects = models.Manager() active = NgoActiveManager() ngo_hub = NgoHubManager() with_forms_this_year = NgoWithFormsThisYearManager() - class Meta: + class Meta: # type: ignore verbose_name = _("NGO") verbose_name_plural = _("NGOs") @@ -388,7 +401,7 @@ def has_ngo_hub(self): @classmethod def mandatory_fields(cls): # noinspection PyTypeChecker - field_names: list[DeferredAttribute] = [ + field_names = [ Ngo.name, Ngo.registration_number, ] @@ -540,7 +553,7 @@ class Cause(CommonFilenameCacheModel): blank=True, null=False, storage=select_public_storage, - upload_to=partial(cause_directory_path, "logos"), + upload_to=partial(cause_directory_path, "logos"), # type: ignore ) slug = models.SlugField( @@ -571,6 +584,10 @@ class Cause(CommonFilenameCacheModel): date_created = models.DateTimeField(verbose_name=_("date created"), db_index=True, auto_now_add=True) date_updated = models.DateTimeField(verbose_name=_("date updated"), db_index=True, auto_now=True) + # Type hinting for related models + donor_set: "models.manager.RelatedManager[Donor]" + + # Model managers objects = models.Manager() active = CauseActiveManager() main = CauseMainManager() @@ -578,7 +595,7 @@ class Cause(CommonFilenameCacheModel): public_active = CausePublicFormManager() nonprivate_active = CauseNonPrivateFormManager() - class Meta: + class Meta: # type: ignore verbose_name = _("Cause") verbose_name_plural = _("Causes") constraints = [ diff --git a/backend/donations/pdf.py b/backend/donations/pdf.py index dd44ea109..eb0a8d4a5 100644 --- a/backend/donations/pdf.py +++ b/backend/donations/pdf.py @@ -25,7 +25,7 @@ def _format_bank_account(bank_account: str): # remove spaces from the bank account number - bank_account: str = bank_account.replace(" ", "") + bank_account = bank_account.replace(" ", "") account: str = "" for i, letter in enumerate(bank_account): @@ -58,7 +58,7 @@ def _add_ngo_data(start_y: int, c: Canvas, cause: Cause | None, ngo: Ngo): c.setFontSize(11) # the bank account - bank_account: str = cause.bank_account if cause else ngo.bank_account + bank_account: str = (cause.bank_account if cause else ngo.bank_account) or "" account: str = _format_bank_account(bank_account) c.drawString(110, start_ngo_y - 84, account) @@ -159,7 +159,7 @@ def _add_donor_data(start_y, c: Canvas, donor: Donor): cnp: str = donor.get_cnp() for letter in cnp: c.drawString(cnp_x, cnp_y, letter) - cnp_x += 18.5 + cnp_x += 18 # email start_email_x: int = donor_block_x + 296 @@ -189,9 +189,9 @@ def _add_donor_data(start_y, c: Canvas, donor: Donor): c.drawString(donor_block_x - 39, donor_block_y - 362, "x") -def _add_signature_to_pdf(c: Canvas, signature: str, new_height: int = 30): +def _add_signature_to_pdf(c: Canvas, raw_signature: str, new_height: int = 30): # Remove the header added by JavaScript - headerless_signature: str = signature.split(",")[1] + headerless_signature: str = raw_signature.split(",")[1] # Make sure the string has the right padding signature: str = headerless_signature + "=" * (-len(headerless_signature) % 4) @@ -279,7 +279,10 @@ def create_full_pdf(donor: Donor, signature: str | None = None): # DRAW DONOR DATA _add_donor_data(start_y, c, donor) - _add_ngo_data(start_y, c, donor.cause, donor.ngo) + if donor.ngo: + _add_ngo_data(start_y, c, donor.cause, donor.ngo) + else: + print("Cannot add NGO data to PDF because there is no NGO") if signature: _add_signature_to_pdf(c, signature) diff --git a/backend/donations/tests/builder.py b/backend/donations/tests/builder.py index cb29ab5b1..6dca32387 100644 --- a/backend/donations/tests/builder.py +++ b/backend/donations/tests/builder.py @@ -53,37 +53,37 @@ def with_all_fields(self) -> "DonorTestBuilder": .with_misc() ) - def with_first_name(self, first_name: str = None) -> "DonorTestBuilder": + def with_first_name(self, first_name: str | None = None) -> "DonorTestBuilder": self.donor_data["f_name"] = first_name if first_name else faker.first_name() return self - def with_last_name(self, last_name: str = None) -> "DonorTestBuilder": + def with_last_name(self, last_name: str | None = None) -> "DonorTestBuilder": self.donor_data["l_name"] = last_name if last_name else faker.last_name() return self - def with_initial(self, initial: str = None) -> "DonorTestBuilder": + def with_initial(self, initial: str | None = None) -> "DonorTestBuilder": self.donor_data["initial"] = initial if initial else faker.random_uppercase_letter() return self - def with_cnp(self, cnp: str = None) -> "DonorTestBuilder": + def with_cnp(self, cnp: str | None = None) -> "DonorTestBuilder": self.donor_data["cnp"] = cnp if cnp else faker.ssn() return self - def with_city(self, city: str = None) -> "DonorTestBuilder": + def with_city(self, city: str | None = None) -> "DonorTestBuilder": self.donor_data["city"] = city if city else faker.city() return self - def with_county(self, county: str = None) -> "DonorTestBuilder": + def with_county(self, county: str | None = None) -> "DonorTestBuilder": self.donor_data["county"] = county if county else faker.state() return self - def with_address(self, address: dict[str, str] = None) -> "DonorTestBuilder": + def with_address(self, address: dict[str, str] | None = None) -> "DonorTestBuilder": if address is None: address = { "street_name": faker.street_name(), @@ -102,17 +102,17 @@ def with_address(self, address: dict[str, str] = None) -> "DonorTestBuilder": return self - def with_phone(self, phone: str = None) -> "DonorTestBuilder": + def with_phone(self, phone: str | None = None) -> "DonorTestBuilder": self.donor_data["phone"] = phone if phone else faker.phone_number() return self - def with_email(self, email: str = None) -> "DonorTestBuilder": + def with_email(self, email: str | None = None) -> "DonorTestBuilder": self.donor_data["email"] = email if email else faker.email() return self - def with_geoip(self, geoip: dict[str, Any] = None) -> "DonorTestBuilder": + def with_geoip(self, geoip: dict[str, Any] | None = None) -> "DonorTestBuilder": if geoip is None: geoip = { "ip": faker.ipv4(), @@ -129,11 +129,11 @@ def with_geoip(self, geoip: dict[str, Any] = None) -> "DonorTestBuilder": def with_misc( self, *, - is_anonymous: bool = None, - anaf_gdpr: bool = None, - two_years: bool = None, - has_signed: bool = None, - income_type: str = None, + is_anonymous: bool | None = None, + anaf_gdpr: bool | None = None, + two_years: bool | None = None, + has_signed: bool | None = None, + income_type: str | None = None, ): if is_anonymous is None: is_anonymous = random.choice([True, False]) diff --git a/backend/donations/views/dashboard/admin_dashboard.py b/backend/donations/views/dashboard/admin_dashboard.py index 880ba2625..fea68f15b 100644 --- a/backend/donations/views/dashboard/admin_dashboard.py +++ b/backend/donations/views/dashboard/admin_dashboard.py @@ -28,7 +28,7 @@ ADMIN_DASHBOARD_YEARLY_CACHE_KEY = "ADMIN_DASHBOARD_YEARLY" -def callback(_, context) -> dict: +def callback(request, context) -> dict: context.update(_get_admin_stats()) return context @@ -36,7 +36,7 @@ def callback(_, context) -> dict: def _get_admin_stats() -> dict: years_range_ascending = get_current_year_range() - header_stats: list[list[dict[str, str | int]]] = _get_header_stats() + header_stats: list[list[dict[str, str | int | datetime]]] = _get_header_stats() yearly_stats: list[dict] = _get_yearly_stats(years_range_ascending) @@ -54,7 +54,7 @@ def _get_header_stats() -> list[list[dict[str, str | int | datetime]]]: today: datetime = now() current_year: int = today.year - tz_info: tzinfo = today.tzinfo + tz_info: tzinfo | None = today.tzinfo current_year_range = get_encoded_current_year_range(current_year, tz_info) diff --git a/backend/donations/views/dashboard/dashboard.py b/backend/donations/views/dashboard/dashboard.py index ebb168374..dad3d18a5 100644 --- a/backend/donations/views/dashboard/dashboard.py +++ b/backend/donations/views/dashboard/dashboard.py @@ -7,7 +7,7 @@ def callback(request, context): - user: UserModel = request.user + user: UserModel = request.user # type: ignore if not user or not user.is_authenticated: return context diff --git a/backend/donations/views/dashboard/helpers.py b/backend/donations/views/dashboard/helpers.py index f5778b288..4faeda403 100644 --- a/backend/donations/views/dashboard/helpers.py +++ b/backend/donations/views/dashboard/helpers.py @@ -15,7 +15,7 @@ @cache_decorator(timeout=settings.TIMEOUT_CACHE_LONG, cache_key_prefix=ENCODED_CURRENT_YEAR_RANGE_CACHE_KEY) -def get_encoded_current_year_range(current_year: int, tz_info: tzinfo) -> str: +def get_encoded_current_year_range(current_year: int, tz_info: tzinfo | None) -> str: start_of_year: datetime = datetime(year=current_year, month=1, day=1, hour=0, minute=0, second=0, tzinfo=tz_info) end_of_next_year: datetime = start_of_year.replace(year=current_year + 1) @@ -67,7 +67,7 @@ def generate_donations_per_month_chart( "datasets": [ { "label": str(data["year"]), - "data": donations_per_month[data["year"]], + "data": donations_per_month[int(data["year"])], "borderColor": data["border_color"], "backgroundColor": data["background_color"], "borderWidth": data.get("border_width", default_border_width), diff --git a/backend/donations/views/download_donations/common.py b/backend/donations/views/download_donations/common.py index 96d4879f2..1f049eae5 100644 --- a/backend/donations/views/download_donations/common.py +++ b/backend/donations/views/download_donations/common.py @@ -118,12 +118,12 @@ def build_borderou_data_from_raw( element.append(new_xml_element(tag="denD", text=ngo_name, clean="alnums")) element.append(new_xml_element(tag="cifD", text=ngo_registration_number, clean="numbers")) - ngo_address: str = ngo_address + full_ngo_address: str = ngo_address if ngo_locality: - ngo_address += ", " + ngo_locality + full_ngo_address += ", " + ngo_locality if ngo_county: - ngo_address += ", " + ngo_county - element.append(new_xml_element(tag="adresaD", text=ngo_address, clean="custom")) + full_ngo_address += ", " + ngo_county + element.append(new_xml_element(tag="adresaD", text=full_ngo_address, clean="custom")) element.append(new_xml_element(tag="ibanD", text=bank_account, clean="alnum")) @@ -171,9 +171,15 @@ def build_donor_raw( element.append(nrCrt) contributor_identity = Element("idCnt") - contributor_identity.append(new_xml_element(tag="nume", text=donor_last_name.upper(), clean="alphabets")) - contributor_identity.append(new_xml_element(tag="init", text=donor_initial.upper(), clean="alphabet")) - contributor_identity.append(new_xml_element(tag="pren", text=donor_first_name.upper(), clean="alphabets")) + contributor_identity.append( + new_xml_element(tag="nume", text=donor_last_name.upper() if donor_last_name else "", clean="alphabets") + ) + contributor_identity.append( + new_xml_element(tag="init", text=donor_initial.upper() if donor_initial else "", clean="alphabet") + ) + contributor_identity.append( + new_xml_element(tag="pren", text=donor_first_name.upper() if donor_first_name else "", clean="alphabets") + ) contributor_identity.append(new_xml_element(tag="cif_c", text=donor_cnp, clean="numbers")) contributor_identity.append(new_xml_element(tag="adresa", text=donor_address, clean="custom")) contributor_identity.append(new_xml_element(tag="telefon", text=clean_phone_number(donor_phone), clean="numbers")) diff --git a/backend/donations/views/download_donations/main.py b/backend/donations/views/download_donations/main.py index a6e3a82b6..e15790073 100644 --- a/backend/donations/views/download_donations/main.py +++ b/backend/donations/views/download_donations/main.py @@ -40,7 +40,11 @@ def download_donations_job(job_id: int = 0): logger.error("Job with ID %d does not exist", job_id) return - cause: Cause = job.cause + cause: Cause | None = job.cause + if not cause: + job.status = JobStatusChoices.ERROR + job.save() + return timestamp: datetime = timezone.now() donations: QuerySet[Donor] = Donor.current_year_signed.filter(cause=cause).order_by("-date_created").all() diff --git a/backend/donations/views/errors.py b/backend/donations/views/errors.py index 3e1ff6e40..a689bdc07 100644 --- a/backend/donations/views/errors.py +++ b/backend/donations/views/errors.py @@ -7,7 +7,7 @@ def create_error_view(error_code: int) -> callable: Return a callable for handling different error codes """ - def custom_error_view(request: HttpRequest, exception: Exception = None) -> HttpResponse: + def custom_error_view(request: HttpRequest, exception: Exception | None = None) -> HttpResponse: if error_code in (400, 403, 404, 500): response = render(request, f"errors/{error_code}.html") response.status_code = error_code diff --git a/backend/donations/views/ngo_account/byof.py b/backend/donations/views/ngo_account/byof.py index 402d7c302..539db4738 100644 --- a/backend/donations/views/ngo_account/byof.py +++ b/backend/donations/views/ngo_account/byof.py @@ -28,7 +28,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user: User = self.request.user - ngo: Ngo = user.ngo if user.ngo else None + ngo: Ngo | None = user.ngo if user.ngo else None context.update( { @@ -51,7 +51,7 @@ def get_queryset(self): def post(self, request: HttpRequest, *args, **kwargs): user: User = request.user - ngo: Ngo = user.ngo if user.ngo else None + ngo: Ngo | None = user.ngo if user.ngo else None if not ngo: messages.error(request, _("You need to add your NGO's information first.")) return redirect(reverse_lazy("my-organization:presentation")) diff --git a/backend/donations/views/ngo_account/causes.py b/backend/donations/views/ngo_account/causes.py index 1e6616fcb..f6f6ae0f6 100644 --- a/backend/donations/views/ngo_account/causes.py +++ b/backend/donations/views/ngo_account/causes.py @@ -79,7 +79,7 @@ def do_post(self, request, **kwargs): post = request.POST user: User = request.user - ngo: Ngo = user.ngo + ngo: Ngo | None = user.ngo must_refresh_prefilled_form = False diff --git a/backend/donations/views/ngo_account/common.py b/backend/donations/views/ngo_account/common.py index 505c8d67f..0ff00ac46 100644 --- a/backend/donations/views/ngo_account/common.py +++ b/backend/donations/views/ngo_account/common.py @@ -22,7 +22,7 @@ class NgoBaseView(BaseContextPropertiesMixin): def get_extra_context(self): user: User = self.request.user - ngo: Ngo = user.ngo if user.ngo else None + ngo: Ngo | None = user.ngo if user.ngo else None return { "user": user, @@ -160,7 +160,7 @@ def delete_cause_prefilled_form(cause_id): :param cause_id: The ID of the cause whose prefilled form is to be deleted. :return: The result of the `Cause.delete_prefilled_form` method. """ - cause: Cause = Cause.objects.filter(pk=cause_id).first() + cause: Cause | None = Cause.objects.filter(pk=cause_id).first() if not cause: return None return cause.delete_prefilled_form() diff --git a/backend/donations/views/ngo_account/redirections.py b/backend/donations/views/ngo_account/redirections.py index 7ee0e808c..6e3b7745d 100644 --- a/backend/donations/views/ngo_account/redirections.py +++ b/backend/donations/views/ngo_account/redirections.py @@ -57,7 +57,9 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user: User = self.request.user - ngo: Ngo = user.ngo if user.ngo else None + ngo: Ngo | None = user.ngo if user.ngo else None + + # TODO: Handle non existing NGO causes = self._get_ngo_causes(ngo=ngo) if ngo else None context.update( @@ -186,7 +188,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user: User = self.request.user - ngo: Ngo = user.ngo if user.ngo else None + ngo: Ngo | None = user.ngo if user.ngo else None context.update( { @@ -309,7 +311,7 @@ class RedirectionDownloadLinkView(BaseVisibleTemplateView): @method_decorator(login_required(login_url=reverse_lazy("login"))) def get(self, request, form_id, *args, **kwargs): user: User = request.user - ngo: Ngo = user.ngo if user.ngo else None + ngo: Ngo | None = user.ngo if user.ngo else None if not ngo: raise Http404 @@ -338,7 +340,7 @@ def get(self, request, form_id, *args, **kwargs): @method_decorator(login_required(login_url=reverse_lazy("login"))) def post(self, request, form_id, *args, **kwargs): user: User = request.user - ngo: Ngo = user.ngo if user.ngo else None + ngo: Ngo | None = user.ngo if user.ngo else None if not ngo: raise Http404 diff --git a/backend/donations/views/ngo_account/user_settings.py b/backend/donations/views/ngo_account/user_settings.py index a02e399c0..8af155780 100644 --- a/backend/donations/views/ngo_account/user_settings.py +++ b/backend/donations/views/ngo_account/user_settings.py @@ -21,7 +21,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user: User = self.request.user - ngo: Ngo = user.ngo if user.ngo else None + ngo: Ngo | None = user.ngo if user.ngo else None has_ngohub = None if ngo: diff --git a/backend/donations/views/ngo_account_filters.py b/backend/donations/views/ngo_account_filters.py index 4f1a21f37..e2d7af158 100644 --- a/backend/donations/views/ngo_account_filters.py +++ b/backend/donations/views/ngo_account_filters.py @@ -1,6 +1,7 @@ from typing import Any from django.db.models import QuerySet +from django.utils import timezone from django.utils.translation import gettext_lazy as _ import redirectioneaza.settings.locations @@ -33,7 +34,7 @@ def __init__(self, *args, **kwargs): def options_default(self) -> list[dict[str, int | str]]: last_year = edition_deadline().year - ngo_date_created = self.ngo.date_created + ngo_date_created = self.ngo.date_created if self.ngo else timezone.now() year_range = range(ngo_date_created.year, last_year + 1) return [{"title": str(year), "value": str(year)} for year in year_range] diff --git a/backend/donations/views/redirections.py b/backend/donations/views/redirections.py index 530712dd9..53f68328f 100644 --- a/backend/donations/views/redirections.py +++ b/backend/donations/views/redirections.py @@ -295,7 +295,7 @@ def post(self, request, cause_slug): ) # send and email to the donor with a link to the PDF file - if signature: + if signature and cause: if cause.notifications_email: send_email( subject=_("Un nou formular de redirecționare"), diff --git a/backend/donations/workers/update_organization.py b/backend/donations/workers/update_organization.py index 6ab343620..20e98635a 100644 --- a/backend/donations/workers/update_organization.py +++ b/backend/donations/workers/update_organization.py @@ -120,8 +120,8 @@ def _get_ngo_hub_data(ngohub_org_id: int, token: str = "") -> Organization: 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) + new_token: str = _authenticate_with_ngohub() + return hub.get_organization(organization_id=ngohub_org_id, admin_token=new_token) def _update_main_cause_of_ngo(ngo: Ngo, ngohub_general_data: OrganizationGeneral) -> list[str] | Cause: @@ -258,7 +258,13 @@ def _update_organization_task(organization_id: int, token: str = "") -> dict[str ngo.ngohub_last_update_started = last_update_start ngo.save() - ngohub_id: int = ngo.ngohub_org_id + ngohub_id: int | None = ngo.ngohub_org_id + if not ngohub_id: + return { + "ngo_id": ngo.pk, + "errors": ["This NGO has no NGO Hub ID"], + } + try: ngohub_org_data: Organization = _get_ngo_hub_data(ngohub_id, token) except HubHTTPException as e: diff --git a/backend/q_heartbeat/management/commands/qheartbeat.py b/backend/q_heartbeat/management/commands/qheartbeat.py index f17e01473..f45a1589b 100644 --- a/backend/q_heartbeat/management/commands/qheartbeat.py +++ b/backend/q_heartbeat/management/commands/qheartbeat.py @@ -50,7 +50,7 @@ def handle(self, *args, **options): else: logger.info("The task queue seems to be working") - def terminate_workers(self, hard_attempt: False) -> None: + def terminate_workers(self, hard_attempt=False) -> None: """ Terminate or kill all cluster workers """ diff --git a/backend/redirectioneaza/context_processors/feature_flags.py b/backend/redirectioneaza/context_processors/feature_flags.py index 84f36f5d4..f5b6d7891 100644 --- a/backend/redirectioneaza/context_processors/feature_flags.py +++ b/backend/redirectioneaza/context_processors/feature_flags.py @@ -2,7 +2,7 @@ from django.http import HttpRequest -def main(_: HttpRequest) -> dict[str, bool]: +def main(request: HttpRequest) -> dict[str, bool]: return { "enable_multiple_forms": settings.ENABLE_MULTIPLE_FORMS, "enable_byof": settings.ENABLE_BYOF, diff --git a/backend/redirectioneaza/context_processors/variables.py b/backend/redirectioneaza/context_processors/variables.py index 47b1d3b27..d9dcfd760 100644 --- a/backend/redirectioneaza/context_processors/variables.py +++ b/backend/redirectioneaza/context_processors/variables.py @@ -2,7 +2,7 @@ from django.http import HttpRequest -def main(_: HttpRequest) -> dict[str, dict[str, list[dict[str, str]]]]: +def main(request: HttpRequest) -> dict[str, dict[str, list[dict[str, str]]]]: return { "CONTACT_EMAIL_ADDRESS": settings.CONTACT_EMAIL_ADDRESS, "GOOGLE_ANALYTICS_ID": settings.GOOGLE_ANALYTICS_ID, diff --git a/backend/users/models.py b/backend/users/models.py index 665026c8d..10398d2c1 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -1,6 +1,7 @@ import hmac import uuid +from allauth.socialaccount.models import SocialAccount from auditlog.registry import auditlog from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser, Group, UserManager @@ -27,12 +28,12 @@ def _create_user(self, email, password, **extra_fields): user.save(using=self._db) return user - def create_user(self, email=None, password=None, **extra_fields): + def create_user(self, email=None, password=None, **extra_fields): # type: ignore extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) return self._create_user(email, password, **extra_fields) - def create_superuser(self, email=None, password=None, **extra_fields): + def create_superuser(self, email=None, password=None, **extra_fields): # type: ignore extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) @@ -97,6 +98,10 @@ class User(AbstractUser): date_created = models.DateTimeField(verbose_name=_("date created"), db_index=True, auto_now_add=True) date_updated = models.DateTimeField(verbose_name=_("date updated"), db_index=True, auto_now=True) + # Type hinting for related models + socialaccount_set: "models.manager.RelatedManager[SocialAccount]" + + # Model managers objects = CustomUserManager() USERNAME_FIELD = "email" @@ -125,7 +130,7 @@ def refresh_token(self, commit=True): return self.validation_token def verify_token(self, token): - validation_token: uuid.UUID = self.validation_token + validation_token: uuid.UUID | None = self.validation_token if not validation_token or not token: return False if hmac.compare_digest(validation_token.hex, token.hex): diff --git a/backend/utils/text/phone_number.py b/backend/utils/text/phone_number.py index 2055f1ada..783c931a3 100644 --- a/backend/utils/text/phone_number.py +++ b/backend/utils/text/phone_number.py @@ -42,7 +42,10 @@ def validate_phone_number(raw_phone_number) -> dict[str, str]: } -def clean_phone_number(raw_phone_number: str) -> str: +def clean_phone_number(raw_phone_number: str | None) -> str: + if not raw_phone_number: + return "" + try: phone_number: PhoneNumber = phonenumbers.parse(raw_phone_number, region="RO") except phonenumbers.NumberParseException: diff --git a/backend/utils/text/registration_number.py b/backend/utils/text/registration_number.py index 2efdec3ee..d9ddc283f 100644 --- a/backend/utils/text/registration_number.py +++ b/backend/utils/text/registration_number.py @@ -37,7 +37,7 @@ def extract_vat_id(registration_number: str) -> dict[str, str]: return result -def clean_registration_number(registration_number: str) -> str | None: +def clean_registration_number(registration_number: str) -> str: """ Clean up a registration number by uppercasing the string, then removing any whitespace or forbidden characters. :param registration_number: The registration number to clean.