.
This commit is contained in:
@@ -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}"))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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>"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user