Skip to content
Draft
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
70 changes: 29 additions & 41 deletions backend/donations/views/dashboard/admin_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _

from donations.models.ngos import Ngo
from redirectioneaza.common.cache import cache_decorator
from donations.views.dashboard.stats_helpers.chart import donors_for_month
from donations.views.dashboard.stats_helpers.metrics import (
all_active_ngos,
all_redirections,
current_year_redirections,
ngos_active_in_current_year,
ngos_with_ngo_hub,
)
from donations.views.dashboard.stats_helpers.yearly import get_stats_for_year

from ...models.donors import Donor
from .helpers import (
generate_donations_per_month_chart,
get_current_year_range,
Expand All @@ -27,7 +33,6 @@ def callback(request, context) -> Dict:
return context


@cache_decorator(timeout=settings.TIMEOUT_CACHE_NORMAL, cache_key=ADMIN_DASHBOARD_STATS_CACHE_KEY)
def _get_admin_stats() -> Dict:
today = now()
years_range_ascending = get_current_year_range()
Expand Down Expand Up @@ -55,53 +60,54 @@ def _get_header_stats(today) -> List[List[Dict[str, Union[str, int | datetime]]]
{
"title": _("Donations this year"),
"icon": "edit_document",
"metric": Donor.available.filter(date_created__year=current_year).count(),
"metric": current_year_redirections()["metric"],
"footer": _create_stat_link(
url=f'{reverse("admin:donations_donor_changelist")}?{current_year_range}', text=_("View all")
url=f"{reverse('admin:donations_donor_changelist')}?{current_year_range}", text=_("View all")
),
"timestamp": now(),
"timestamp": current_year_redirections()["timestamp"],
},
{
"title": _("Donations all-time"),
"icon": "edit_document",
"metric": Donor.available.count(),
"metric": all_redirections()["metric"],
"footer": _create_stat_link(url=reverse("admin:donations_donor_changelist"), text=_("View all")),
"timestamp": now(),
"timestamp": all_redirections()["timestamp"],
},
{
"title": _("NGOs registered"),
"icon": "foundation",
"metric": Ngo.active.count(),
"metric": all_active_ngos()["metric"],
"footer": _create_stat_link(
url=f'{reverse("admin:donations_ngo_changelist")}?is_active=1', text=_("View all")
url=f"{reverse('admin:donations_ngo_changelist')}?is_active=1", text=_("View all")
),
"timestamp": now(),
"timestamp": all_active_ngos()["timestamp"],
},
{
"title": _("Functioning NGOs"),
"icon": "foundation",
"metric": Ngo.with_forms_this_year.count(),
"footer": _create_stat_link(url=f'{reverse("admin:donations_ngo_changelist")}', text=_("View all")),
"timestamp": now(),
"metric": ngos_active_in_current_year()["metric"],
"footer": _create_stat_link(url=f"{reverse('admin:donations_ngo_changelist')}", text=_("View all")),
"timestamp": ngos_active_in_current_year()["timestamp"],
},
{
"title": _("NGOs from NGO Hub"),
"icon": "foundation",
"metric": Ngo.ngo_hub.count(),
"metric": ngos_with_ngo_hub()["metric"],
"footer": _create_stat_link(
url=f'{reverse("admin:donations_ngo_changelist")}?is_active=1&has_ngohub=1', text=_("View all")
url=f"{reverse('admin:donations_ngo_changelist')}?is_active=1&has_ngohub=1", text=_("View all")
),
"timestamp": now(),
"timestamp": ngos_with_ngo_hub()["timestamp"],
},
]
]


def _create_chart_statistics() -> Dict[str, str]:
default_border_width: int = 3
current_year = now().year

donations_per_month_queryset = [
Donor.available.filter(date_created__month=month) for month in range(1, settings.DONATIONS_LIMIT.month + 1)
donors_for_month(month, current_year)["metric"] for month in range(1, settings.DONATIONS_LIMIT.month + 1)
]

forms_per_month_chart = generate_donations_per_month_chart(default_border_width, donations_per_month_queryset)
Expand All @@ -110,7 +116,7 @@ def _create_chart_statistics() -> Dict[str, str]:


def _get_yearly_stats(years_range_ascending) -> List[Dict[str, Union[int, List[Dict]]]]:
statistics = [_get_stats_for_year(year) for year in years_range_ascending]
statistics = [get_stats_for_year(year) for year in years_range_ascending]

for index, statistic in enumerate(statistics):
if index == 0:
Expand All @@ -129,24 +135,6 @@ def _get_yearly_stats(years_range_ascending) -> List[Dict[str, Union[int, List[D
return sorted(final_statistics, key=lambda x: x["year"], reverse=True)


# TODO: This cache seems useless because we already cache the entire dashboard stats
@cache_decorator(timeout=settings.TIMEOUT_CACHE_NORMAL, cache_key_prefix=ADMIN_DASHBOARD_CACHE_KEY)
def _get_stats_for_year(year: int) -> Dict[str, int | datetime]:
donations: int = Donor.available.filter(date_created__year=year).count()
ngos_registered: int = Ngo.objects.filter(date_created__year=year).count()
ngos_with_forms: int = Donor.available.filter(date_created__year=year).values("ngo_id").distinct().count()

statistic = {
"year": year,
"donations": donations,
"ngos_registered": ngos_registered,
"ngos_with_forms": ngos_with_forms,
"timestamp": now(),
}

return statistic


def _format_yearly_stats(statistics) -> List[Dict[str, Union[int, List[Dict]]]]:
return [
{
Expand All @@ -158,7 +146,7 @@ def _format_yearly_stats(statistics) -> List[Dict[str, Union[int, List[Dict]]]]:
"metric": statistic["donations"],
"label": statistic.get("donations_difference"),
"footer": _create_stat_link(
url=f'{reverse("admin:donations_donor_changelist")}?date_created__year={statistic["year"]}',
url=f"{reverse('admin:donations_donor_changelist')}?date_created__year={statistic['year']}",
text=_("View all"),
),
"timestamp": statistic["timestamp"],
Expand All @@ -169,7 +157,7 @@ def _format_yearly_stats(statistics) -> List[Dict[str, Union[int, List[Dict]]]]:
"metric": statistic["ngos_registered"],
"label": statistic.get("ngos_registered_difference"),
"footer": _create_stat_link(
url=f'{reverse("admin:donations_ngo_changelist")}?date_created__year={statistic["year"]}',
url=f"{reverse('admin:donations_ngo_changelist')}?date_created__year={statistic['year']}",
text=_("View all"),
),
"timestamp": statistic["timestamp"],
Expand All @@ -180,7 +168,7 @@ def _format_yearly_stats(statistics) -> List[Dict[str, Union[int, List[Dict]]]]:
"metric": statistic["ngos_with_forms"],
"label": statistic.get("ngos_with_forms_difference"),
"footer": _create_stat_link(
url=f'{reverse("admin:donations_ngo_changelist")}?has_forms=1&date_created__year={statistic["year"]}',
url=f"{reverse('admin:donations_ngo_changelist')}?has_forms=1&date_created__year={statistic['year']}",
text=_("View all"),
),
"timestamp": statistic["timestamp"],
Expand Down
Empty file.
75 changes: 75 additions & 0 deletions backend/donations/views/dashboard/stats_helpers/chart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Dict, Any

from django.conf import settings
from django.utils.timezone import now

from donations.models import Donor
from donations.views.dashboard.stats_helpers.utils import cache_set
from redirectioneaza.common.async_cache import async_cache_decorator, is_cache_valid_for_past_date

STATS_FOR_MONTH_CACHE_PREFIX = "STATS_FOR_MONTH_"


@async_cache_decorator(
cache_key_custom=f"{STATS_FOR_MONTH_CACHE_PREFIX}{{1}}_{{{0}}}",
placeholder_value={
"metric": -2,
"year": 0,
"month": 0,
},
validation_func=is_cache_valid_for_past_date,
timeout=settings.TIMEOUT_CACHE_NORMAL,
)
def donors_for_month(month: int, year: int = None) -> Dict[str, Any]:
"""
Determines the number of donors for a specified month and year.

This function retrieves the number of donors for a specific month and year
from the cache if available and valid. If the cache is invalid or absent,
it initiates an asynchronous task to update the stats and returns a default
statistic. If the year parameter is not provided, it defaults to the
current year.

Parameters:
month (int): The month for which donor statistics are requested.
year (int, optional): The year for which donor statistics are required or current year.

Returns:
Dict[str, Any]: A dictionary containing the number of donors for the specified month and year.
"""
if year is None:
year = now().year

# This function is decorated with async_cache_decorator, which handles caching and async tasks.
# The actual implementation is in _update_stats_for_month, which is called by the decorator.
return _update_stats_for_month(month, year)


def _update_stats_for_month(month: int, year: int, _cache_key: str = None, _timeout: int = None) -> Dict[str, Any]:
"""
Updates the number of donors for a specific month and year, and caches the result.

Parameters:
month (int): The month for which to compute donor statistics.
year (int): The year for which to compute donor statistics.
_cache_key (str, optional): The cache key to use when storing the result.
This is passed by the async_cache_decorator.
_timeout (int, optional): The cache timeout in seconds.
This is passed by the async_cache_decorator.

Returns:
Dict[str, Any]: A dictionary containing the number of donors for the specified month and year.
"""
donors_count = Donor.objects.filter(date_created__year=year, date_created__month=month).count()

stat = {
"metric": donors_count,
"year": year,
"month": month,
"timestamp": now(),
}

if _cache_key and _timeout is not None:
cache_set(_cache_key, stat, timeout=_timeout)

return stat
Loading
Loading