Skip to content

Commit

Permalink
Create an MVP for the admin dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
tudoramariei committed Oct 11, 2024
1 parent e2932bd commit c7cbbf9
Show file tree
Hide file tree
Showing 19 changed files with 1,034 additions and 296 deletions.
35 changes: 30 additions & 5 deletions backend/donations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,51 @@ def link_to_user(self, obj: User):
return format_html(f'<a href="{link_url}">{obj.email}</a>')


class HasNgoHubFilter(admin.SimpleListFilter):
title = _("Has NGO Hub account")
parameter_name = "has_ngohub"

def lookups(self, request, model_admin):
return (1, _("Yes")), (0, _("No"))

def queryset(self, request, queryset):
filter_value = None
if self.value() == "1":
filter_value = True
elif self.value() == "0":
filter_value = False

if filter_value is not None:
return queryset.filter(ngohub_org_id__isnull=filter_value)


class HasOwnerFilter(admin.SimpleListFilter):
title = _("Has owner")
parameter_name = "has_owner"

def lookups(self, request, model_admin):
return ("yes", _("Yes")), ("no", _("No"))
return (1, _("Yes")), (0, _("No"))

def queryset(self, request, queryset):
if self.value() == "yes":
return queryset.filter(users__isnull=False)
if self.value() == "no":
return queryset.filter(users__isnull=True)
filter_value = None
if self.value() == "1":
filter_value = True
elif self.value() == "0":
filter_value = False

if filter_value is not None:
return queryset.filter(users__isnull=filter_value)


@admin.register(Ngo)
class NgoAdmin(ModelAdmin):
list_filter_submit = True

list_display = ("id", "ngohub_org_id", "slug", "registration_number", "name")
list_display_links = ("id", "ngohub_org_id", "slug", "registration_number", "name")
list_filter = (
"date_created",
HasNgoHubFilter,
"is_verified",
"is_active",
"is_accepting_forms",
Expand Down
Empty file.
6 changes: 5 additions & 1 deletion backend/donations/management/commands/generate_donations.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ def handle(self, *args, **options):
generated_donations.append(donor)

self.stdout.write(self.style.SUCCESS("Writing to the database..."))
Donor.objects.bulk_create(generated_donations, batch_size=500)
Donor.objects.bulk_create(
generated_donations,
batch_size=500,
ignore_conflicts=True,
)

self.stdout.write(self.style.SUCCESS("Done!"))
2 changes: 1 addition & 1 deletion backend/donations/management/commands/generate_orgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ def handle(self, *args, **options):
"name": name,
"slug": kebab_case_name,
"description": fake.paragraph(nb_sentences=3, variable_nb_sentences=True),
"logo_url": "https://storage.googleapis.com/redirectioneaza/logo_bw.png",
"bank_account": fake.iban(),
"registration_number": fake.ssn()[:8],
"address": address,
Expand All @@ -206,6 +205,7 @@ def handle(self, *args, **options):
"website": fake.url(),
"is_active": create_valid or random.choice([True, False]),
"is_accepting_forms": create_valid or random.choice([True, False]),
"ngohub_org_id": random.randint(1, 9999) or None,
}
try:
org = Ngo.objects.create(**organization_details)
Expand Down
18 changes: 17 additions & 1 deletion backend/donations/models/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ def get_queryset(self):
return super().get_queryset().filter(is_active=True)


class NgoHubManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True, ngohub_org_id__isnull=False)


class NgoWithFormsManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True, is_accepting_forms=True)


class Ngo(models.Model):
slug = models.SlugField(
verbose_name=_("slug"),
Expand Down Expand Up @@ -243,6 +253,8 @@ class Ngo(models.Model):

objects = models.Manager()
active = NgoActiveManager()
ngo_hub = NgoHubManager()
with_forms = NgoWithFormsManager()

def save(self, *args, **kwargs):
is_new = self.id is None
Expand Down Expand Up @@ -403,7 +415,11 @@ class Donor(models.Model):
upload_to=partial(year_ngo_donor_directory_path, "donation-forms"),
)

date_created = models.DateTimeField(verbose_name=_("date created"), db_index=True, auto_now_add=timezone.now)
date_created = models.DateTimeField(
verbose_name=_("date created"),
db_index=True,
auto_now_add=timezone.now,
)

objects = models.Manager()
signed = DonorSignedManager()
Expand Down
Empty file.
230 changes: 230 additions & 0 deletions backend/donations/views/unfold/admin_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import json
from datetime import datetime
from typing import Dict

from django.conf import settings
from django.contrib import messages
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.timezone import localtime, now
from django.utils.translation import gettext_lazy as _

from donations.models.main import Donor, Ngo
from redirectioneaza.common.cache import cache_decorator

ADMIN_DASHBOARD_CACHE_KEY = "ADMIN_DASHBOARD"


@cache_decorator(timeout=settings.TIMEOUT_CACHE_SHORT, cache_key_prefix=ADMIN_DASHBOARD_CACHE_KEY)
def admin_callback(request, context):
today = now()
years_range_ascending = range(settings.START_YEAR, today.year + 1)

messages.warning(
request,
render_to_string(
"admin/announcements/work_in_progress.html",
context={
"contact_email": settings.CONTACT_EMAIL_ADDRESS,
},
),
)

header_stats = _get_header_stats(today)

yearly_stats = _get_yearly_stats(years_range_ascending)

forms_per_month_chart = _create_chart_statistics(years_range_ascending)

context.update(
{
"header_stats": header_stats,
"yearly_stats": yearly_stats,
"forms_per_month_chart": forms_per_month_chart,
}
)

return context


def _get_header_stats(today):
current_year = today.year

start_of_this_year = datetime(year=current_year, month=1, day=1, hour=0, minute=0, second=0, tzinfo=today.tzinfo)
end_of_next_year = start_of_this_year.replace(year=current_year + 1)

current_year_range: str = urlencode(
{
"date_created__gte": localtime(start_of_this_year),
"date_created__lt": localtime(end_of_next_year),
}
)

return [
{
"title": _("Donations this year"),
"icon": "edit_document",
"metric": Donor.objects.filter(date_created__year=current_year).count(),
"footer": _create_stat_link(
url=f'{reverse("admin:donations_donor_changelist")}?{current_year_range}', text=_("View all")
),
},
{
"title": _("Donations all-time"),
"icon": "edit_document",
"metric": Donor.objects.count(),
"footer": _create_stat_link(url=reverse("admin:donations_donor_changelist"), text=_("View all")),
},
{
"title": _("NGOs registered"),
"icon": "foundation",
"metric": Ngo.active.count(),
"footer": _create_stat_link(
url=f'{reverse("admin:donations_ngo_changelist")}?is_active=1', text=_("View all")
),
},
{
"title": _("NGOs from NGO Hub"),
"icon": "foundation",
"metric": Ngo.ngo_hub.count(),
"footer": _create_stat_link(
url=f'{reverse("admin:donations_ngo_changelist")}?is_active=1&is_ngohub=1', text=_("View all")
),
},
]


def _create_chart_statistics(years_range_ascending):
default_border_width: int = 3

donations_per_month_queryset = [
Donor.objects.filter(date_created__month=month) for month in range(1, settings.DONATIONS_LIMIT.month + 1)
]

dataset_parameters = [
{
"year": year,
"border_color": (
"rgba("
f"{settings.CHART_COLORS[year % len(settings.CHART_COLORS)]['r']}, "
f"{settings.CHART_COLORS[year % len(settings.CHART_COLORS)]['g']}, "
f"{settings.CHART_COLORS[year % len(settings.CHART_COLORS)]['b']}, "
"1)"
),
"background_color": (
"rgba("
f"{settings.CHART_COLORS[year % len(settings.CHART_COLORS)]['r']}, "
f"{settings.CHART_COLORS[year % len(settings.CHART_COLORS)]['g']}, "
f"{settings.CHART_COLORS[year % len(settings.CHART_COLORS)]['b']}, "
"0.2)"
),
}
for year in years_range_ascending
]

forms_per_month_chart = {
"title": _("Donations per month"),
"data": json.dumps(
{
"labels": [str(month["label"]) for month in settings.MONTHS[: settings.DONATIONS_LIMIT.month]],
"datasets": [
{
"label": str(data["year"]),
"data": [
donations.filter(date_created__year=data["year"]).count()
for donations in donations_per_month_queryset
],
"borderColor": data["border_color"],
"backgroundColor": data["background_color"],
"borderWidth": data.get("border_width", default_border_width),
}
for data in dataset_parameters
],
}
),
}

return forms_per_month_chart


def _get_yearly_stats(years_range_ascending):
statistics = [_get_stats_for_year(year) for year in years_range_ascending]

for index, statistic in enumerate(statistics):
if index == 0:
continue

statistics[index]["donations_difference"] = statistics[index]["donations"] - statistics[index - 1]["donations"]
statistics[index]["ngos_registered_difference"] = (
statistics[index]["ngos_registered"] - statistics[index - 1]["ngos_registered"]
)
statistics[index]["ngos_with_forms_difference"] = (
statistics[index]["ngos_with_forms"] - statistics[index - 1]["ngos_with_forms"]
)

final_statistics = _format_yearly_stats(statistics)

return sorted(final_statistics, key=lambda x: x["year"], reverse=True)


@cache_decorator(timeout=settings.TIMEOUT_CACHE_NORMAL, cache_key_prefix=ADMIN_DASHBOARD_CACHE_KEY)
def _get_stats_for_year(year: int) -> Dict[str, int]:
donations: int = Donor.objects.filter(date_created__year=year).count()
ngos_registered: int = Ngo.objects.filter(date_created__year=year).count()
ngos_with_forms: int = Donor.objects.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,
}

return statistic


def _format_yearly_stats(statistics):
return [
{
"year": statistic["year"],
"stats": [
{
"title": _("Donations"),
"icon": "edit_document",
"metric": statistic["donations"],
"label": statistic.get("donations_difference"),
"footer": _create_stat_link(
url=f'{reverse("admin:donations_donor_changelist")}?date_created__year={statistic["year"]}',
text=_("View all"),
),
},
{
"title": _("NGOs registered"),
"icon": "foundation",
"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"]}',
text=_("View all"),
),
},
{
"title": _("NGOs with forms"),
"icon": "foundation",
"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"]}',
text=_("View all"),
),
},
],
}
for statistic in statistics
]


def _create_stat_link(url: str, text: str) -> str:
return mark_safe(f'<a href="{url}" class="text-orange-700 font-semibold">{text}</a>')
14 changes: 14 additions & 0 deletions backend/donations/views/unfold/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from donations.views.unfold.admin_dashboard import admin_callback
from donations.views.unfold.ngo_dashboard import ngo_callback


def callback(request, context):
user = request.user

if not user or not user.is_authenticated:
return context

if user.is_superuser:
admin_callback(request, context)

ngo_callback(request, context)
2 changes: 2 additions & 0 deletions backend/donations/views/unfold/ngo_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def ngo_callback(request, context):
return context
Loading

0 comments on commit c7cbbf9

Please sign in to comment.