Skip to content

Commit

Permalink
Merge pull request #5 from now-u/dashboard
Browse files Browse the repository at this point in the history
Add admin dashboard
  • Loading branch information
JElgar authored Oct 25, 2024
2 parents fe11458 + 3e36648 commit 12e9e4d
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 5 deletions.
134 changes: 134 additions & 0 deletions causes_service/admin/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from typing import Type
from django.db.models import Count, Q
from unfold.admin import mark_safe
from datetime import datetime, timedelta
from django.utils import timezone
from typing import NamedTuple


from causes.models import LearningResource, UserAction, Action, UserCampaign, UserCause, UserLearningResources, UserNewsArticle
from utils.models import TimeStampedMixin

def start_of_day(date: datetime) -> datetime:
return timezone.make_aware(datetime(day=date.day, month=date.month, year=date.year))

def dashboard_callback(request, context):
now = timezone.now()
end_date = start_of_day(now) + timedelta(days=1)
start_date = end_date - timedelta(days=7)

kpis = [
build_kpi(resource, now)
for resource in
[
Resource(title="Actions completed", completion_class=UserAction),
Resource(title="Learning resources completed", completion_class=UserLearningResources),
Resource(title="News articles viewed", completion_class=UserNewsArticle),
Resource(title="Campaigns completed", completion_class=UserCampaign),
Resource(title="Causes joined", completion_class=UserCause),
]
]

top_actions = Action.objects.all()\
.annotate(
completed_count=Count('completions', filter=Q(
completions__created_at__gte=start_date,
completions__created_at__lt=end_date,
))
)\
.order_by('completed_count')\
.filter(completed_count__gt=0)\
[:10]

top_learning_resources = LearningResource.objects.all()\
.annotate(
completed_count=Count('completions', filter=Q(
completions__created_at__gte=start_date,
completions__created_at__lt=end_date,
))
)\
.order_by('completed_count')\
.filter(completed_count__gt=0)\
[:10]

context.update(
{
"kpi": kpis,
"top_actions": [
{
"title": item.title,
"completed_count": item.completed_count,
"percentage_of_top": (item.completed_count / top_actions[0].completed_count) * 100,
}
for item in top_actions
],
"top_learning_resources": [
{
"title": item.title,
"completed_count": item.completed_count,
"percentage_of_top": (item.completed_count / top_learning_resources[0].completed_count) * 100,
}
for item in top_learning_resources
],
}
)

return context

class Resource(NamedTuple):
title: str
completion_class: Type[TimeStampedMixin]

class CompletetionData(NamedTuple):
num_completed_last_week: int
percentage_increase_since_week_before: float | None

def build_kpi(resource: Resource, now: datetime):
completion_data = compute_completion_data(resource.completion_class, now)

return {
"title": resource.title,
"metric": completion_data.num_completed_last_week,
"footer": build_footer(completion_data.percentage_increase_since_week_before),
}

def compute_completion_data(model: Type[TimeStampedMixin], now: datetime) -> CompletetionData:
# Very beginning of tomorrow
end_date = now.date() + timedelta(days=1)
start_date = end_date - timedelta(days=7)

num_completed_last_week = model.objects.filter(
created_at__gte=start_date,
created_at__lt=end_date,
).count()
num_completed_last_last_week = model.objects.filter(
created_at__gte=start_date - timedelta(days=7),
created_at__lt=start_date
).count()

percentage_increase = ((num_completed_last_week - num_completed_last_last_week) / num_completed_last_last_week) * 100 if num_completed_last_last_week != 0 else None

return CompletetionData(
num_completed_last_week=num_completed_last_week,
percentage_increase_since_week_before=percentage_increase,
)

def get_footer_color(percentage_increase: float | None):
if percentage_increase is None:
return 'white'
if percentage_increase > 0:
return 'text-green-700'
return 'text-red-700'

def get_footer_text_value(percentage_increase: float | None):
if percentage_increase is None:
return '-'
if percentage_increase > 0:
return f'+{percentage_increase}%'
return f'{percentage_increase}%'

def build_footer(percentage_increase: float | None):
text_color = get_footer_color(percentage_increase)
text_value = get_footer_text_value(percentage_increase)

return mark_safe(f'<strong class="{text_color} font-semibold dark:text-green-400">{text_value}</strong>&nbsp;progress from last week')
19 changes: 19 additions & 0 deletions causes_service/causes/migrations/0026_alter_useraction_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.1.2 on 2024-10-25 09:05

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('causes', '0025_usernewsarticle'),
]

operations = [
migrations.AlterField(
model_name='useraction',
name='action',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='completions', to='causes.action'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.1 on 2024-10-25 18:22

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('causes', '0026_alter_useraction_action'),
]

operations = [
migrations.AlterField(
model_name='userlearningresources',
name='learning_resource',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='completions', to='causes.learningresource'),
),
]
4 changes: 2 additions & 2 deletions causes_service/causes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,11 @@ class OrganisationExtraLink(models.Model):

class UserAction(TimeStampedMixin, models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='completed_actions')
action = models.ForeignKey(Action, on_delete=models.CASCADE)
action = models.ForeignKey(Action, on_delete=models.CASCADE, related_name='completions')

class UserLearningResources(TimeStampedMixin, models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='completed_learning_resources')
learning_resource = models.ForeignKey(LearningResource, on_delete=models.CASCADE)
learning_resource = models.ForeignKey(LearningResource, on_delete=models.CASCADE, related_name='completions')

class UserNewsArticle(TimeStampedMixin, models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='completed_news_articles')
Expand Down
1 change: 1 addition & 0 deletions causes_service/now_u_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ class SUPABASE:
"STYLES": [
lambda request: static("css/ts-styles.css"),
],
"DASHBOARD_CALLBACK": "admin.views.dashboard_callback",
}

TEMPLATES = [
Expand Down
6 changes: 3 additions & 3 deletions causes_service/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ pytest-django==4.5.2
factory_boy==3.2.1

## Linting deps
mypy==1.4.1
django-stubs[compatible-mypy]==4.2.2
djangorestframework-stubs==3.14.2
mypy==1.11.0
django-stubs[compatible-mypy]==5.1.0
djangorestframework-stubs==3.15.1

ruff==0.0.287
56 changes: 56 additions & 0 deletions causes_service/templates/admin/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{% extends 'unfold/layouts/base_simple.html' %}

{% load cache i18n unfold %}

{% block breadcrumbs %}{% endblock %}

{% block title %}
{% if subtitle %}
{{ subtitle }} |
{% endif %}

{{ title }} | {{ site_title|default:_('Django site admin') }}
{% endblock %}

{% block branding %}
<h1 id="site-name">
<a href="{% url 'admin:index' %}">
{{ site_header|default:_('Django administration') }}
</a>
</h1>
{% endblock %}

{% block content %}
{% component "unfold/components/container.html" with class="grid grid-cols-4 lg:grid-cols-12 gap-6" %}
{% for stats in kpi %}
{% trans "Last 7 days" as label %}
{% component "unfold/components/card.html" with class="col-span-4" label=label footer=stats.footer %}
{% component "unfold/components/text.html" %}
{{ stats.title }}
{% endcomponent %}

{% component "unfold/components/title.html" %}
{{ stats.metric }}
{% endcomponent %}
{% endcomponent %}
{% endfor %}

{% component "unfold/components/card.html" with class="col-span-6" title="Top actions by completion last week" %}
{% component "unfold/components/flex.html" with col=1 class="gap-5" %}
{% for action in top_actions %}
{% component "unfold/components/progress.html" with title=action.title description=action.completed_count value=action.percentage_of_top %}{% endcomponent %}
{% endfor %}
{% endcomponent %}
{% endcomponent %}

{% component "unfold/components/card.html" with class="col-span-6" title="Top learning resources by completion last week" %}
{% component "unfold/components/flex.html" with col=1 class="gap-5" %}
{% for item in top_learning_resources %}
{% component "unfold/components/progress.html" with title=item.title description=item.completed_count value=item.percentage_of_top %}{% endcomponent %}
{% endfor %}
{% endcomponent %}
{% endcomponent %}

{% endcomponent %}

{% endblock %}

0 comments on commit 12e9e4d

Please sign in to comment.