Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/base/utils.py
Original file line number Diff line number Diff line change
@@ -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)

96 changes: 92 additions & 4 deletions src/datasources/admin.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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",
)
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
19 changes: 19 additions & 0 deletions src/datasources/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.db import models

from base.models import SOURCE_TYPES


# Create your models here.
class SourceSubdivision(models.Model):
Expand Down Expand Up @@ -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"
Expand All @@ -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"
23 changes: 22 additions & 1 deletion src/datasources/resources.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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()
10 changes: 10 additions & 0 deletions src/epiportal/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/fixtures/geographic_granularities.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading