This commit is contained in:
Ross
2025-12-09 12:36:18 +00:00
parent e6943ccfc7
commit 40135a282d
6 changed files with 92 additions and 8 deletions
+6 -1
View File
@@ -122,7 +122,12 @@ USE_TZ = True
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "output"]
# Project-level output directory (app-level) plus repo-level `output` for legacy layout
STATICFILES_DIRS = [
BASE_DIR / "output",
# also look for output/ at the repository root (BASE_DIR.parent)
BASE_DIR.parent / "output",
]
# Default primary key field type
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
@@ -10,6 +10,8 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument("rota_id", type=int)
parser.add_argument("--external", action="store_true", help="Use external generator hook if available")
parser.add_argument("--no-export", action="store_true", help="Do not generate HTML export for this run")
parser.add_argument("--no-solve", action="store_true", help="Do not run solver when generating export")
def handle(self, *args, **options):
rota_id = options["rota_id"]
@@ -19,5 +21,12 @@ class Command(BaseCommand):
except RotaSchedule.DoesNotExist:
raise CommandError(f"RotaSchedule with id={rota_id} not found")
run = run_rota_async(rota.id, use_external_generator=use_external)
no_export = options.get("no_export", False)
no_solve = options.get("no_solve", False)
run = run_rota_async(
rota.id,
use_external_generator=use_external,
generate_builder=not no_export,
builder_solve=not no_solve,
)
self.stdout.write(self.style.SUCCESS(f"Started RotaRun id={run.id} for rota={rota.name}"))
+2 -2
View File
@@ -7,7 +7,7 @@ from django.utils import timezone
from .models import RotaRun, RotaSchedule
def _run_rota_job(run_id: int, use_external_generator: bool = False, generate_builder: bool = False, builder_solve: bool = False):
def _run_rota_job(run_id: int, use_external_generator: bool = False, generate_builder: bool = True, builder_solve: bool = True):
"""Background job that executes the scheduler and updates RotaRun.
If `use_external_generator` is True, callers should implement a project
@@ -83,7 +83,7 @@ def _run_rota_job(run_id: int, use_external_generator: bool = False, generate_bu
print(tb)
def run_rota_async(rota_id: int, use_external_generator: bool = False, generate_builder: bool = False, builder_solve: bool = False) -> RotaRun:
def run_rota_async(rota_id: int, use_external_generator: bool = False, generate_builder: bool = True, builder_solve: bool = True) -> RotaRun:
"""Create a RotaRun and start background thread to run it.
Returns the created RotaRun instance.
+1
View File
@@ -20,4 +20,5 @@ urlpatterns = [
path("run/<int:run_id>/export/download/", views.rota_run_export_download, name="rota_run_export_download"),
path("rota/<int:rota_id>/export/builder/", views.rota_export_builder, name="rota_export_builder"),
path("rota/<int:rota_id>/runs/clear/", views.rota_runs_clear, name="rota_runs_clear"),
path("run/<int:run_id>/export/regenerate/", views.rota_run_export_regenerate, name="rota_run_export_regenerate"),
]
+40 -1
View File
@@ -9,6 +9,7 @@ from django.views.decorators.http import require_http_methods
from django.template.loader import render_to_string
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.http import require_POST
from django.shortcuts import HttpResponseRedirect
def index(request):
@@ -213,6 +214,11 @@ def rota_run_export_html(request, run_id):
from .models import RotaRun
run = get_object_or_404(RotaRun, pk=run_id)
# If the export HTML was persisted with the run, return it directly.
if getattr(run, "export_html", None):
return HttpResponse(run.export_html, content_type="text/html; charset=utf-8")
# Fallback: render the export template which shows run.result.
return render(request, "rota/rota_run_export.html", {"run": run})
@@ -220,13 +226,46 @@ def rota_run_export_download(request, run_id):
from .models import RotaRun
run = get_object_or_404(RotaRun, pk=run_id)
html = render_to_string("rota/rota_run_export.html", {"run": run}, request=request)
# Prefer persisted export HTML if available
if getattr(run, "export_html", None):
html = run.export_html
else:
html = render_to_string("rota/rota_run_export.html", {"run": run}, request=request)
response = HttpResponse(html, content_type="text/html; charset=utf-8")
filename = f"rota_run_{run.id}_export.html"
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
@require_POST
def rota_run_export_regenerate(request, run_id):
"""Regenerate the HTML export for a completed run and persist it on the RotaRun.
This is a POST endpoint — regenerating may be slow if the solver is invoked.
"""
from .models import RotaRun
run = get_object_or_404(RotaRun, pk=run_id)
rota = run.rota
solve = request.POST.get("solve") == "1"
try:
rb = rota.to_rota_builder()
rb.build_and_solve(solve=solve)
html = rb.get_worker_timetable_html(include_html_tag=True)
run.export_html = html
run.save(update_fields=["export_html"])
except Exception as exc:
tb = traceback.format_exc()
run.log = (run.log or "") + "\n" + tb
run.save(update_fields=["log"])
return HttpResponse(f"Failed to regenerate export: {exc}", status=500)
# Redirect to the HTML view so user sees the generated page
return HttpResponseRedirect(reverse("rota:rota_run_export_html", args=(run.id,)))
def rota_export_builder(request, rota_id):
"""Build a `RotaBuilder` for the given `RotaSchedule` and return its HTML export.
+33 -3
View File
@@ -4279,7 +4279,7 @@ class RotaBuilder(object):
return "\n".join(timetable)
def get_worker_timetable_html(
self, include_html_tag=False, table_name="rota-table"
self, include_html_tag=False, table_name="rota-table", html_static_prefix=None
):
model = self.model
@@ -4718,13 +4718,43 @@ class RotaBuilder(object):
"""
if include_html_tag:
# html_static_prefix controls how CSS/JS are referenced:
# - If html_static_prefix is None: try to use Django STATIC_URL when running inside Django, otherwise use relative links
# - If html_static_prefix is a falsey value (e.g. empty string or False): use relative links ("timetable.css")
# - If html_static_prefix is a string: use it as the prefix (ensuring trailing slash)
static_prefix = None
if html_static_prefix is not None:
static_prefix = html_static_prefix
if static_prefix is None:
# Attempt to detect Django settings; if not present, fall back to relative links
try:
from django.conf import settings
static_prefix = getattr(settings, "STATIC_URL", "")
except Exception:
static_prefix = ""
# If a prefix is provided and not empty, ensure trailing slash
if isinstance(static_prefix, str) and static_prefix:
if not static_prefix.endswith("/"):
static_prefix = static_prefix + "/"
if static_prefix:
css_href = f"{static_prefix}timetable.css"
js_src = f"{static_prefix}timetable.js"
else:
# Relative links for standalone usage
css_href = "timetable.css"
js_src = "timetable.js"
html = f"""<html>
<head>
<link rel="stylesheet" type="text/css" href="timetable.css">
<link rel="stylesheet" type="text/css" href="{css_href}">
<link rel="stylesheet" type="text/css" href="https://code.jquery.com/ui/1.13.0/themes/base/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.13.0/jquery-ui.min.js"></script>
<script src="timetable.js" defer></script>
<script src="{js_src}" defer></script>
</head>
{html}</html>"""