From abf37f681fcd8e9648caa27532a0bb52170cdf13 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Wed, 19 Mar 2025 21:23:28 +0200 Subject: [PATCH 1/8] Added display order number to the severity pyramid rungs, fixed pathogens import for signal sets --- src/datasources/resources.py | 13 ++++++- src/fixtures/severity_pyramid_rungs.json | 34 ++++++++++++------- src/signal_sets/forms.py | 2 +- src/signal_sets/resources.py | 5 +-- src/signals/admin.py | 1 + ...everitypyramidrung_display_order_number.py | 18 ++++++++++ src/signals/models.py | 6 ++++ 7 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 src/signals/migrations/0017_severitypyramidrung_display_order_number.py 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..09c0cf7 100644 --- a/src/signals/admin.py +++ b/src/signals/admin.py @@ -102,6 +102,7 @@ class SeverityPyramidRungAdmin(admin.ModelAdmin): "name", "display_name", "used_in", + "display_order_number", ) exclude = ("id",) search_fields: tuple[Literal["name"]] = ("name",) 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/models.py b/src/signals/models.py index f9f7070..64f7d16 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"] From ef48f18004036c7c713f3ed1dc357602fb3da52f Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Tue, 25 Mar 2025 16:08:37 +0200 Subject: [PATCH 2/8] Fixed issue with reseting filters before form is submit & reset filters when back button was clicked --- src/templates/signal_sets/signal_sets.html | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/templates/signal_sets/signal_sets.html b/src/templates/signal_sets/signal_sets.html index 16717ae..dcb01a7 100644 --- a/src/templates/signal_sets/signal_sets.html +++ b/src/templates/signal_sets/signal_sets.html @@ -744,7 +744,6 @@ const geoValues = {{ geographic_granularities|safe }}; var relatedSignals = JSON.parse(JSON.stringify({{ related_signals|safe }})); - console.log(relatedSignals.length) var urlParams = JSON.parse(JSON.stringify({{ url_params_dict|safe }})); @@ -785,10 +784,9 @@ }); - // Add event listener for filtering. - window.addEventListener('beforeunload', () => { - document.getElementById('filterSignalSetsForm').reset(); - }); + // Added these two lines to disable bfcache (https://web.dev/articles/bfcache). + window.addEventListener('unload', function(){}); + window.addEventListener('beforeunload', function(){}); document.getElementsByName('modes').forEach((el) => { el.addEventListener('change', (event) => { From 528d040543b87ed0e3a99b18ad00f228f736a83d Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Tue, 25 Mar 2025 16:29:11 +0200 Subject: [PATCH 3/8] Replaced bfcache handling with better solution --- src/templates/signal_sets/signal_sets.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/templates/signal_sets/signal_sets.html b/src/templates/signal_sets/signal_sets.html index dcb01a7..a7fb44c 100644 --- a/src/templates/signal_sets/signal_sets.html +++ b/src/templates/signal_sets/signal_sets.html @@ -785,8 +785,13 @@ // Added these two lines to disable bfcache (https://web.dev/articles/bfcache). - window.addEventListener('unload', function(){}); - window.addEventListener('beforeunload', function(){}); + window.addEventListener('pageshow', (event) => { + if (event.persisted) { + location.reload() + } + }); + {% comment %} window.addEventListener('unload', function(){}); + window.addEventListener('beforeunload', function(){}); {% endcomment %} document.getElementsByName('modes').forEach((el) => { el.addEventListener('change', (event) => { From 1013274f53c328483ad4ba8011bbc7fe5ae7f73c Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Tue, 25 Mar 2025 17:38:39 +0200 Subject: [PATCH 4/8] Removed commented code --- src/templates/signal_sets/signal_sets.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/templates/signal_sets/signal_sets.html b/src/templates/signal_sets/signal_sets.html index a7fb44c..579f388 100644 --- a/src/templates/signal_sets/signal_sets.html +++ b/src/templates/signal_sets/signal_sets.html @@ -790,8 +790,6 @@ location.reload() } }); - {% comment %} window.addEventListener('unload', function(){}); - window.addEventListener('beforeunload', function(){}); {% endcomment %} document.getElementsByName('modes').forEach((el) => { el.addEventListener('change', (event) => { From 6fdba6e7605d1f1b2df4c9167da5f25d6f1fd9f2 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Tue, 25 Mar 2025 17:42:29 +0200 Subject: [PATCH 5/8] Added warning message if no locations were selected and user tries to submit form --- src/assets/js/signal_sets.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/assets/js/signal_sets.js b/src/assets/js/signal_sets.js index 676490e..ac6ad37 100644 --- a/src/assets/js/signal_sets.js +++ b/src/assets/js/signal_sets.js @@ -448,8 +448,15 @@ $('#geographic_value').on('select2:select', function (e) { 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') { + // appendAlert(warningMessage, "warning") plotData(); } else if (currentMode === 'export') { exportData(); From 3929271ca3842e9200fa979e7eeefa70e772ad04 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Tue, 25 Mar 2025 19:15:27 +0200 Subject: [PATCH 6/8] 1. If Location Search was used on the main page, the Locations used should be copied as default to the "Selected Indicators" page. --- src/assets/js/signal_sets.js | 70 ++++++++++++++-------- src/templates/signal_sets/signal_sets.html | 8 ++- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/assets/js/signal_sets.js b/src/assets/js/signal_sets.js index ac6ad37..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,10 +456,12 @@ 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); + } } + ); }); @@ -454,9 +473,8 @@ function submitMode(event) { appendAlert("Please select at least one geographic location", "warning") return; } - + if (currentMode === 'epivis') { - // appendAlert(warningMessage, "warning") plotData(); } else if (currentMode === 'export') { exportData(); diff --git a/src/templates/signal_sets/signal_sets.html b/src/templates/signal_sets/signal_sets.html index 579f388..92b4b9c 100644 --- a/src/templates/signal_sets/signal_sets.html +++ b/src/templates/signal_sets/signal_sets.html @@ -758,10 +758,14 @@ } }) initSelect2('location_search', locationSearchValues); + initSelect2('geographic_value', geoValues); if (urlParams.location_search != "") { - $("#location_search").val(urlParams.location_search).trigger("change"); + var locationSearch = urlParams.location_search; + $("#location_search").val(locationSearch).trigger("change"); + var locationIds = locationSearch.map((item) => item.split(":")[1]); + $('#geographic_value').val(locationIds).trigger('change'); } - initSelect2('geographic_value', geoValues); + table.columns.adjust() $("#totalRowsNumber").text(`Showing ${table.page.info().recordsTotal} indicator ${pluralize(table.page.info().recordsTotal, 'set')} containing ${relatedSignals.length} individual ${pluralize(relatedSignals.length, 'indicator')}`) From 4b154aa792d3df588d6808faad70f06c46ecbbb6 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Thu, 27 Mar 2025 00:06:10 +0200 Subject: [PATCH 7/8] Added links to the bottom of the left panel --- src/templates/signal_sets/signal_sets.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/templates/signal_sets/signal_sets.html b/src/templates/signal_sets/signal_sets.html index 92b4b9c..68ac88f 100644 --- a/src/templates/signal_sets/signal_sets.html +++ b/src/templates/signal_sets/signal_sets.html @@ -278,6 +278,18 @@

Clear filters +
+ From fdc424f26db2236e52217bbeec59628d16dbf4bb Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Fri, 28 Mar 2025 00:16:53 +0200 Subject: [PATCH 8/8] Fixed Signals import --- src/signals/admin.py | 32 ++- .../migrations/0018_otherendointsignal.py | 26 +++ src/signals/models.py | 7 + src/signals/resources.py | 213 +++++++++++++++++- 4 files changed, 270 insertions(+), 8 deletions(-) create mode 100644 src/signals/migrations/0018_otherendointsignal.py diff --git a/src/signals/admin.py b/src/signals/admin.py index 09c0cf7..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, @@ -150,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/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 64f7d16..b55ca0b 100644 --- a/src/signals/models.py +++ b/src/signals/models.py @@ -575,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")