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