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 eb725d9..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.1" +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 @@