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

Operational learning stats API #2335

Merged
80 changes: 78 additions & 2 deletions per/drf_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytz
from django.conf import settings
from django.db import transaction
from django.db.models import Prefetch, Q
from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import get_language as django_get_language
Expand All @@ -20,7 +20,7 @@
from rest_framework.response import Response
from rest_framework.settings import api_settings

from api.models import Country
from api.models import Appeal, AppealType, Country
from deployments.models import SectorTag
from main.permissions import DenyGuestUserMutationPermission, DenyGuestUserPermission
from main.utils import SpreadSheetContentNegotiation
Expand Down Expand Up @@ -921,6 +921,82 @@ def summary(self, request):
)
return response.Response(OpsLearningSummarySerializer(ops_learning_summary_instance).data)

@action(
detail=False,
methods=["GET"],
permission_classes=[DenyGuestUserMutationPermission, OpsLearningPermission],
url_path="stats",
)
def stats(self, request):
"""
Get the Ops Learning stats based on the filters
"""
ops_data = (
super()
.get_queryset()
.filter(is_validated=True)
.select_related("appeal_code")
.prefetch_related(
"appeal_code__appealdocument",
"sector_validated",
)
.aggregate(
operations_included=Count("appeal_code", distinct=True),
learning_extracts=Count("id", distinct=True),
sector_covered=Count("sector_validated", distinct=True),
source_used=Count("appeal_code__appealdocument", distinct=True),
)
)

learning_by_sector = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add _qs as postfix (learning_by_sector_qs) for all querysets variables

SectorTag.objects.filter(title__isnull=False)
.annotate(count=Count("validated_sectors", distinct=True))
susilnem marked this conversation as resolved.
Show resolved Hide resolved
.values("title", "count")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to add 'id', this will be used by client as unique key

)

sources_overtime = {
str(appeal_type_label): OpsLearning.objects.filter(appeal_code__atype=appeal_type, is_validated=True)
.annotate(date=F(("appeal_code__start_date")))
.values("date")
.annotate(count=Count("appeal_code__appealdocument", distinct=True))
.order_by("date")
for appeal_type, appeal_type_label in AppealType.choices
}

region_subquery = Appeal.objects.filter(code=OuterRef("appeal_code"), region__isnull=False).values("region__label")[:1]
susilnem marked this conversation as resolved.
Show resolved Hide resolved
region_id = Appeal.objects.filter(code=OuterRef("appeal_code"), region__isnull=False).values("region__id")[:1]

learning_by_region = (
OpsLearning.objects.filter(is_validated=True)
.annotate(name=Subquery(region_subquery))
.values("name")
.annotate(id=Subquery(region_id), count=Count("id", distinct=True))
.order_by("name")
)

country_subquery = Appeal.objects.filter(code=OuterRef("appeal_code"), country__isnull=False).values("country__name")[:1]
country_id = Appeal.objects.filter(code=OuterRef("appeal_code"), country__isnull=False).values("country__id")[:1]

learning_by_country = (
OpsLearning.objects.filter(is_validated=True)
.annotate(name=Subquery(country_subquery))
.values("name")
.annotate(id=Subquery(country_id), count=Count("id", distinct=True))
.order_by("name")
)

data = {
"operations_included": ops_data["operations_included"],
"learning_extracts": ops_data["learning_extracts"],
"sectors_covered": ops_data["sector_covered"],
"sources_used": ops_data["source_used"],
"learning_by_region": learning_by_region,
"learning_by_sector": learning_by_sector,
"sources_overtime": sources_overtime,
"learning_by_country": learning_by_country,
}
return response.Response(data)


class PerDocumentUploadViewSet(viewsets.ModelViewSet):
queryset = PerDocumentUpload.objects.all()
Expand Down
19 changes: 19 additions & 0 deletions per/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import factory
from factory import fuzzy

from api.factories.country import CountryFactory
from api.models import Appeal, AppealDocument
from deployments.factories.project import SectorTagFactory
from per.models import (
AssessmentType,
Expand Down Expand Up @@ -105,12 +107,22 @@ class Meta:
model = FormPrioritization


class AppealFactory(factory.django.DjangoModelFactory):
class Meta:
model = Appeal

country = factory.SubFactory(CountryFactory)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use factory.SubFactory, this will create objects without developer knowledge which can have weird confusing related to data generation, and it's effect.



class OpsLearningFactory(factory.django.DjangoModelFactory):
learning = fuzzy.FuzzyText(length=50)

class Meta:
model = OpsLearning

appeal_code = factory.SubFactory(AppealFactory)
is_validated = fuzzy.FuzzyChoice([True, False])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use fuzzy choice for is_validated, we should manually define in the test cases as lots of endpoints depends on this



class OpsLearningCacheResponseFactory(factory.django.DjangoModelFactory):
used_filters_hash = fuzzy.FuzzyText(length=20)
Expand Down Expand Up @@ -141,3 +153,10 @@ class OpsLearningComponentCacheResponseFactory(factory.django.DjangoModelFactory

class Meta:
model = OpsLearningComponentCacheResponse


class AppealDocumentFactory(factory.django.DjangoModelFactory):
class Meta:
model = AppealDocument

appeal = factory.SubFactory(AppealFactory)
95 changes: 95 additions & 0 deletions per/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
from unittest import mock

from api.factories.country import CountryFactory
from api.factories.region import RegionFactory
from api.models import AppealType
from main.test_case import APITestCase
from per.factories import (
AppealDocumentFactory,
AppealFactory,
FormAreaFactory,
FormComponentFactory,
FormPrioritizationFactory,
OpsLearningFactory,
OverviewFactory,
PerWorkPlanFactory,
SectorTagFactory,
)

from .models import WorkPlanStatus
Expand Down Expand Up @@ -224,3 +229,93 @@ def test_summary_generation(self, generate_summary):
}
self.check_response_id(url=url, data=filters)
self.assertTrue(generate_summary.assert_called)


class OpsLearningStatsTestCase(APITestCase):
@classmethod
def setUpTestData(cls):
cls.region = RegionFactory.create(label="Region A")
cls.country = CountryFactory.create(region=cls.region, name="Country A")

cls.sector1 = SectorTagFactory.create(title="Sector 1")
cls.sector2 = SectorTagFactory.create(title="Sector 2")

cls.appeal1 = AppealFactory.create(
region=cls.region, country=cls.country, code="APP001", atype=0, start_date="2023-01-01"
)
cls.appeal2 = AppealFactory.create(
region=cls.region, country=cls.country, code="APP002", atype=1, start_date="2023-02-01"
)

AppealDocumentFactory.create(appeal=cls.appeal1)
AppealDocumentFactory.create(appeal=cls.appeal2)

cls.ops_learning1 = OpsLearningFactory.create(is_validated=True, appeal_code=cls.appeal1)
cls.ops_learning1.sector_validated.set([cls.sector1])

cls.ops_learning2 = OpsLearningFactory.create(is_validated=False, appeal_code=cls.appeal2)
cls.ops_learning2.sector_validated.set([cls.sector2])

cls.ops_learning3 = OpsLearningFactory.create(is_validated=False, appeal_code=cls.appeal2)
cls.ops_learning3.sector_validated.set([cls.sector2])

def test_ops_learning_stats(self):
url = "/api/v2/ops-learning/stats/"
response = self.client.get(url)

self.assertEqual(response.status_code, 200)

expected_keys = [
"operations_included",
"sources_used",
"learning_extracts",
"sectors_covered",
"sources_overtime",
"learning_by_region",
"learning_by_sector",
"learning_by_country",
]
for key in expected_keys:
self.assertIn(key, response.data)

# Updated counts based on validated entries
self.assertEqual(response.data["operations_included"], 1)
self.assertEqual(response.data["sources_used"], 1)
self.assertEqual(response.data["learning_extracts"], 1)
self.assertEqual(response.data["sectors_covered"], 1)

# Validate learning by region
region_data = response.data["learning_by_region"]
self.assertEqual(len(region_data), 1)
self.assertEqual(region_data[0]["name"], "Region A")
self.assertEqual(region_data[0]["count"], 1)

# Validate learning by sector
sector_data = response.data["learning_by_sector"]
self.assertEqual(len(sector_data), 2)
self.assertEqual(sector_data[0]["title"], "Sector 1")
self.assertEqual(sector_data[0]["count"], 1)

# Validate learning by country
country_data = response.data["learning_by_country"]
self.assertEqual(len(country_data), 1)
self.assertEqual(country_data[0]["name"], "Country A")
self.assertEqual(country_data[0]["count"], 1)

# Validate sources overtime
for appeal_type, label in AppealType.choices:
self.assertIn(label, response.data["sources_overtime"])
for item in response.data["sources_overtime"][label]:
susilnem marked this conversation as resolved.
Show resolved Hide resolved
self.assertIn("date", item)
self.assertIn("count", item)

date_str = item["date"]
date_str_iso = date_str.replace(tzinfo=None).isoformat() + "Z"

if label == "DREF":
susilnem marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(date_str_iso, "2023-01-01T00:00:00Z")
susilnem marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(item["count"], 1)

elif label == "Emergency Appeal":
susilnem marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(date_str_iso, "2023-02-01T00:00:00Z")
self.assertEqual(item["count"], 0)
Loading