Skip to content

Commit

Permalink
Merge pull request #422 from dimagi/dashboard-v2
Browse files Browse the repository at this point in the history
Program Dashboard Updates
  • Loading branch information
czue authored Nov 6, 2024
2 parents 9210cf4 + 8166964 commit 764f7dd
Show file tree
Hide file tree
Showing 5 changed files with 443 additions and 168 deletions.
34 changes: 34 additions & 0 deletions commcare_connect/reports/queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from django.db.models import F
from django.db.models.fields.json import KT


def get_visit_map_queryset(base_queryset):
return (
base_queryset.annotate(
deliver_unit_name=F("deliver_unit__name"),
username_connectid=KT("form_json__metadata__username"),
timestart_str=KT("form_json__metadata__timeStart"),
timeend_str=KT("form_json__metadata__timeEnd"),
location_str=KT("form_json__metadata__location"),
)
.select_related("deliver_unit", "opportunity", "opportunity__delivery_type", "opportunity__organization")
.values(
"opportunity_id",
"opportunity__delivery_type__name",
"opportunity__delivery_type__slug",
"opportunity__organization__slug",
"opportunity__organization__name",
"xform_id",
"visit_date",
"username_connectid",
"deliver_unit_name",
"entity_id",
"status",
"flagged",
"flag_reason",
"reason",
"timestart_str",
"timeend_str",
"location_str",
)
)
37 changes: 25 additions & 12 deletions commcare_connect/reports/tests/test_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,32 @@ def test_delivery_stats(opportunity: Opportunity):


def test_results_to_geojson():
class MockQuerySet:
def __init__(self, results):
self.results = results

def all(self):
return self.results

# Test input
results = [
{"gps_location_long": "10.123", "gps_location_lat": "20.456", "status": "approved", "other_field": "value1"},
{"gps_location_long": "30.789", "gps_location_lat": "40.012", "status": "rejected", "other_field": "value2"},
{"gps_location_long": "invalid", "gps_location_lat": "50.678", "status": "unknown", "other_field": "value3"},
{"status": "approved", "other_field": "value4"}, # Case where lat/lon are not present
{ # Case where lat/lon are null
"gps_location_long": None,
"gps_location_lat": None,
"status": "rejected",
"other_field": "value5",
},
]
results = MockQuerySet(
[
{"location_str": "20.456 10.123 0 0", "status": "approved", "other_field": "value1"},
{"location_str": "40.012 30.789", "status": "rejected", "other_field": "value2"},
{"location_str": "invalid location", "status": "unknown", "other_field": "value3"},
{"location_str": "bad location", "status": "unknown", "other_field": "value4"},
{
"location_str": None,
"status": "approved",
"other_field": "value5",
}, # Case where lat/lon are not present
{ # Case where lat/lon are null
"location_str": None,
"status": "rejected",
"other_field": "value5",
},
]
)

# Call the function
geojson = _results_to_geojson(results)
Expand Down
3 changes: 2 additions & 1 deletion commcare_connect/reports/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

urlpatterns = [
path("program_dashboard", views.program_dashboard_report, name="program_dashboard_report"),
path("api/visit_map_data/", views.visit_map_data, name="visit_map_data"),
path("delivery_stats", view=views.DeliveryStatsReportView.as_view(), name="delivery_stats_report"),
path("api/visit_map_data/", views.visit_map_data, name="visit_map_data"),
path("api/dashboard_stats/", views.dashboard_stats_api, name="dashboard_stats_api"),
]
148 changes: 125 additions & 23 deletions commcare_connect/reports/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from datetime import date, datetime
from datetime import date, datetime, timedelta

import django_filters
import django_tables2 as tables
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Column, Layout, Row
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db import connection
from django.db.models import Max, Q, Sum
from django.http import JsonResponse
from django.shortcuts import render
Expand All @@ -16,7 +17,9 @@
from django_filters.views import FilterView

from commcare_connect.cache import quickcache
from commcare_connect.opportunity.models import CompletedWork, CompletedWorkStatus, DeliveryType, Payment
from commcare_connect.opportunity.models import CompletedWork, CompletedWorkStatus, DeliveryType, Payment, UserVisit
from commcare_connect.organization.models import Organization
from commcare_connect.reports.queries import get_visit_map_queryset

from .tables import AdminReportTable

Expand Down Expand Up @@ -151,35 +154,102 @@ def _get_table_data_for_quarter(quarter, delivery_type, group_by_delivery_type=F
return data


class DashboardFilters(django_filters.FilterSet):
program = django_filters.ModelChoiceFilter(
queryset=DeliveryType.objects.all(),
field_name="opportunity__delivery_type",
label="Program",
empty_label="All Programs",
required=False,
)
organization = django_filters.ModelChoiceFilter(
queryset=Organization.objects.all(),
field_name="opportunity__organization",
label="Organization",
empty_label="All Organizations",
required=False,
)
from_date = django_filters.DateTimeFilter(
widget=forms.DateInput(attrs={"type": "date"}),
field_name="visit_date",
lookup_expr="gt",
label="From Date",
required=False,
)
to_date = django_filters.DateTimeFilter(
widget=forms.DateInput(attrs={"type": "date"}),
field_name="visit_date",
lookup_expr="lte",
label="To Date",
required=False,
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form.helper = FormHelper()
self.form.helper.form_class = "form-inline"
self.form.helper.layout = Layout(
Row(
Column("program", css_class="col-md-3"),
Column("organization", css_class="col-md-3"),
Column("from_date", css_class="col-md-3"),
Column("to_date", css_class="col-md-3"),
)
)

# Set default values if no data is provided
if not self.data:
# Create a mutable copy of the QueryDict
self.data = self.data.copy() if self.data else {}

# Set default dates
today = date.today()
default_from = today - timedelta(days=90)

# Set the default values
self.data["to_date"] = today.strftime("%Y-%m-%d")
self.data["from_date"] = default_from.strftime("%Y-%m-%d")

# Force the form to bind with the default data
self.form.is_bound = True
self.form.data = self.data

class Meta:
model = UserVisit
fields = ["program", "organization", "from_date", "to_date"]


@login_required
@user_passes_test(lambda user: user.is_superuser)
@require_GET
@user_passes_test(lambda u: u.is_superuser)
def program_dashboard_report(request):
filterset = DashboardFilters(request.GET)

return render(
request,
"reports/dashboard.html",
context={"mapbox_token": settings.MAPBOX_TOKEN},
context={
"mapbox_token": settings.MAPBOX_TOKEN,
"filter": filterset,
},
)


@login_required
@user_passes_test(lambda user: user.is_superuser)
@require_GET
def visit_map_data(request):
with connection.cursor() as cursor:
# Read the SQL file
with open("commcare_connect/reports/sql/visit_map.sql") as sql_file:
sql_query = sql_file.read()
filterset = DashboardFilters(request.GET)

# Use the filtered queryset to calculate stats

# Execute the query
cursor.execute(sql_query)
queryset = UserVisit.objects.all()
if filterset.is_valid():
queryset = filterset.filter_queryset(queryset)

# Fetch all results
columns = [col[0] for col in cursor.description]
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
queryset = get_visit_map_queryset(queryset)

# Convert to GeoJSON
geojson = _results_to_geojson(results)
geojson = _results_to_geojson(queryset)

# Return the GeoJSON as JSON response
return JsonResponse(geojson, safe=False)
Expand All @@ -191,14 +261,20 @@ def _results_to_geojson(results):
"approved": "#00FF00",
"rejected": "#FF0000",
}
for result in results:
for i, result in enumerate(results.all()):
location_str = result.get("location_str")
# Check if both latitude and longitude are not None and can be converted to float
if result.get("gps_location_long") and result.get("gps_location_lat"):
try:
longitude = float(result["gps_location_long"])
latitude = float(result["gps_location_lat"])
except ValueError:
# Skip this result if conversion to float fails
if location_str:
split_location = location_str.split(" ")
if len(split_location) >= 2:
try:
longitude = float(split_location[1])
latitude = float(split_location[0])
except ValueError:
# Skip this result if conversion to float fails
continue
else:
# Or if the location string is not in the expected format
continue

feature = {
Expand Down Expand Up @@ -317,3 +393,29 @@ def object_list(self):
data = _get_table_data_for_quarter(q, delivery_type, group_by_delivery_type)
table_data += data
return table_data


@login_required
@user_passes_test(lambda u: u.is_superuser)
def dashboard_stats_api(request):
filterset = DashboardFilters(request.GET)

# Use the filtered queryset to calculate stats
queryset = UserVisit.objects.all()
if filterset.is_valid():
queryset = filterset.filter_queryset(queryset)

# Example stats calculation (adjust based on your needs)
active_users = queryset.values("opportunity_access__user").distinct().count()
total_visits = queryset.count()
verified_visits = queryset.filter(status=CompletedWorkStatus.approved).count()
percent_verified = round(float(verified_visits / total_visits) * 100, 1) if total_visits > 0 else 0

return JsonResponse(
{
"total_visits": total_visits,
"active_users": active_users,
"verified_visits": verified_visits,
"percent_verified": f"{percent_verified:.1f}%",
}
)
Loading

0 comments on commit 764f7dd

Please sign in to comment.