-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/snt 293 impact data integration #192
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
base: main
Are you sure you want to change the base?
Changes from all commits
dc90d3d
09042da
f0ce0ab
4d27951
48caefc
7f1b647
0ff0a36
b928198
665d944
721d229
476037a
0369354
2997845
e9deece
af7385d
40245d9
9d0e30e
9745feb
ac34793
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| from rest_framework import serializers | ||
|
|
||
| from plugins.snt_malaria.models import Scenario | ||
|
|
||
|
|
||
| # -- Request serializers ----------------------------------------------------- | ||
|
|
||
|
|
||
| class ImpactQuerySerializer(serializers.Serializer): | ||
| scenario_id = serializers.PrimaryKeyRelatedField( | ||
| queryset=Scenario.objects.none(), | ||
| source="scenario", | ||
| ) | ||
| age_group = serializers.CharField() | ||
| year_from = serializers.IntegerField(required=False, allow_null=True, default=None) | ||
| year_to = serializers.IntegerField(required=False, allow_null=True, default=None) | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| super().__init__(*args, **kwargs) | ||
| user = self.context["request"].user | ||
| account = user.iaso_profile.account | ||
| self.fields["scenario_id"].queryset = Scenario.objects.filter(account=account) | ||
|
|
||
|
|
||
| # -- Response serializers ---------------------------------------------------- | ||
|
|
||
|
|
||
| class MetricWithCISerializer(serializers.Serializer): | ||
| value = serializers.FloatField(allow_null=True) | ||
| lower = serializers.FloatField(allow_null=True) | ||
| upper = serializers.FloatField(allow_null=True) | ||
|
Comment on lines
+29
to
+31
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use |
||
|
|
||
|
|
||
| class ImpactMetricsSerializer(serializers.Serializer): | ||
| number_cases = MetricWithCISerializer() | ||
| number_severe_cases = MetricWithCISerializer() | ||
| prevalence_rate = MetricWithCISerializer() | ||
| averted_cases = MetricWithCISerializer() | ||
| direct_deaths = MetricWithCISerializer() | ||
| cost = serializers.FloatField(allow_null=True) | ||
| cost_per_averted_case = MetricWithCISerializer() | ||
|
|
||
|
|
||
| class OrgUnitMetricsSerializer(ImpactMetricsSerializer): | ||
| org_unit_id = serializers.IntegerField() | ||
| org_unit_name = serializers.CharField() | ||
|
|
||
|
|
||
| class YearMetricsSerializer(ImpactMetricsSerializer): | ||
| year = serializers.IntegerField() | ||
| org_units = OrgUnitMetricsSerializer(many=True) | ||
|
|
||
|
|
||
| class ScenarioImpactSerializer(ImpactMetricsSerializer): | ||
| scenario_id = serializers.IntegerField() | ||
| by_year = YearMetricsSerializer(many=True) | ||
| org_units = OrgUnitMetricsSerializer(many=True) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| from rest_framework import viewsets | ||
| from rest_framework.exceptions import NotFound | ||
| from rest_framework.response import Response | ||
|
|
||
| from plugins.snt_malaria.providers.impact import get_provider_for_account | ||
| from plugins.snt_malaria.services.impact import ImpactService | ||
|
|
||
| from .serializers import ImpactQuerySerializer, ScenarioImpactSerializer | ||
|
|
||
|
|
||
| def _get_provider(request): | ||
| """Resolve the ImpactProvider for the requesting user's account.""" | ||
| account = request.user.iaso_profile.account | ||
| provider = get_provider_for_account(account) | ||
| if provider is None: | ||
| raise NotFound("No impact data provider configured for this account.") | ||
| return provider | ||
|
|
||
|
|
||
| class ImpactYearRangeViewSet(viewsets.ViewSet): | ||
| http_method_names = ["get", "options"] | ||
|
|
||
| def list(self, request): | ||
| provider = _get_provider(request) | ||
| min_year, max_year = provider.get_year_range() | ||
| return Response({"min_year": min_year, "max_year": max_year}) | ||
|
|
||
|
|
||
| class ImpactAgeGroupsViewSet(viewsets.ViewSet): | ||
| http_method_names = ["get", "options"] | ||
|
|
||
| def list(self, request): | ||
| provider = _get_provider(request) | ||
| age_groups = provider.get_age_groups() | ||
| return Response({"age_groups": age_groups}) | ||
|
|
||
|
|
||
| class ImpactViewSet(viewsets.ViewSet): | ||
michiwend marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| http_method_names = ["get", "options"] | ||
|
|
||
| def list(self, request): | ||
| provider = _get_provider(request) | ||
|
|
||
| serializer = ImpactQuerySerializer(data=request.query_params, context={"request": request}) | ||
| serializer.is_valid(raise_exception=True) | ||
| data = serializer.validated_data | ||
|
|
||
| service = ImpactService(provider) | ||
| result = service.get_scenario_impact( | ||
| scenario=data["scenario"], | ||
| age_group=data["age_group"], | ||
| year_from=data["year_from"], | ||
| year_to=data["year_to"], | ||
| ) | ||
| return Response(ScenarioImpactSerializer(result).data) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Generated by Django 4.2 | ||
|
|
||
| from django.db import migrations, models | ||
| import django.db.models.deletion | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another |
||
|
|
||
| dependencies = [ | ||
| ("snt_malaria", "0020_budget_population_input"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name="ImpactProviderConfig", | ||
| fields=[ | ||
| ( | ||
| "id", | ||
| models.AutoField( | ||
| auto_created=True, | ||
| primary_key=True, | ||
| serialize=False, | ||
| verbose_name="ID", | ||
| ), | ||
| ), | ||
| ( | ||
| "provider_key", | ||
| models.CharField( | ||
| choices=[("swisstph", "SwissTPH"), ("idm", "IDM")], | ||
| help_text="The impact data provider to use for this account.", | ||
| max_length=50, | ||
| ), | ||
| ), | ||
| ( | ||
| "account", | ||
| models.OneToOneField( | ||
| on_delete=django.db.models.deletion.CASCADE, | ||
| related_name="impact_provider_config", | ||
| to="iaso.account", | ||
| ), | ||
| ), | ||
| ], | ||
| options={ | ||
| "verbose_name": "Impact provider configuration", | ||
| "verbose_name_plural": "Impact provider configurations", | ||
| }, | ||
| ), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| from django.db import models | ||
|
|
||
|
|
||
| class IDMAdminInfo(models.Model): | ||
| """Geographic administrative units (LGAs) with population data in the IDM database.""" | ||
michiwend marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| class Meta: | ||
| app_label = "snt_malaria" | ||
| managed = False | ||
| db_table = "admin_info" | ||
|
|
||
| id = models.AutoField(primary_key=True) | ||
michiwend marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| admin_2_name = models.CharField(max_length=255, null=True, blank=True) | ||
| state = models.CharField(max_length=255, null=True, blank=True) | ||
michiwend marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| population = models.IntegerField(null=True, blank=True) | ||
|
|
||
| def __str__(self): | ||
| return f"{self.admin_2_name} ({self.state})" | ||
|
|
||
|
|
||
| class IDMInterventionPackage(models.Model): | ||
| """Available intervention types in the IDM database.""" | ||
|
|
||
| class Meta: | ||
| app_label = "snt_malaria" | ||
| managed = False | ||
| db_table = "intervention_package" | ||
|
|
||
| id = models.IntegerField(primary_key=True) | ||
| option = models.CharField(max_length=255, null=True, blank=True) | ||
| type = models.CharField(max_length=255, null=True, blank=True) | ||
| description = models.CharField(max_length=255, null=True, blank=True) | ||
|
|
||
| def __str__(self): | ||
| return f"{self.option} ({self.id})" | ||
|
|
||
|
|
||
| class IDMCoverage(models.Model): | ||
| """Coverage level options in the IDM database.""" | ||
|
|
||
| class Meta: | ||
| app_label = "snt_malaria" | ||
| managed = False | ||
| db_table = "coverage" | ||
|
|
||
| id = models.IntegerField(primary_key=True) | ||
| option = models.CharField(max_length=255, null=True, blank=True) | ||
| key = models.CharField(max_length=255, null=True, blank=True) | ||
| description = models.CharField(max_length=255, null=True, blank=True) | ||
|
|
||
| def __str__(self): | ||
| return f"{self.option} ({self.id})" | ||
|
|
||
|
|
||
| class IDMAgeGroup(models.Model): | ||
| """Age group demographics in the IDM database.""" | ||
|
|
||
| class Meta: | ||
| app_label = "snt_malaria" | ||
| managed = False | ||
| db_table = "age_group" | ||
|
|
||
| id = models.IntegerField(primary_key=True) | ||
| option = models.CharField(max_length=255, null=True, blank=True) | ||
|
|
||
| def __str__(self): | ||
| return f"{self.option} ({self.id})" | ||
|
|
||
|
|
||
| class IDMModelOutput(models.Model): | ||
| """Main epidemiological simulation results from the IDM database. | ||
|
|
||
| Each row represents simulation output for a specific admin unit, year, | ||
| age group, and combination of interventions with their coverage levels. | ||
| """ | ||
|
|
||
| class Meta: | ||
| app_label = "snt_malaria" | ||
| managed = False | ||
| db_table = "model_output" | ||
|
|
||
| id = models.AutoField(primary_key=True) | ||
|
|
||
| # Admin info FK | ||
| admin_info_ref = models.ForeignKey( | ||
| IDMAdminInfo, | ||
| on_delete=models.DO_NOTHING, | ||
| db_column="admin_info", | ||
| null=True, | ||
| blank=True, | ||
| ) | ||
|
|
||
| year = models.SmallIntegerField(null=True, blank=True) | ||
|
|
||
| # Age group FK | ||
| age_group_ref = models.ForeignKey( | ||
| IDMAgeGroup, | ||
| on_delete=models.DO_NOTHING, | ||
| db_column="age_group", | ||
| null=True, | ||
| blank=True, | ||
| ) | ||
|
|
||
| # Intervention columns (FK to intervention_package) | ||
| cm = models.SmallIntegerField(null=True, blank=True) | ||
| cm_coverage = models.SmallIntegerField(null=True, blank=True) | ||
| cm_subsidy = models.SmallIntegerField(null=True, blank=True) | ||
| cm_subsidy_coverage = models.SmallIntegerField(null=True, blank=True) | ||
| smc = models.SmallIntegerField(null=True, blank=True) | ||
| smc_coverage = models.SmallIntegerField(null=True, blank=True) | ||
| itn_c = models.SmallIntegerField(null=True, blank=True) | ||
| itn_c_coverage = models.SmallIntegerField(null=True, blank=True) | ||
| itn_r = models.SmallIntegerField(null=True, blank=True) | ||
| itn_r_coverage = models.SmallIntegerField(null=True, blank=True) | ||
| irs = models.SmallIntegerField(null=True, blank=True) | ||
| irs_coverage = models.SmallIntegerField(null=True, blank=True) | ||
| vacc = models.SmallIntegerField(null=True, blank=True) | ||
| vacc_coverage = models.SmallIntegerField(null=True, blank=True) | ||
| iptp = models.SmallIntegerField(null=True, blank=True) | ||
| iptp_coverage = models.SmallIntegerField(null=True, blank=True) | ||
| lsm = models.SmallIntegerField(null=True, blank=True) | ||
| lsm_coverage = models.SmallIntegerField(null=True, blank=True) | ||
|
|
||
| # Outcome metrics | ||
| clinical_incidence = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
| clinical_incidence_lower = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
| clinical_incidence_higher = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
| severe_incidence = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
| severe_incidence_lower = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
| severe_incidence_higher = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
| prevalence = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
| prevalence_lower = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
| prevalence_higher = models.DecimalField( | ||
| max_digits=20, decimal_places=10, null=True, blank=True | ||
| ) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| from django.db import models | ||
|
|
||
|
|
||
| class ImpactProviderConfig(models.Model): | ||
| class Meta: | ||
| app_label = "snt_malaria" | ||
| verbose_name = "Impact provider configuration" | ||
| verbose_name_plural = "Impact provider configurations" | ||
|
|
||
| PROVIDER_CHOICES = [ | ||
| ("swisstph", "SwissTPH"), | ||
| ("idm", "IDM"), | ||
| ] | ||
|
Comment on lines
+10
to
+13
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We usually do a nested enum class in IASO, but I don't mind this |
||
|
|
||
| account = models.OneToOneField( | ||
| "iaso.Account", | ||
| on_delete=models.CASCADE, | ||
| related_name="impact_provider_config", | ||
| ) | ||
| provider_key = models.CharField( | ||
| max_length=50, | ||
| choices=PROVIDER_CHOICES, | ||
| help_text="The impact data provider to use for this account.", | ||
| ) | ||
|
|
||
| def __str__(self): | ||
| return f"{self.account.name} - {self.get_provider_key_display()}" | ||
Uh oh!
There was an error while loading. Please reload this page.