Skip to content

Commit

Permalink
add delivery-type, year, quarter filter to admin report
Browse files Browse the repository at this point in the history
  • Loading branch information
sravfeyn committed Sep 22, 2024
1 parent 35ced03 commit 727e704
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 20 deletions.
5 changes: 3 additions & 2 deletions commcare_connect/reports/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.urls import path

from commcare_connect.reports import views
from commcare_connect.reports.views import DeliveryStatsReportView

app_name = "reports"

urlpatterns = [
path("delivery_stats", views.delivery_stats_report, name="delivery_stats_report"),
# path("delivery_stats", views.delivery_stats_report, name="delivery_stats_report"),
path("delivery_stats", view=DeliveryStatsReportView.as_view(), name="delivery_stats_report"),
]
124 changes: 106 additions & 18 deletions commcare_connect/reports/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from datetime import date
from datetime import date, datetime

from django.contrib.auth.decorators import login_required, user_passes_test
from django.db.models import Max, Sum
from django.shortcuts import render
from django.views.decorators.http import require_GET
import django_filters
import django_tables2 as tables
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db.models import Max, Q, Sum
from django.urls import reverse
from django_filters.views import FilterView

from commcare_connect.opportunity.models import CompletedWork, CompletedWorkStatus, Payment
from commcare_connect.opportunity.models import CompletedWork, CompletedWorkStatus, DeliveryType, Payment

from .tables import AdminReportTable

Expand Down Expand Up @@ -33,7 +35,12 @@ def _get_quarters_since_start():
return quarters


def _get_table_data_for_quarter(quarter):
def _get_table_data_for_quarter(quarter, delivery_type):
if delivery_type:
delivery_type_filter = Q(opportunity_access__opportunity__delivery_type__slug=delivery_type)
else:
delivery_type_filter = Q()

quarter_start = date(quarter[0], (quarter[1] - 1) * 3 + 1, 1)
next_quarter = _increment(quarter)
quarter_end = date(next_quarter[0], (next_quarter[1] - 1) * 3 + 1, 1)
Expand All @@ -48,6 +55,7 @@ def _get_table_data_for_quarter(quarter):
visit_data = (
CompletedWork.objects.annotate(work_date=Max("uservisit__visit_date"))
.filter(
delivery_type_filter,
opportunity_access__opportunity__is_test=False,
status=CompletedWorkStatus.approved,
work_date__gte=quarter_start,
Expand All @@ -67,6 +75,7 @@ def _get_table_data_for_quarter(quarter):

approved_payment_data = (
Payment.objects.filter(
delivery_type_filter,
opportunity_access__opportunity__is_test=False,
confirmed=True,
date_paid__gte=quarter_start,
Expand All @@ -88,6 +97,7 @@ def payment_strings(payment_data):

total_payment_data = (
Payment.objects.filter(
delivery_type_filter,
opportunity_access__opportunity__is_test=False,
date_paid__gte=quarter_start,
date_paid__lt=quarter_end,
Expand All @@ -106,14 +116,92 @@ def payment_strings(payment_data):
}


@login_required
@user_passes_test(lambda user: user.is_superuser)
@require_GET
def delivery_stats_report(request):
table_data = []
quarters = _get_quarters_since_start()
for q in quarters:
data = _get_table_data_for_quarter(q)
table_data.append(data)
table = AdminReportTable(table_data)
return render(request, "reports/admin.html", context={"table": table})
class SuperUserRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
def test_func(self):
return self.request.user.is_superuser


class YearQuarterFilter(django_filters.FilterSet):
delivery_type = django_filters.ChoiceFilter(method="filter_by_ignore")
quarter = django_filters.ChoiceFilter(
choices=[(1, "Q1"), (2, "Q2"), (3, "Q3"), (4, "Q4")], label="Quarter", method="filter_by_ignore"
)
year = django_filters.ChoiceFilter(method="filter_by_ignore")

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
current_year = datetime.now().year
year_choices = [(year, str(year)) for year in range(2023, current_year + 1)]
self.filters["year"] = django_filters.ChoiceFilter(
choices=year_choices, label="Year", method="filter_by_ignore"
)

delivery_types = DeliveryType.objects.values_list("slug", "name")
self.filters["delivery_type"] = django_filters.ChoiceFilter(choices=delivery_types, label="Delivery Type")

def filter_by_ignore(self, queryset, name, value):
return queryset

class Meta:
model = None
fields = ["delivery_type", "year", "quarter"]
unknown_field_behavior = django_filters.UnknownFieldBehavior.IGNORE


class NonModelFilterView(FilterView):
def get_queryset(self):
# Doesn't matter which model it is here
return CompletedWork.objects.none()

@property
def object_list(self):
# Override this
return []

def get(self, request, *args, **kwargs):
filterset_class = self.get_filterset_class()
self.filterset = self.get_filterset(filterset_class)
context = self.get_context_data(filter=self.filterset, object_list=self.object_list)
return self.render_to_response(context)


class DeliveryStatsReportView(tables.SingleTableMixin, SuperUserRequiredMixin, NonModelFilterView):
table_class = AdminReportTable
filterset_class = YearQuarterFilter

def get_template_names(self):
if self.request.htmx:
template_name = "reports/htmx_table.html"
else:
template_name = "reports/report_table.html"

return template_name

def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
context["report_url"] = reverse("reports:delivery_stats_report")
return context

@property
def object_list(self):
table_data = []
if not self.filterset.form.is_valid():
return []

filter_values = self.filterset.form.cleaned_data
delivery_type = filter_values["delivery_type"]
year = int(filter_values["year"])
quarter = filter_values["quarter"]

if not year:
quarters = _get_quarters_since_start()
elif year:
if quarter:
quarters = [(year, quarter)]
else:
quarters = [(year, q) for q in range(1, 5)]

for q in quarters:
data = _get_table_data_for_quarter(q, delivery_type)
table_data.append(data)
return table_data
87 changes: 87 additions & 0 deletions commcare_connect/templates/reports/htmx_table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{% extends "django_tables2/bootstrap5.html" %}

{% load django_tables2 %}
{% load i18n %}

{% block table.thead %}
{% if table.show_header %}
<thead {{ table.attrs.thead.as_html }}>
<tr>
{% for column in table.columns %}
<th {{ column.attrs.th.as_html }}
{% if column.orderable %}
hx-get="{% querystring table.prefixed_order_by_field=column.order_by_alias.next%}&{{ table.prefixed_page_field }}=1"
{% endif %}
hx-trigger="click"
hx-target="div.table-container"
hx-swap="outerHTML"
style="cursor: pointer;">
{{ column.header }}
{% if column.orderable %}
{% if column.order_by_alias == column.order_by_alias.next %}
<i class="bi bi-arrow-down-short"></i>
{% elif column.order_by_alias|slice:":1" == "-" %}
<i class="bi bi-arrow-down-square-fill"></i>
{% else %}
<i class="bi bi-arrow-up-square-fill"></i>
{% endif %}
{% endif%}
</th>
{% endfor %}
</tr>
</thead>
{% endif %}
{% endblock table.thead %}

{% block pagination %}
{% if table.page and table.paginator.num_pages > 1 %}
<nav aria-label="Table navigation">
<ul class="pagination justify-content-center mt-2">
{% if table.page.has_previous %}
{% block pagination.previous %}
<li class="previous page-item">
<div hx-get="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"
hx-trigger="click"
hx-target="div.table-container"
hx-swap="outerHTML"
hx-indicator=".progress"
class="page-link">
<span aria-hidden="true">&laquo;</span>
{% trans 'previous' %}
</div>
</li>
{% endblock pagination.previous %}
{% endif %}
{% if table.page.has_previous or table.page.has_next %}
{% for p in table.page|table_page_range:table.paginator %}
<li class="page-item{% if table.page.number == p %} active{% endif %}">
<div class="page-link"
{% if p != '...' %}hx-get="{% querystring table.prefixed_page_field=p %}"{% endif %}
hx-trigger="click"
hx-target="div.table-container"
hx-swap="outerHTML"
hx-indicator=".progress">
{{ p }}
</div>
</li>
{% endfor %}
{% endif %}
{% if table.page.has_next %}
{% block pagination.next %}
<li class="next page-item">
<div hx-get="{% querystring table.prefixed_page_field=table.page.next_page_number %}"
hx-trigger="click"
hx-target="div.table-container"
hx-swap="outerHTML"
hx-indicator=".progress"
class="page-link">
{% trans 'next' %}
<span aria-hidden="true">&raquo;</span>
</div>
</li>
{% endblock pagination.next %}
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock pagination %}
44 changes: 44 additions & 0 deletions commcare_connect/templates/reports/report_table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{% extends "base.html" %}

{% load render_table from django_tables2 %}
{% load i18n %}
{% load crispy_forms_tags %}

{% block content %}
<h1>Events</h1>

<form hx-get="{{ report_url }}"
hx-target="div.table-container"
hx-swap="outerHTML"
hx-trigger="change"
hx-push-url="true"
class="form-inline">
{% crispy filter.form %}
</form>

{% render_table table %}
{% endblock %}

{% block javascript %}
{{ block.super }}
<!-- This is required for select2 'user' field-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

<script>
window.addEventListener("DOMContentLoaded", (e) => {
$('select').on('select2:select', function (e) {
$(this).closest('form').get(0).dispatchEvent(new Event('change'));
});
})
</script>

{% endblock %}

{% block css %}
{{ block.super }}
<style>
.select2-selection__rendered {
line-height: initial!important;
}
</style>
{% endblock %}
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django_htmx.middleware.HtmxMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"commcare_connect.users.middleware.OrganizationMiddleware",
Expand Down
3 changes: 3 additions & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ django-cors-headers
# DRF-spectacular for api documentation
drf-spectacular
django-tables2
django-filter
django-autocomplete-light
django-htmx

# Temporary
# -------------------------------------------------------------------------------
Expand Down

0 comments on commit 727e704

Please sign in to comment.