From 932479f9991f6865658696afa51ed483a30468b9 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Thu, 27 Nov 2025 19:30:27 +0200 Subject: [PATCH 1/2] Version bump --- src/epiportal/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/epiportal/settings.py b/src/epiportal/settings.py index eb725d9..4cc0c68 100644 --- a/src/epiportal/settings.py +++ b/src/epiportal/settings.py @@ -25,7 +25,7 @@ from sentry_sdk.integrations.redis import RedisIntegration APP_VERSION = "1.0.14" -ALTERNATIVE_INTERFACE_VERSION = "1.0.1" +ALTERNATIVE_INTERFACE_VERSION = "1.0.2" EPIVIS_URL = os.environ.get("EPIVIS_URL", "https://delphi.cmu.edu/epivis/") From 29f29232aaa95cea78a2dcfc8b07a68cc6089564 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Tue, 2 Dec 2025 00:33:10 +0200 Subject: [PATCH 2/2] Added seprate model for the expess interface indicators --- src/alternative_interface/admin.py | 14 +++- .../migrations/0001_initial.py | 62 +++++++++++++++++ src/alternative_interface/models.py | 36 +++++++++- src/alternative_interface/resources.py | 54 +++++++++++++++ src/alternative_interface/views.py | 66 ++++++++++++------- src/epiportal/settings.py | 2 +- .../alter_dashboard.html | 4 +- 7 files changed, 209 insertions(+), 29 deletions(-) create mode 100644 src/alternative_interface/migrations/0001_initial.py create mode 100644 src/alternative_interface/resources.py diff --git a/src/alternative_interface/admin.py b/src/alternative_interface/admin.py index 8c38f3f..3d22e1e 100644 --- a/src/alternative_interface/admin.py +++ b/src/alternative_interface/admin.py @@ -1,3 +1,15 @@ from django.contrib import admin -# Register your models here. +from import_export.admin import ImportExportModelAdmin + +from alternative_interface.models import ExpressViewIndicator +from alternative_interface.resources import ExpressViewIndicatorResource + + +@admin.register(ExpressViewIndicator) +class ExpressViewIndicatorAdmin(ImportExportModelAdmin): + resource_class = ExpressViewIndicatorResource + list_display = ["menu_item", "indicator", "display_name"] + search_fields = ["menu_item", "indicator", "display_name"] + list_filter = ["menu_item", "indicator"] + ordering = ["menu_item", "indicator"] diff --git a/src/alternative_interface/migrations/0001_initial.py b/src/alternative_interface/migrations/0001_initial.py new file mode 100644 index 0000000..71d8b86 --- /dev/null +++ b/src/alternative_interface/migrations/0001_initial.py @@ -0,0 +1,62 @@ +# Generated by Django 5.2.5 on 2025-11-28 18:20 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("indicators", "0008_alter_indicator_source_type"), + ] + + operations = [ + migrations.CreateModel( + name="ExpressViewIndicator", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "menu_item", + models.CharField(max_length=255, verbose_name="Menu Item"), + ), + ( + "display_name", + models.CharField(max_length=255, verbose_name="Display Name"), + ), + ( + "indicator", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="indicators.indicator", + verbose_name="Indicator", + ), + ), + ], + options={ + "verbose_name": "Express View Indicator", + "verbose_name_plural": "Express View Indicators", + "ordering": ["menu_item", "indicator"], + "indexes": [ + models.Index( + fields=["menu_item", "indicator"], name="expr_view_ind_menu_idx" + ) + ], + "constraints": [ + models.UniqueConstraint( + fields=("menu_item", "indicator"), + name="uniq_expr_view_menu_ind", + ) + ], + }, + ), + ] diff --git a/src/alternative_interface/models.py b/src/alternative_interface/models.py index 71a8362..d6ae8f7 100644 --- a/src/alternative_interface/models.py +++ b/src/alternative_interface/models.py @@ -1,3 +1,37 @@ from django.db import models -# Create your models here. + +class ExpressViewIndicator(models.Model): + menu_item = models.CharField( + verbose_name="Menu Item", + max_length=255, + ) + indicator = models.ForeignKey( + "indicators.Indicator", + verbose_name="Indicator", + on_delete=models.PROTECT, + ) + display_name = models.CharField( + verbose_name="Display Name", + max_length=255, + ) + + class Meta: + verbose_name = "Express View Indicator" + verbose_name_plural = "Express View Indicators" + ordering = ["menu_item", "indicator"] + indexes = [ + models.Index( + fields=["menu_item", "indicator"], + name="expr_view_ind_menu_idx", + ), + ] + constraints = [ + models.UniqueConstraint( + fields=["menu_item", "indicator"], + name="uniq_expr_view_menu_ind", + ), + ] + + def __str__(self): + return self.display_name diff --git a/src/alternative_interface/resources.py b/src/alternative_interface/resources.py new file mode 100644 index 0000000..156a08b --- /dev/null +++ b/src/alternative_interface/resources.py @@ -0,0 +1,54 @@ +from import_export import resources +from import_export.fields import Field +from import_export.widgets import ForeignKeyWidget +from indicators.models import Indicator + +from alternative_interface.models import ExpressViewIndicator + + +def process_indicator(row): + indicator_source = row.get("Indicator Source") + indicator_name = row.get("Indicator Name") + indicator = None + if indicator_name and indicator_source: + try: + indicator = Indicator.objects.get( + name=indicator_name, source__name=indicator_source + ) + except Indicator.DoesNotExist: + indicator = None + if indicator: + row["Indicator Name"] = indicator.id + else: + row["Indicator Name"] = None + + +class ExpressViewIndicatorResource(resources.ModelResource): + menu_item = Field(attribute="menu_item", column_name="Menu Item") + indicator = Field( + attribute="indicator", + column_name="Indicator Name", + widget=ForeignKeyWidget(Indicator), + ) + display_name = Field( + attribute="display_name", column_name="text for display legend" + ) + + def before_import_row(self, row, **kwargs): + process_indicator(row) + + def skip_row(self, instance, original, row, import_validation_errors=None): + if not row["Indicator Name"]: + return True + + class Meta: + model = ExpressViewIndicator + fields = ( + "menu_item", + "indicator", + "display_name", + ) + import_id_fields = ("menu_item", "indicator") + skip_unchanged = True + report_skipped = True + exclude = ("id",) diff --git a/src/alternative_interface/views.py b/src/alternative_interface/views.py index cda2fc4..4f8835b 100644 --- a/src/alternative_interface/views.py +++ b/src/alternative_interface/views.py @@ -1,11 +1,18 @@ from django.shortcuts import render -from indicators.models import Indicator -from base.models import Pathogen +from django.db.models import Case, When, Value, IntegerField +from alternative_interface.models import ExpressViewIndicator from epiportal.settings import ALTERNATIVE_INTERFACE_VERSION from alternative_interface.utils import get_available_geos, get_chart_data +MENU_ITEMS_DISPLAY_ORDER_NUMBER = { + "Influenza": 1, + "COVID-19": 2, + "RSV": 3, + "Influenza-Like Illness (ILI)": 4, +} + HEADER_DESCRIPTION = "Discover, display and download real-time infectious disease indicators (time series) that track a variety of pathogens, diseases and syndromes in a variety of locations (primarily within the USA). Browse the list, or filter it first by locations and pathogens of interest, by surveillance categories, and more. Expand any row to expose and select from a set of related indicators, then hit 'Show Selected Indicators' at bottom to plot or export your selected indicators, or to generate code snippets to retrieve them from the Delphi Epidata API. Most indicators are served from the Delphi Epidata real-time repository, but some may be available only from third parties or may require prior approval." @@ -20,41 +27,52 @@ def alternative_interface_view(request): ctx["selected_pathogen"] = pathogen_filter ctx["selected_geography"] = geography_filter - # Build queryset with optional filtering - indicators_qs = Indicator.objects.filter( - use_in_express_interface=True - ).prefetch_related("pathogens", "available_geographies", "indicator_set") - # Fetch pathogens for dropdown - pathogens_qs = Pathogen.objects.filter( - id__in=indicators_qs.values_list("pathogens", flat=True) - ).order_by("display_order_number") - ctx["pathogens"] = list[Pathogen](pathogens_qs) - - if pathogen_filter: - indicators_qs = indicators_qs.filter( - pathogens__id=pathogen_filter, + pathogens_qs = ( + ExpressViewIndicator.objects.annotate( + order_number=Case( + *[ + When(menu_item=key, then=Value(value)) + for key, value in MENU_ITEMS_DISPLAY_ORDER_NUMBER.items() + ], + default=Value(999), + output_field=IntegerField(), + ) ) + .values("menu_item", "order_number") + .distinct() + .order_by("order_number") + ) + pathogens = [item["menu_item"] for item in pathogens_qs] + ctx["pathogens"] = pathogens + + indicators_qs = ExpressViewIndicator.objects.filter( + menu_item=pathogen_filter + ).prefetch_related("indicator") # Convert to list of dictionaries ctx["indicators"] = [ { "_endpoint": ( - indicator.indicator_set.epidata_endpoint - if indicator.indicator_set + indicator.indicator.indicator_set.epidata_endpoint + if indicator.indicator.indicator_set else "" ), - "name": indicator.name, - "data_source": indicator.source.name if indicator.source else "Unknown", - "time_type": indicator.time_type, + "name": indicator.indicator.name, + "data_source": ( + indicator.indicator.source.name + if indicator.indicator.source + else "Unknown" + ), + "time_type": indicator.indicator.time_type, "indicator_set_short_name": ( - indicator.indicator_set.short_name - if indicator.indicator_set + indicator.indicator.indicator_set.short_name + if indicator.indicator.indicator_set else "Unknown" ), "member_short_name": ( - indicator.member_short_name - if indicator.member_short_name + indicator.indicator.member_short_name + if indicator.indicator.member_short_name else "Unknown" ), } diff --git a/src/epiportal/settings.py b/src/epiportal/settings.py index 4cc0c68..9d03d24 100644 --- a/src/epiportal/settings.py +++ b/src/epiportal/settings.py @@ -25,7 +25,7 @@ from sentry_sdk.integrations.redis import RedisIntegration APP_VERSION = "1.0.14" -ALTERNATIVE_INTERFACE_VERSION = "1.0.2" +ALTERNATIVE_INTERFACE_VERSION = "1.0.3" EPIVIS_URL = os.environ.get("EPIVIS_URL", "https://delphi.cmu.edu/epivis/") diff --git a/src/templates/alternative_interface/alter_dashboard.html b/src/templates/alternative_interface/alter_dashboard.html index 0d0a126..c9aa951 100644 --- a/src/templates/alternative_interface/alter_dashboard.html +++ b/src/templates/alternative_interface/alter_dashboard.html @@ -82,8 +82,8 @@

Respiratory Diseases Dashboard