Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Program Dashboard Updates #422

Merged
merged 24 commits into from
Nov 6, 2024
Merged
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
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
Loading