diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 86a0de7..9ac48d3 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -33,8 +33,16 @@ jobs: run: | curl -L -s https://unpkg.com/wq > docs/js/wq.js curl -L -s https://unpkg.com/@wq/markdown@latest > docs/js/markdown.js + curl -L -s https://unpkg.com/@wq/analyst@next > docs/js/analyst.js + curl -L -s https://unpkg.com/@wq/chart@next > docs/js/chart.js sed -i "s/^import\(.*\)https:\/\/unpkg.com\/wq/import\1.\/wq.js/" docs/js/*.js sed -i "s/^import\(.*\)https:\/\/unpkg.com\/@wq\/markdown@next/import\1.\/markdown.js/" docs/js/*.js + sed -i "s/^import\(.*\)https:\/\/unpkg.com\/@wq\/analyst/import\1.\/analyst.js/" docs/js/*.js + sed -i "s/^import\(.*\)https:\/\/unpkg.com\/@wq\/chart/import\1.\/chart.js/" docs/js/*.js + - name: Export Django site + run: | + python -m pip install django djangorestframework pandas openpyxl matplotlib + python -m unittest tests.generate_docs - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 with: diff --git a/.gitignore b/.gitignore index 74bde4b..538f181 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ build dist node_modules +docs/static +docs/timeseries.* +docs/weather.* diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index ed4a06a..cefab5f 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -13,6 +13,15 @@ margin-right: auto; max-width: 100%; } + .MuiAppBar-colorPrimary img { + border-radius: 4px; + padding-left: 4px; + padding-right: 4px; + margin-left: -18px !important; + margin-top: 4px; + margin-bottom: 4px; + background-color: rgba(0, 0, 0, 0.6); + } + + - + - + diff --git a/tests/files/timeseries.html b/tests/files/timeseries.html index cdaed18..bd9dbfc 100644 --- a/tests/files/timeseries.html +++ b/tests/files/timeseries.html @@ -3,20 +3,20 @@ Time Series Custom - + - - + + - + - + diff --git a/tests/generate_docs.py b/tests/generate_docs.py new file mode 100644 index 0000000..4833fc3 --- /dev/null +++ b/tests/generate_docs.py @@ -0,0 +1,52 @@ +import unittest +from rest_framework.test import APITestCase +from tests.testapp.models import TimeSeries +from tests.weather.models import Station +from django.core.management import call_command +import pathlib + + +DOCS = pathlib.Path("docs") + +STATIONS = { + "MSP": "USW00014922", + "ATL": "USW00013874", + "LAX": "USW00023174", +} + +class DocsTestCase(APITestCase): + def setUp(self): + data = ( + ("2014-01-01", 0.5), + ("2014-01-02", 0.4), + ("2014-01-03", 0.6), + ("2014-01-04", 0.2), + ("2014-01-05", 0.1), + ) + for date, value in data: + TimeSeries.objects.create(date=date, value=value) + + for name, code in STATIONS.items(): + station = Station.objects.create(name=name, code=code) + station.load_weather() + + def test_docs(self): + call_command('collectstatic', interactive=False) + for url in ( + "timeseries.html", + "timeseries.csv", + "timeseries.json", + "timeseries.xlsx", + "timeseries.png", + "timeseries.svg", + "weather.html", + "weather.csv", + "weather.json", + "weather.xlsx", + "weather.png", + "weather.svg", + ): + response = self.client.get(f"/{url}") + path = DOCS / url + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(response.content) diff --git a/tests/settings.py b/tests/settings.py index cc993ed..32a862a 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -5,7 +5,9 @@ "django.contrib.contenttypes", "django.contrib.messages", "django.contrib.sessions", + "django.contrib.staticfiles", "tests.testapp", + "tests.weather", "rest_pandas", "rest_framework", ) @@ -16,6 +18,8 @@ } } ROOT_URLCONF = "tests.urls" +STATIC_URL = "/static" +STATIC_ROOT = "docs/static" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", diff --git a/tests/urls.py b/tests/urls.py index fdc3049..60d5a36 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -3,5 +3,6 @@ urlpatterns = [ path("", include("tests.testapp.urls")), + path("", include("tests.weather.urls")), path("admin", admin.site.urls), ] diff --git a/tests/weather/migrations/0001_initial.py b/tests/weather/migrations/0001_initial.py new file mode 100644 index 0000000..4e1b1c3 --- /dev/null +++ b/tests/weather/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 5.0.3 on 2024-04-02 02:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Station", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50, unique=True)), + ("code", models.CharField(max_length=20, unique=True)), + ], + ), + migrations.CreateModel( + name="Weather", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(verbose_name="Date")), + ( + "tavg", + models.IntegerField(null=True, verbose_name="Average Temp (°F)"), + ), + ("tmax", models.IntegerField(verbose_name="Max Temp (°F)")), + ("tmin", models.IntegerField(verbose_name="Min Temp (°F)")), + ("prcp", models.FloatField(verbose_name="Precipitation (in)")), + ("snow", models.FloatField(null=True, verbose_name="Snow (in)")), + ("snwd", models.FloatField(null=True, verbose_name="Snow Depth (in)")), + ( + "station", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="weather.station", + ), + ), + ], + ), + ] diff --git a/tests/weather/migrations/__init__.py b/tests/weather/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/weather/models.py b/tests/weather/models.py new file mode 100644 index 0000000..9f7e067 --- /dev/null +++ b/tests/weather/models.py @@ -0,0 +1,41 @@ +from django.db import models +import requests + + +DATA_URL = "https://www.ncei.noaa.gov/access/past-weather/{code}/data.csv" + + +class Station(models.Model): + name = models.CharField(max_length=50, unique=True) + code = models.CharField(max_length=20, unique=True) + + def load_weather(self): + response = requests.get(DATA_URL.format(code=self.code)) + + for i, row in enumerate(response.iter_lines(decode_unicode=True)): + if i < 2: + continue + assert row.count(",") == 6 + date, tavg, tmax, tmin, prcp, snow, snwd = row.split(",") + if date < '2020-01-01': + continue + self.weather_set.create( + date=date, + tavg=tavg or None, + tmax=tmax or tavg, + tmin=tmin, + prcp=prcp or None, + snow=snow or None, + snwd=snwd or None, + ) + + +class Weather(models.Model): + station = models.ForeignKey(Station, on_delete=models.PROTECT) + date = models.DateField(verbose_name="Date") + tavg = models.IntegerField(verbose_name="Average Temp (°F)", null=True) + tmax = models.IntegerField(verbose_name="Max Temp (°F)") + tmin = models.IntegerField(verbose_name="Min Temp (°F)") + prcp = models.FloatField(verbose_name="Precipitation (in)") + snow = models.FloatField(verbose_name="Snow (in)", null=True) + snwd = models.FloatField(verbose_name="Snow Depth (in)", null=True) diff --git a/tests/weather/serializers.py b/tests/weather/serializers.py new file mode 100644 index 0000000..c47e18e --- /dev/null +++ b/tests/weather/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from .models import Weather + + +class WeatherSerializer(serializers.ModelSerializer): + station = serializers.ReadOnlyField(source="station.name", label="Station") + + class Meta: + model = Weather + exclude = ["id"] + pandas_index = ["date"] # Date + pandas_unstacked_header = ["Station"] diff --git a/tests/weather/urls.py b/tests/weather/urls.py new file mode 100644 index 0000000..293f82d --- /dev/null +++ b/tests/weather/urls.py @@ -0,0 +1,8 @@ +from django.urls import include, path +from rest_framework.urlpatterns import format_suffix_patterns +from .views import WeatherView + +urlpatterns = [ + path("weather", WeatherView.as_view()), +] +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/tests/weather/views.py b/tests/weather/views.py new file mode 100644 index 0000000..1b23b90 --- /dev/null +++ b/tests/weather/views.py @@ -0,0 +1,9 @@ +from rest_pandas import PandasView, PandasUnstackedSerializer +from .models import Weather +from .serializers import WeatherSerializer + + +class WeatherView(PandasView): + queryset = Weather.objects.select_related("station") + serializer_class = WeatherSerializer + pandas_serializer_class = PandasUnstackedSerializer