diff --git a/src/base/utils.py b/src/base/utils.py new file mode 100644 index 0000000..caaa9c5 --- /dev/null +++ b/src/base/utils.py @@ -0,0 +1,52 @@ +from io import BytesIO, TextIOWrapper + +import requests +from django.contrib import messages +from django.shortcuts import redirect +from django.utils.module_loading import import_string +from django.http import FileResponse +from import_export.results import RowResult + + +def import_data(admin_instance, request, resource_class, spreadsheet_url): + resource = resource_class() + format_class = import_string("import_export.formats.base_formats.CSV") + + response = requests.get(spreadsheet_url) + response.raise_for_status() + + csvfile = TextIOWrapper(BytesIO(response.content), encoding="utf-8") + + dataset = format_class().create_dataset(csvfile.read()) + + result = resource.import_data(dataset, dry_run=False, raise_errors=True) + + if result.has_errors(): + error_messages = ["Import errors!"] + for error in result.base_errors: + error_messages.append(repr(error.error)) + for line, errors in result.row_errors(): + for error in errors: + error_messages.append(f"Line number: {line} - {repr(error.error)}") + admin_instance.message_user( + request, "\n".join(error_messages), level=messages.ERROR + ) + else: + success_message = ( + "Import finished: {} new, {} updated, {} deleted and {} skipped {}." + ).format( + result.totals[RowResult.IMPORT_TYPE_NEW], + result.totals[RowResult.IMPORT_TYPE_UPDATE], + result.totals[RowResult.IMPORT_TYPE_DELETE], + result.totals[RowResult.IMPORT_TYPE_SKIP], + resource._meta.model._meta.verbose_name_plural, + ) + admin_instance.message_user(request, success_message, level=messages.SUCCESS) + return redirect(".") + + +def download_source_file(url, file_name): + response = requests.get(url) + response.raise_for_status() + return FileResponse(BytesIO(response.content), as_attachment=True, filename=file_name) + diff --git a/src/datasources/admin.py b/src/datasources/admin.py index a4465ba..e4df1da 100644 --- a/src/datasources/admin.py +++ b/src/datasources/admin.py @@ -1,10 +1,14 @@ from django.contrib import admin - +from django.urls import path +from django.conf import settings from import_export.admin import ImportExportModelAdmin -from datasources.models import SourceSubdivision -from datasources.resources import SourceSubdivisionResource -# Register your models here. +from base.utils import import_data, download_source_file +from datasources.models import OtherEndpointSourceSubdivision, SourceSubdivision +from datasources.resources import ( + OtherEndpointSourceSubdivisionResource, + SourceSubdivisionResource, +) @admin.register(SourceSubdivision) @@ -26,3 +30,87 @@ class SourceSubdivisionAdmin(ImportExportModelAdmin): ordering = ["name"] list_filter = ["datasource_name"] resource_classes = [SourceSubdivisionResource] + + change_list_template = "admin/datasources/source_subdivision_changelist.html" + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "import-from-spreadsheet", + self.admin_site.admin_view(self.import_from_spreadsheet), + name="import_sourcesubdivisions", + ), + path( + "download-source-file", + self.admin_site.admin_view(self.download_source_subdivision), + name="download_source_subdivision", + ), + ] + return custom_urls + urls + + def import_from_spreadsheet(self, request): + return import_data( + self, + request, + SourceSubdivisionResource, + settings.SPREADSHEET_URLS["source_subdivisions"], + ) + + def download_source_subdivision(self, request): + return download_source_file( + settings.SPREADSHEET_URLS["source_subdivisions"], "Source_Subdivisions.csv" + ) + + +@admin.register(OtherEndpointSourceSubdivision) +class OtherEndpointSourceSubdivisionAdmin(ImportExportModelAdmin): + """ + Admin interface for Other Endpoint Source Subdivision model. + """ + + list_display = ( + "name", + "display_name", + "external_name", + "description", + "license", + "dua", + "datasource_name", + ) + search_fields = ("name", "display_name", "external_name") + ordering = ["name"] + list_filter = ["datasource_name"] + resource_classes = [OtherEndpointSourceSubdivisionResource] + + change_list_template = "admin/datasources/source_subdivision_changelist.html" + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "import-from-spreadsheet", + self.admin_site.admin_view(self.import_from_spreadsheet), + name="import_other_endpoint_sourcesubdivisions", + ), + path( + "download-source-file", + self.admin_site.admin_view(self.download_other_endpoint_sourcesubdivision), + name="download_other_endpoint_sourcesubdivision", + ), + ] + return custom_urls + urls + + def import_from_spreadsheet(self, request): + return import_data( + self, + request, + OtherEndpointSourceSubdivisionResource, + settings.SPREADSHEET_URLS["other_endpoint_source_subdivisions"], + ) + + def download_other_endpoint_sourcesubdivision(self, request): + return download_source_file( + settings.SPREADSHEET_URLS["other_endpoint_source_subdivisions"], + "Other_Endpoint_Source_Subdivisions.csv", + ) diff --git a/src/datasources/migrations/0002_otherendpointsourcesubdivision_and_more.py b/src/datasources/migrations/0002_otherendpointsourcesubdivision_and_more.py new file mode 100644 index 0000000..25da00b --- /dev/null +++ b/src/datasources/migrations/0002_otherendpointsourcesubdivision_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2.5 on 2025-08-27 14:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("datasources", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="OtherEndpointSourceSubdivision", + fields=[], + options={ + "verbose_name": "Other Endpoint Source Subdivision", + "verbose_name_plural": "Other Endpoint Source Subdivisions", + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("datasources.sourcesubdivision",), + ), + migrations.AddField( + model_name="sourcesubdivision", + name="source_type", + field=models.CharField( + blank=True, + choices=[ + ("covidcast", "Covidcast"), + ("other_endpoint", "Other Endpoint"), + ("non_delphi", "Non Delphi"), + ], + default="covidcast", + help_text="Type of source for the source subdivision", + max_length=255, + null=True, + verbose_name="Source Type", + ), + ), + ] diff --git a/src/datasources/models.py b/src/datasources/models.py index 4af68b8..5492efc 100644 --- a/src/datasources/models.py +++ b/src/datasources/models.py @@ -1,5 +1,7 @@ from django.db import models +from base.models import SOURCE_TYPES + # Create your models here. class SourceSubdivision(models.Model): @@ -32,6 +34,16 @@ class SourceSubdivision(models.Model): verbose_name="Datasource Name", max_length=255, blank=True ) + source_type: models.CharField = models.CharField( + verbose_name="Source Type", + max_length=255, + choices=SOURCE_TYPES, + default="covidcast", + help_text="Type of source for the source subdivision", + blank=True, + null=True, + ) + class Meta: ordering = ["name"] verbose_name = "Source Subdivision" @@ -50,3 +62,10 @@ def __str__(self): def get_display_name(self): return self.display_name if self.display_name else self.name + + +class OtherEndpointSourceSubdivision(SourceSubdivision): + class Meta: + proxy = True + verbose_name = "Other Endpoint Source Subdivision" + verbose_name_plural = "Other Endpoint Source Subdivisions" diff --git a/src/datasources/resources.py b/src/datasources/resources.py index 766e432..b25dc41 100644 --- a/src/datasources/resources.py +++ b/src/datasources/resources.py @@ -1,7 +1,7 @@ from import_export import resources from import_export.fields import Field -from datasources.models import SourceSubdivision +from datasources.models import SourceSubdivision, OtherEndpointSourceSubdivision class SourceSubdivisionResource(resources.ModelResource): @@ -48,3 +48,24 @@ class Meta: "dua", "datasource_name", ) + + +class OtherEndpointSourceSubdivisionResource(SourceSubdivisionResource): + class Meta: + model = OtherEndpointSourceSubdivision + import_id_fields = ("name", "display_name") + skip_unchanged = True + report_skipped = False + fields = ( + "name", + "display_name", + "external_name", + "description", + "license", + "dua", + "datasource_name", + ) + + def after_save_instance(self, instance, row, **kwargs): + instance.source_type = "other_endpoint" + instance.save() diff --git a/src/epiportal/settings.py b/src/epiportal/settings.py index ce38745..ec824ad 100644 --- a/src/epiportal/settings.py +++ b/src/epiportal/settings.py @@ -31,6 +31,16 @@ EPIDATA_URL = os.environ.get("EPIDATA_URL", "https://api.delphi.cmu.edu/epidata/") EPIDATA_API_KEY = os.environ.get("EPIDATA_API_KEY", "") +SPREADSHEET_URLS = { + "source_subdivisions": "https://docs.google.com/spreadsheets/d/1zb7ItJzY5oq1n-2xtvnPBiJu2L3AqmCKubrLkKJZVHs/export?format=csv&gid=0", + "other_endpoint_source_subdivisions": "https://docs.google.com/spreadsheets/d/1zb7ItJzY5oq1n-2xtvnPBiJu2L3AqmCKubrLkKJZVHs/export?format=csv&gid=214580132", + "indicator_sets": "https://docs.google.com/spreadsheets/d/1zb7ItJzY5oq1n-2xtvnPBiJu2L3AqmCKubrLkKJZVHs/export?format=csv&gid=1266808975", + "non_delphi_indicator_sets": "https://docs.google.com/spreadsheets/d/1zb7ItJzY5oq1n-2xtvnPBiJu2L3AqmCKubrLkKJZVHs/export?format=csv&gid=1266477926", + "indicators": "https://docs.google.com/spreadsheets/d/1zb7ItJzY5oq1n-2xtvnPBiJu2L3AqmCKubrLkKJZVHs/export?format=csv&gid=329338228", + "other_endpoint_indicators": "https://docs.google.com/spreadsheets/d/1zb7ItJzY5oq1n-2xtvnPBiJu2L3AqmCKubrLkKJZVHs/export?format=csv&gid=1364181703", + "non_delphi_indicators": "https://docs.google.com/spreadsheets/d/1zb7ItJzY5oq1n-2xtvnPBiJu2L3AqmCKubrLkKJZVHs/export?format=csv&gid=493612863" +} + SENTRY_DSN = os.environ.get('SENTRY_DSN') if SENTRY_DSN: sentry_sdk.init( diff --git a/src/fixtures/geographic_granularities.json b/src/fixtures/geographic_granularities.json index 6c4b347..9e2cd51 100644 --- a/src/fixtures/geographic_granularities.json +++ b/src/fixtures/geographic_granularities.json @@ -14,7 +14,7 @@ "model": "base.Geography", "pk": 2, "fields": { - "name": "hhs", + "name": "hhs-region", "display_name": "U.S. HHS Region", "display_order_number": 2, "used_in": "indicatorsets", diff --git a/src/indicators/admin.py b/src/indicators/admin.py index 14fdf98..091d3f8 100644 --- a/src/indicators/admin.py +++ b/src/indicators/admin.py @@ -1,21 +1,15 @@ +from django.conf import settings from django.contrib import admin +from django.urls import path from import_export.admin import ImportExportModelAdmin -from indicators.models import ( - Category, - FormatType, - Indicator, - IndicatorGeography, - IndicatorType, - OtherEndpointIndicator, - NonDelphiIndicator -) -from indicators.resources import ( - IndicatorResource, - IndicatorBaseResource, - OtherEndpointIndicatorResource, - NonDelphiIndicatorResource, -) +from base.utils import download_source_file, import_data +from indicators.models import (Category, FormatType, Indicator, + IndicatorGeography, IndicatorType, + NonDelphiIndicator, OtherEndpointIndicator) +from indicators.resources import (IndicatorBaseResource, IndicatorResource, + NonDelphiIndicatorResource, + OtherEndpointIndicatorResource) @admin.register(IndicatorType) @@ -72,6 +66,39 @@ class IndicatorAdmin(ImportExportModelAdmin): resource_classes = [IndicatorResource, IndicatorBaseResource] + change_list_template = "admin/indicators/indicator_changelist.html" + + def get_queryset(self, request): + # Exclude proxy model objects + qs = super().get_queryset(request) + return qs.filter(source_type="covidcast") + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "import-from-spreadsheet", + self.admin_site.admin_view(self.import_from_spreadsheet), + name="import_indicators", + ), + path( + "download-source-file", + self.admin_site.admin_view(self.download_indicator), + name="download_indicator", + ), + ] + return custom_urls + urls + + def import_from_spreadsheet(self, request): + return import_data( + self, request, IndicatorResource, settings.SPREADSHEET_URLS["indicators"] + ) + + def download_indicator(self, request): + return download_source_file( + settings.SPREADSHEET_URLS["indicators"], "Indicators.csv" + ) + @admin.register(OtherEndpointIndicator) class OtherEndpointIndicatorAdmin(ImportExportModelAdmin): @@ -93,6 +120,45 @@ class OtherEndpointIndicatorAdmin(ImportExportModelAdmin): resource_classes = [OtherEndpointIndicatorResource] + change_list_template = "admin/indicators/indicator_changelist.html" + + def get_queryset(self, request): + # Exclude proxy model objects + qs = super().get_queryset(request) + return qs.filter(source_type="other_endpoint") + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "import-from-spreadsheet", + self.admin_site.admin_view(self.import_from_spreadsheet), + name="import_otherendpoint_indicators", + ), + path( + "download-source-file", + self.admin_site.admin_view( + self.download_other_endpoint_indicator + ), + name="download_other_endpoint_indicator", + ), + ] + return custom_urls + urls + + def import_from_spreadsheet(self, request): + return import_data( + self, + request, + OtherEndpointIndicatorResource, + settings.SPREADSHEET_URLS["other_endpoint_indicators"], + ) + + def download_other_endpoint_indicator(self, request): + return download_source_file( + settings.SPREADSHEET_URLS["other_endpoint_indicators"], + "Other_Endpoint_Indicators.csv", + ) + @admin.register(NonDelphiIndicator) class NonDelphiIndicatorAdmin(ImportExportModelAdmin): @@ -110,3 +176,37 @@ class NonDelphiIndicatorAdmin(ImportExportModelAdmin): list_display_links = ("name",) resource_classes = [NonDelphiIndicatorResource] + + change_list_template = "admin/indicators/indicator_changelist.html" + + def get_queryset(self, request): + # Exclude proxy model objects + qs = super().get_queryset(request) + return qs.filter(source_type="non_delphi") + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "import-from-spreadsheet", + self.admin_site.admin_view(self.import_from_spreadsheet), + name="import_nondelphi_indicators", + ), + path( + "download-source-file", + self.admin_site.admin_view(self.download_nondelphi_indicator), + name="download_nondelphi_indicator", + ), + ] + return custom_urls + urls + + def import_from_spreadsheet(self, request): + spreadsheet_url = "https://docs.google.com/spreadsheets/d/1zb7ItJzY5oq1n-2xtvnPBiJu2L3AqmCKubrLkKJZVHs/export?format=csv&gid=493612863" + + return import_data(self, request, NonDelphiIndicatorResource, spreadsheet_url) + + def download_nondelphi_indicator(self, request): + return download_source_file( + settings.SPREADSHEET_URLS["non_delphi_indicators"], + "Non_Delphi_Indicators.csv", + ) diff --git a/src/indicatorsets/admin.py b/src/indicatorsets/admin.py index 27fda14..0072d02 100644 --- a/src/indicatorsets/admin.py +++ b/src/indicatorsets/admin.py @@ -1,11 +1,14 @@ +from django.conf import settings from django.contrib import admin - +from django.urls import path from import_export.admin import ImportExportModelAdmin + +from base.utils import download_source_file, import_data from indicatorsets.models import ( + ColumnDescription, + FilterDescription, IndicatorSet, NonDelphiIndicatorSet, - FilterDescription, - ColumnDescription, ) from indicatorsets.resources import IndicatorSetResource, NonDelphiIndicatorSetResource @@ -35,6 +38,42 @@ class IndicatorSetAdmin(ImportExportModelAdmin): ordering = ["name"] list_filter = ["original_data_provider"] + def get_queryset(self, request): + # Exclude proxy model objects + qs = super().get_queryset(request) + return qs.exclude(source_type="non_delphi") + + change_list_template = "admin/indicatorsets/indicator_set_changelist.html" + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "import-from-spreadsheet", + self.admin_site.admin_view(self.import_from_spreadsheet), + name="import_indicatorsets", + ), + path( + "download-source-file", + self.admin_site.admin_view(self.download_indicator_set), + name="download_indicator_set", + ), + ] + return custom_urls + urls + + def import_from_spreadsheet(self, request): + return import_data( + self, + request, + IndicatorSetResource, + settings.SPREADSHEET_URLS["indicator_sets"], + ) + + def download_indicator_set(self, request): + return download_source_file( + settings.SPREADSHEET_URLS["indicator_sets"], "Indicator_Sets.csv" + ) + @admin.register(NonDelphiIndicatorSet) class NonDelphiIndicatorSetAdmin(ImportExportModelAdmin): @@ -61,6 +100,43 @@ class NonDelphiIndicatorSetAdmin(ImportExportModelAdmin): ordering = ["name"] list_filter = ["original_data_provider", "source_type"] + def get_queryset(self, request): + # Exclude proxy model objects + qs = super().get_queryset(request) + return qs.filter(source_type="non_delphi") + + change_list_template = "admin/indicatorsets/indicator_set_changelist.html" + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "import-from-spreadsheet", + self.admin_site.admin_view(self.import_from_spreadsheet), + name="import_nondelphi_indicatorsets", + ), + path( + "download-source-file", + self.admin_site.admin_view(self.download_nondelphi_indicator_set), + name="download_nondelphi_indicator_set", + ), + ] + return custom_urls + urls + + def import_from_spreadsheet(self, request): + return import_data( + self, + request, + NonDelphiIndicatorSetResource, + settings.SPREADSHEET_URLS["non_delphi_indicator_sets"], + ) + + def download_nondelphi_indicator_set(self, request): + return download_source_file( + settings.SPREADSHEET_URLS["non_delphi_indicator_sets"], + "Non_Delphi_Indicator_Sets.csv", + ) + @admin.register(FilterDescription) class FilterDescriptionAdmin(admin.ModelAdmin): diff --git a/src/indicatorsets/migrations/0005_alter_indicatorset_source_type.py b/src/indicatorsets/migrations/0005_alter_indicatorset_source_type.py new file mode 100644 index 0000000..d923c86 --- /dev/null +++ b/src/indicatorsets/migrations/0005_alter_indicatorset_source_type.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.5 on 2025-08-27 14:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("indicatorsets", "0004_columndescription_filterdescription"), + ] + + operations = [ + migrations.AlterField( + model_name="indicatorset", + name="source_type", + field=models.CharField( + blank=True, + choices=[ + ("covidcast", "Covidcast"), + ("other_endpoint", "Other Endpoint"), + ("non_delphi", "Non Delphi"), + ], + default="covidcast", + help_text="Type of source for the indicator set", + max_length=255, + null=True, + verbose_name="Source Type", + ), + ), + ] diff --git a/src/indicatorsets/models.py b/src/indicatorsets/models.py index ac00c76..a39d2a5 100644 --- a/src/indicatorsets/models.py +++ b/src/indicatorsets/models.py @@ -190,7 +190,7 @@ class IndicatorSet(models.Model): max_length=255, choices=SOURCE_TYPES, default="covidcast", - help_text="Type of source for the indicator", + help_text="Type of source for the indicator set", blank=True, null=True, ) diff --git a/src/templates/admin/change_list.html b/src/templates/admin/change_list.html new file mode 100644 index 0000000..feed979 --- /dev/null +++ b/src/templates/admin/change_list.html @@ -0,0 +1,101 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static admin_list %} + +{% block title %}{% if cl.formset and cl.formset.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %} +{% block extrastyle %} + {{ block.super }} + + {% if cl.formset %} + + {% endif %} + {% if cl.formset or action_form %} + + {% endif %} + {{ media.css }} + {% if not actions_on_top and not actions_on_bottom %} + + {% endif %} +{% endblock %} + +{% block extrahead %} +{{ block.super }} +{{ media.js }} + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block coltype %}{% endblock %} + +{% block content %} +
+ {% block object-tools %} + + {% endblock %} + {% if cl.formset and cl.formset.errors %} +

+ {% blocktranslate count counter=cl.formset.total_error_count %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %} +

+ {{ cl.formset.non_form_errors }} + {% endif %} +
+
+ {% block filters %} + {% if cl.has_filters %} + +

{% translate 'Filter' %}

+ {% if cl.is_facets_optional or cl.has_active_filters %}
+ {% if cl.is_facets_optional %}

+ {% if cl.add_facets %}{% translate "Hide counts" %} + {% else %}{% translate "Show counts" %}{% endif %} +

{% endif %} + {% if cl.has_active_filters %}

+ ✖ {% translate "Clear all filters" %} +

{% endif %} +
{% endif %} + {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} +
+ {% endif %} + {% endblock %} +
+ {% block search %}{% search_form cl %}{% endblock %} + {% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %} + +
{% csrf_token %} + {% if cl.formset %} +
{{ cl.formset.management_form }}
+ {% endif %} + + {% block result_list %} + {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %} + {% result_list cl %} + {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} + {% endblock %} + {% block pagination %} + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/admin/datasources/source_subdivision_changelist.html b/src/templates/admin/datasources/source_subdivision_changelist.html new file mode 100644 index 0000000..ea087a4 --- /dev/null +++ b/src/templates/admin/datasources/source_subdivision_changelist.html @@ -0,0 +1,22 @@ +{% extends "admin/change_list.html" %} + +{% block object-tools-items %} + {{ block.super }} +
  • + {% if opts.model_name == "sourcesubdivision" %} + + Import data from spreadsheet + + + Download source file + + {% elif opts.model_name == "otherendpointsourcesubdivision" %} + + Import data from spreadsheet + + + Download source file + + {% endif %} +
  • +{% endblock object-tools-items %} \ No newline at end of file diff --git a/src/templates/admin/indicators/indicator_changelist.html b/src/templates/admin/indicators/indicator_changelist.html new file mode 100644 index 0000000..e3af998 --- /dev/null +++ b/src/templates/admin/indicators/indicator_changelist.html @@ -0,0 +1,29 @@ +{% extends "admin/change_list.html" %} + +{% block object-tools-items %} + {{ block.super }} +
  • + {% if opts.model_name == "indicator" %} + + Import data from spreadsheet + + + Download source file + + {% elif opts.model_name == "otherendpointindicator" %} + + Import data from spreadsheet + + + Download source file + + {% elif opts.model_name == "nondelphiindicator" %} + + Import data from spreadsheet + + + Download source file + + {% endif %} +
  • +{% endblock object-tools-items %} \ No newline at end of file diff --git a/src/templates/admin/indicatorsets/indicator_set_changelist.html b/src/templates/admin/indicatorsets/indicator_set_changelist.html new file mode 100644 index 0000000..c655611 --- /dev/null +++ b/src/templates/admin/indicatorsets/indicator_set_changelist.html @@ -0,0 +1,22 @@ +{% extends "admin/change_list.html" %} + +{% block object-tools-items %} + {{ block.super }} +
  • + {% if opts.model_name == "indicatorset" %} + + Import data from spreadsheet + + + Download source file + + {% elif opts.model_name == "nondelphiindicatorset" %} + + Import data from spreadsheet + + + Download source file + + {% endif %} +
  • +{% endblock %} \ No newline at end of file