diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 6ef485cd304cc29bfdf350dd75b99870abbee828..97f0ee3628e6f9c791882905634bf9e2b4f9dfd0 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import os
 from datetime import date, datetime, time, timedelta
 from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
 
@@ -1121,14 +1122,9 @@ class ExtraLesson(
         indexes = [models.Index(fields=["week", "year"], name="extra_lesson_week_year")]
 
 
-@receiver(substitutions_changed)
-def automatic_plan_signal_receiver(sender: Revision, versions: Iterable[Version], **kwargs):
-    """Check all automatic plans for updates after substitutions changed."""
-    for automatic_plan in AutomaticPlan.objects.all():
-        automatic_plan.check_update(sender, versions)
-
-
 class AutomaticPlan(ExtensibleModel):
+    """Model for configuring automatically updated PDF substitution plans."""
+
     slug = models.SlugField(
         verbose_name=_("Slug"),
         help_text=_("This will be used for the name of the PDF file with the generated plan."),
@@ -1144,7 +1140,6 @@ class AutomaticPlan(ExtensibleModel):
         verbose_name=_("Show header box"),
         help_text=_("The header box shows affected teachers/groups."),
     )
-
     last_revision = models.ForeignKey(
         to=Revision,
         on_delete=models.SET_NULL,
@@ -1152,13 +1147,13 @@ class AutomaticPlan(ExtensibleModel):
         null=True,
         verbose_name=_("Revision which triggered the last update"),
     )
+
     last_update = models.DateTimeField(
         blank=True, null=True, verbose_name=_("Date and time of the last update")
     )
     last_update_triggered_manually = models.BooleanField(
         default=False, verbose_name=_("Was the last update triggered manually?")
     )
-
     current_file = models.FileField(
         upload_to="chronos/plan_pdfs/", null=True, blank=True, verbose_name=_("Current file")
     )
@@ -1175,7 +1170,7 @@ class AutomaticPlan(ExtensibleModel):
 
     def get_context_data(self) -> Dict[str, Any]:
         """Get context data for generating the substitutions PDF."""
-        from aleksis.apps.chronos.views import get_substitutions_context_data
+        from aleksis.apps.chronos.views import get_substitutions_context_data  # noqa
 
         context = get_substitutions_context_data(
             request=None,
@@ -1237,12 +1232,15 @@ class AutomaticPlan(ExtensibleModel):
     @property
     def path(self) -> str:
         """Get the relative path of the PDF file in the media directory."""
-        return f"chronos/plans/{self.filename}"
+        return os.path.join("chronos", "plans", self.filename)
 
     @property
     def local_path(self) -> str:
         """Get the full path under which the PDF file can accessed on the local system."""
-        return default_storage.path(self.path)
+        try:
+            return default_storage.path(self.path)
+        except NotImplementedError:
+            return self.path
 
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
@@ -1258,6 +1256,13 @@ class AutomaticPlan(ExtensibleModel):
         constraints = [models.UniqueConstraint(fields=["site_id", "slug"], name="site_slug")]
 
 
+@receiver(substitutions_changed)
+def automatic_plan_signal_receiver(sender: Revision, versions: Iterable[Version], **kwargs):
+    """Check all automatic plans for updates after substitutions changed."""
+    for automatic_plan in AutomaticPlan.objects.all():
+        automatic_plan.check_update(sender, versions)
+
+
 class ChronosGlobalPermissions(GlobalPermissionModel):
     class Meta:
         managed = False