diff --git a/src/assets/js/signal_sets.js b/src/assets/js/signal_sets.js index 676490e..c926b42 100644 --- a/src/assets/js/signal_sets.js +++ b/src/assets/js/signal_sets.js @@ -15,27 +15,32 @@ function showWarningAlert(warningMessage, slideUpTime = 2000) { }); } -function checkGeoCoverage(geoType, geoValue) { - var notCoveredSignals = []; - $.ajax({ - url: "epidata/covidcast/geo_coverage/", - type: 'GET', - async: false, - data: { - 'geo': `${geoType}:${geoValue}` - }, - success: function (result) { - checkedSignalMembers.forEach(signal => { - var covered = result["epidata"].some( - e => (e.source === signal.data_source && e.signal === signal.signal) - ) - if (!covered) { - notCoveredSignals.push(signal); - } - }) - } - }) - return notCoveredSignals; +async function checkGeoCoverage(geoType, geoValue) { + const notCoveredSignals = []; + + try { + const result = await $.ajax({ + url: "epidata/covidcast/geo_coverage/", + type: 'GET', + data: { + 'geo': `${geoType}:${geoValue}` + } + }); + + checkedSignalMembers.forEach(signal => { + const covered = result["epidata"].some( + e => (e.source === signal.data_source && e.signal === signal.signal) + ); + if (!covered) { + notCoveredSignals.push(signal); + } + }); + + return notCoveredSignals; + } catch (error) { + console.error('Error fetching geo coverage:', error); + return notCoveredSignals; + } } @@ -123,6 +128,18 @@ function addSelectedSignal(element) { } } +$("#showSelectedSignalsButton").click(function() { + alertPlaceholder.innerHTML = ""; + $('#geographic_value').select2("data").forEach(geo => { + checkGeoCoverage(geo.geoType, geo.id).then((notCoveredSignals) => { + if (notCoveredSignals.length > 0) { + showNotCoveredGeoWarningMessage(notCoveredSignals, geo.text); + } + }) + + }); +}); + // Add an event listener to each 'bulk-select' element let bulkSelectDivs = document.querySelectorAll('.bulk-select'); bulkSelectDivs.forEach(div => { @@ -439,15 +456,23 @@ function showNotCoveredGeoWarningMessage(notCoveredSignals, geoValue) { $('#geographic_value').on('select2:select', function (e) { var geo = e.params.data; - var notCoveredSignals = checkGeoCoverage(geo.geoType, geo.id) - if (notCoveredSignals.length > 0) { - showNotCoveredGeoWarningMessage(notCoveredSignals, geo.text); + checkGeoCoverage(geo.geoType, geo.id).then((notCoveredSignals) => { + if (notCoveredSignals.length > 0) { + showNotCoveredGeoWarningMessage(notCoveredSignals, geo.text); + } } + ); }); function submitMode(event) { event.preventDefault(); + var geographicValues = $('#geographic_value').select2('data'); + + if (geographicValues.length === 0) { + appendAlert("Please select at least one geographic location", "warning") + return; + } if (currentMode === 'epivis') { plotData(); diff --git a/src/datasources/resources.py b/src/datasources/resources.py index 15a337c..07aa6bd 100644 --- a/src/datasources/resources.py +++ b/src/datasources/resources.py @@ -27,11 +27,21 @@ def process_links(row, dua_column_name="DUA", link_column_name="Link"): links.append(link.id) else: for match in matches: - link, _ = Link.objects.get_or_create(url=match[1], defaults={'link_type': match[0], }) + link, _ = Link.objects.get_or_create( + url=match[1], + defaults={ + "link_type": match[0], + }, + ) links.append(link.id) row["Links"] = links +def process_datasource_name(row): + if row["Name"]: + row["Name"] = row["Name"].capitalize() + + def process_datasources(row): datasource, _ = DataSource.objects.get_or_create( name=row["DB Source"], @@ -67,5 +77,6 @@ class Meta: skip_unchanged = True def before_import_row(self, row, **kwargs): + process_datasource_name(row) process_links(row) process_datasources(row) diff --git a/src/fixtures/severity_pyramid_rungs.json b/src/fixtures/severity_pyramid_rungs.json index 36568e9..cff74c7 100644 --- a/src/fixtures/severity_pyramid_rungs.json +++ b/src/fixtures/severity_pyramid_rungs.json @@ -82,9 +82,10 @@ "fields": { "created": "2024-08-15T09:57:49.327Z", "modified": "2024-08-15T13:07:46.136Z", - "name": "Population", - "display_name": "Population", - "used_in": "signal_sets" + "name": "Entire Population", + "display_name": "Entire Population", + "used_in": "signal_sets", + "display_order_number": 1 } }, { @@ -95,7 +96,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Vaccinated", "display_name": "Vaccinated", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 2 } }, { @@ -106,7 +108,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Infected", "display_name": "Infected", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 3 } }, { @@ -117,7 +120,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Tested", "display_name": "Tested", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 4 } }, { @@ -128,7 +132,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Ascertained (Case)", "display_name": "Ascertained (Case)", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 5 } }, { @@ -139,7 +144,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Symptomatic", "display_name": "Symptomatic", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 6 } }, { @@ -150,7 +156,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Outpatient / ED", "display_name": "Outpatient / ED", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 7 } }, { @@ -161,7 +168,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Hospitalized", "display_name": "Hospitalized", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 8 } }, { @@ -172,7 +180,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "ICU", "display_name": "ICU", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 9 } }, { @@ -183,7 +192,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Deceased", "display_name": "Deceased", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 10 } } ] diff --git a/src/signal_sets/forms.py b/src/signal_sets/forms.py index 8c767cc..2dd9a6b 100644 --- a/src/signal_sets/forms.py +++ b/src/signal_sets/forms.py @@ -35,7 +35,7 @@ class SignalSetFilterForm(forms.ModelForm): queryset=SeverityPyramidRung.objects.filter( # id__in=SignalSet.objects.values_list("severity_pyramid_rungs", flat="True") used_in="signal_sets" - ), + ).order_by("display_order_number"), widget=forms.CheckboxSelectMultiple(), ) diff --git a/src/signal_sets/resources.py b/src/signal_sets/resources.py index 2eb437a..6390bfd 100644 --- a/src/signal_sets/resources.py +++ b/src/signal_sets/resources.py @@ -221,8 +221,10 @@ def skip_row(self, instance, original, row, import_validation_errors=None): def after_import_row(self, row, row_result, **kwargs): try: signal_set_obj = SignalSet.objects.get(id=row_result.object_id) + signal_set_obj.pathogens.clear() + signal_set_obj.severity_pyramid_rungs.clear() + signal_set_obj.available_geographies.clear() for pathogen in row["Pathogen(s)/Syndrome(s)"].split(","): - signal_set_obj.pathogens.clear() pathogen = Pathogen.objects.get(name=pathogen, used_in="signal_sets") signal_set_obj.pathogens.add(pathogen) for severity_pyramid_rung in row["Surveillance Categories"].split(","): @@ -231,7 +233,6 @@ def after_import_row(self, row, row_result, **kwargs): used_in="signal_sets" ).first() signal_set_obj.severity_pyramid_rungs.add(severity_pyramid_rung) - for available_geography in row["Geographic Granularity - Delphi"].split(","): available_geography = Geography.objects.get(name=available_geography, used_in="signal_sets") signal_set_obj.available_geographies.add(available_geography) diff --git a/src/signals/admin.py b/src/signals/admin.py index 552020e..531d8fa 100644 --- a/src/signals/admin.py +++ b/src/signals/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from signals.resources import SignalResource, SignalBaseResource +from signals.resources import SignalResource, SignalBaseResource, OtherEndpointSignalResource from signals.models import ( @@ -14,6 +14,7 @@ Pathogen, SeverityPyramidRung, Signal, + OtherEndointSignal, SignalType, SignalGeography, GeographyUnit, @@ -102,6 +103,7 @@ class SeverityPyramidRungAdmin(admin.ModelAdmin): "name", "display_name", "used_in", + "display_order_number", ) exclude = ("id",) search_fields: tuple[Literal["name"]] = ("name",) @@ -149,6 +151,35 @@ class SignalAdmin(ImportExportModelAdmin): resource_classes: list[type[SignalResource]] = [SignalResource, SignalBaseResource] +@admin.register(OtherEndointSignal) +class OtherEndpointsSignalAdmin(ImportExportModelAdmin): + """ + Admin interface for managing signal objects. + """ + + list_display: tuple[ + Literal["name"], + Literal["signal_type"], + Literal["format_type"], + Literal["category"], + Literal["geographic_scope"], + ] = ("name", "signal_type", "format_type", "category", "geographic_scope") + search_fields: tuple[ + Literal["name"], + Literal["signal_type__name"], + Literal["format_type__name"], + Literal["category__name"], + Literal["geographic_scope__name"], + ] = ( + "name", + "signal_type__name", + "format_type__name", + "category__name", + "geographic_scope__name", + ) + resource_classes: list[type[SignalResource]] = [OtherEndpointSignalResource] + + @admin.register(SignalGeography) class SignalGeographyAdmin(admin.ModelAdmin): """ diff --git a/src/signals/migrations/0017_severitypyramidrung_display_order_number.py b/src/signals/migrations/0017_severitypyramidrung_display_order_number.py new file mode 100644 index 0000000..3b60130 --- /dev/null +++ b/src/signals/migrations/0017_severitypyramidrung_display_order_number.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2025-03-12 17:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('signals', '0016_signal_source_view'), + ] + + operations = [ + migrations.AddField( + model_name='severitypyramidrung', + name='display_order_number', + field=models.IntegerField(help_text='Display order number of the severity pyramid rung.', null=True, verbose_name='display order number'), + ), + ] diff --git a/src/signals/migrations/0018_otherendointsignal.py b/src/signals/migrations/0018_otherendointsignal.py new file mode 100644 index 0000000..3542808 --- /dev/null +++ b/src/signals/migrations/0018_otherendointsignal.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.7 on 2025-03-27 18:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('signals', '0017_severitypyramidrung_display_order_number'), + ] + + operations = [ + migrations.CreateModel( + name='OtherEndointSignal', + fields=[ + ], + options={ + 'verbose_name': 'Other Endpoint Signal', + 'verbose_name_plural': 'Other Endpoint Signals', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('signals.signal',), + ), + ] diff --git a/src/signals/models.py b/src/signals/models.py index f9f7070..b55ca0b 100644 --- a/src/signals/models.py +++ b/src/signals/models.py @@ -81,6 +81,12 @@ class SeverityPyramidRung(TimeStampedModel): default="signals", ) + display_order_number: models.IntegerField = models.IntegerField( + verbose_name=_("display order number"), + help_text=_("Display order number of the severity pyramid rung."), + null=True, + ) + class Meta: verbose_name_plural: str = "Severity Pyramid Rungs" unique_together: list[str] = ["name", "used_in"] @@ -569,6 +575,13 @@ def get_display_name(self): return self.name +class OtherEndointSignal(Signal): + class Meta: + proxy = True + verbose_name = "Other Endpoint Signal" + verbose_name_plural = "Other Endpoint Signals" + + class SignalsDbView(models.Model): id = models.BigIntegerField(primary_key=True) name = models.CharField(max_length=255) diff --git a/src/signals/resources.py b/src/signals/resources.py index 267286d..f2bd827 100644 --- a/src/signals/resources.py +++ b/src/signals/resources.py @@ -155,9 +155,14 @@ def process_available_geographies(row) -> None: "display_order_number": max_display_order_number + 1, }, ) - signal = Signal.objects.get( - name=row["Indicator"], source=row["Source Subdivision"] - ) + try: + signal = Signal.objects.get( + name=row["Indicator"], source=row["Source Subdivision"] + ) + except KeyError: + signal = Signal.objects.get( + name=row["Signal"], source=row["Source Subdivision"] + ) signal_geography, _ = SignalGeography.objects.get_or_create( geography=geography_instance, signal=signal ) @@ -167,12 +172,12 @@ def process_available_geographies(row) -> None: def process_base(row) -> None: - if row["Indicator BaseName"]: + if row["Signal BaseName"]: source: SourceSubdivision = SourceSubdivision.objects.get( name=row["Source Subdivision"] ) base_signal: Signal = Signal.objects.get( - name=row["Indicator BaseName"], source=source + name=row["Signal BaseName"], source=source ) row["base"] = base_signal.id @@ -180,7 +185,7 @@ def process_base(row) -> None: class ModelResource(resources.ModelResource): def get_field_names(self): names = [] - for field in self.get_fields(): + for field in list(self.fields.values()): names.append(self.get_field_name(field)) return names @@ -213,7 +218,7 @@ class SignalBaseResource(ModelResource): Resource class for importing Signals base. """ - name = Field(attribute="name", column_name="Indicator") + name = Field(attribute="name", column_name="Signal") display_name = Field(attribute="display_name", column_name="Name") base = Field( attribute="base", @@ -241,6 +246,200 @@ class SignalResource(ModelResource): Resource class for importing and exporting Signal models """ + name = Field(attribute="name", column_name="Signal") + display_name = Field(attribute="display_name", column_name="Name") + member_name = Field(attribute="member_name", column_name="Member API Name") + member_short_name = Field( + attribute="member_short_name", column_name="Member Short Name" + ) + member_description = Field( + attribute="member_description", column_name="Member Description" + ) + pathogen = Field( + attribute="pathogen", + column_name="Pathogen/\nDisease Area", + widget=widgets.ManyToManyWidget(Pathogen, field="name", separator=","), + ) + signal_type = Field( + attribute="signal_type", + column_name="Indicator Type", + widget=widgets.ForeignKeyWidget(SignalType, field="name"), + ) + active = Field(attribute="active", column_name="Active") + description = Field(attribute="description", column_name="Description") + short_description = Field( + attribute="short_description", column_name="Short Description" + ) + format_type = Field( + attribute="format_type", + column_name="Format", + widget=widgets.ForeignKeyWidget(FormatType, field="name"), + ) + time_type = Field(attribute="time_type", column_name="Time Type") + time_label = Field(attribute="time_label", column_name="Time Label") + reporting_cadence = Field( + attribute="reporting_cadence", column_name="Reporting Cadence" + ) + typical_reporting_lag = Field( + attribute="typical_reporting_lag", column_name="Typical Reporting Lag" + ) + typical_revision_cadence = Field( + attribute="typical_revision_cadence", column_name="Typical Revision Cadence" + ) + demographic_scope = Field( + attribute="demographic_scope", column_name="Population" + ) + severity_pyramid_rung = Field( + attribute="severity_pyramid_rung", + column_name="Surveillance Categories", + widget=widgets.ForeignKeyWidget(SeverityPyramidRung), + ) + category = Field( + attribute="category", + column_name="Category", + widget=widgets.ForeignKeyWidget(Category, "name"), + ) + geographic_scope = Field( + attribute="geographic_scope", + column_name="Geographic Coverage", + widget=widgets.ForeignKeyWidget(GeographicScope), + ) + available_geographies = Field( + attribute="available_geography", + column_name="Geographic Levels", + widget=widgets.ManyToManyWidget(Geography, field="name", separator=","), + ) + temporal_scope_start = Field( + attribute="temporal_scope_start", column_name="Temporal Scope Start" + ) + temporal_scope_start_note = Field( + attribute="temporal_scope_start_note", column_name="Temporal Scope Start Note" + ) + temporal_scope_end = Field( + attribute="temporal_scope_end", column_name="Temporal Scope End" + ) + temporal_scope_end_note = Field( + attribute="temporal_scope_end_note", column_name="Temporal Scope End Note" + ) + is_smoothed = Field(attribute="is_smoothed", column_name="Is Smoothed") + is_weighted = Field(attribute="is_weighted", column_name="Is Weighted") + is_cumulative = Field(attribute="is_cumulative", column_name="Is Cumulative") + has_stderr = Field(attribute="has_stderr", column_name="Has StdErr") + has_sample_size = Field(attribute="has_sample_size", column_name="Has Sample Size") + high_values_are = Field(attribute="high_values_are", column_name="High Values Are") + source = Field( + attribute="source", + column_name="Source Subdivision", + widget=widgets.ForeignKeyWidget(SourceSubdivision, field="name"), + ) + data_censoring = Field(attribute="data_censoring", column_name="Data Censoring") + missingness = Field(attribute="missingness", column_name="Missingness") + organization_access_list = Field( + attribute="organization_access_list", column_name="Who may access this indicator?" + ) + organization_sharing_list = Field( + attribute="organization_sharing_list", + column_name="Who may be told about this indicator?", + ) + license = Field(attribute="license", column_name="Data Use Terms") + restrictions = Field(attribute="restrictions", column_name="Use Restrictions") + signal_set = Field( + attribute="signal_set", + column_name="Indicator Set", + widget=widgets.ForeignKeyWidget(SignalSet, field="name"), + ) + + class Meta: + model = Signal + fields: list[str] = [ + "name", + "display_name", + "member_name", + "member_short_name", + "member_description", + "pathogen", + "signal_type", + "active", + "description", + "short_description", + "time_label", + "reporting_cadence", + "typical_reporting_lag", + "typical_revision_cadence", + "demographic_scope", + "category", + "geographic_scope", + "available_geographies", + "temporal_scope_start", + "temporal_scope_start_note", + "temporal_scope_end", + "temporal_scope_end_note", + "is_smoothed", + "is_weighted", + "is_cumulative", + "has_stderr", + "has_sample_size", + "high_values_are", + "source", + "data_censoring", + "missingness", + "organization_access_list", + "organization_sharing_list", + "license", + "restrictions", + "time_type", + "signal_set", + "format_type", + "severity_pyramid_rung", + ] + import_id_fields: list[str] = ["name", "source"] + store_instance = True + skip_unchanged = True + + def before_import_row(self, row, **kwargs) -> None: + fix_boolean_fields(row) + process_pathogen(row) + process_signal_type(row) + process_format_type(row) + process_severity_pyramid_rungs(row) + process_category(row) + process_geographic_scope(row) + process_source(row) + process_links(row, dua_column_name="Link to DUA", link_column_name="Link") + if not row.get("Indicator Set"): + row["Indicator Set"] = None + if not row.get("Source Subdivision"): + row["Source Subdivision"] = None + + def skip_row(self, instance, original, row, import_validation_errors=None): + if not row["Include in indicator app"]: + try: + signal = Signal.objects.get( + name=row["Signal"], source=row["Source Subdivision"] + ) + signal.delete() + except Signal.DoesNotExist: + pass + return True + + def after_import_row(self, row, row_result, **kwargs): + try: + signal_obj = Signal.objects.get(id=row_result.object_id) + for link in row["Links"]: + signal_obj.related_links.add(link) + process_available_geographies(row) + signal_obj.severity_pyramid_rung = SeverityPyramidRung.objects.get(id=row["Surveillance Categories"]) + signal_obj.format_type = row["Format"] + signal_obj.save() + except Signal.DoesNotExist as e: + print(f"Signal.DoesNotExist: {e}") + + +class OtherEndpointSignalResource(ModelResource): + """ + Resource class for importing and exporting Signal models + """ + name = Field(attribute="name", column_name="Indicator") display_name = Field(attribute="display_name", column_name="Name") member_name = Field(attribute="member_name", column_name="Member API Name") diff --git a/src/templates/signal_sets/signal_sets.html b/src/templates/signal_sets/signal_sets.html index 16717ae..68ac88f 100644 --- a/src/templates/signal_sets/signal_sets.html +++ b/src/templates/signal_sets/signal_sets.html @@ -278,6 +278,18 @@