diff --git a/l4d2web/routes/page_routes.py b/l4d2web/routes/page_routes.py index fb4bf5c..bbd3423 100644 --- a/l4d2web/routes/page_routes.py +++ b/l4d2web/routes/page_routes.py @@ -1,6 +1,6 @@ import json -from flask import Blueprint, Response, redirect, render_template +from flask import Blueprint, Response, redirect, render_template, request from sqlalchemy import select from l4d2web.auth import current_user, require_admin, require_login @@ -70,7 +70,23 @@ def servers_page() -> str: blueprints = db.scalars( select(BlueprintModel).where(BlueprintModel.user_id == user.id).order_by(BlueprintModel.name) ).all() - return render_template("servers.html", rows=rows, blueprints=blueprints) + + prefill_blueprint_id: int | None = None + raw_prefill = request.args.get("blueprint_id") + if raw_prefill: + try: + candidate = int(raw_prefill) + except ValueError: + candidate = None + if candidate is not None and any(b.id == candidate for b in blueprints): + prefill_blueprint_id = candidate + + return render_template( + "servers.html", + rows=rows, + blueprints=blueprints, + prefill_blueprint_id=prefill_blueprint_id, + ) @bp.get("/servers/") @@ -84,11 +100,6 @@ def server_detail(server_id: int): if server is None: return Response(status=404) blueprint = db.scalar(select(BlueprintModel).where(BlueprintModel.id == server.blueprint_id)) - blueprints = db.scalars( - select(BlueprintModel) - .where(BlueprintModel.user_id == user.id) - .order_by(BlueprintModel.name) - ).all() recent_job_rows = db.execute( select(Job, User, Server) .join(User, User.id == Job.user_id) @@ -102,7 +113,6 @@ def server_detail(server_id: int): "server_detail.html", server=server, blueprint=blueprint, - blueprints=blueprints, recent_job_rows=recent_job_rows, ) diff --git a/l4d2web/routes/server_routes.py b/l4d2web/routes/server_routes.py index 82a8aeb..8e501ef 100644 --- a/l4d2web/routes/server_routes.py +++ b/l4d2web/routes/server_routes.py @@ -119,36 +119,6 @@ def update_server(server_id: int) -> Response: return jsonify({"id": server_id}), 200 -@bp.post("/servers/") -@require_login -def update_server_form(server_id: int) -> Response: - user = current_user() - assert user is not None - - try: - blueprint_id = int(request.form["blueprint_id"]) - except (KeyError, TypeError, ValueError): - return Response("blueprint_id is required", status=400) - - with session_scope() as db: - server = db.scalar(select(Server).where(Server.id == server_id, Server.user_id == user.id)) - if server is None: - return Response(status=404) - - blueprint = db.scalar( - select(BlueprintModel).where( - BlueprintModel.id == blueprint_id, - BlueprintModel.user_id == user.id, - ) - ) - if blueprint is None: - return Response("blueprint not found", status=404) - - server.blueprint_id = blueprint.id - - return redirect(f"/servers/{server_id}") - - LIFECYCLE_OPERATIONS = {"initialize", "start", "stop", "delete"} diff --git a/l4d2web/static/css/components.css b/l4d2web/static/css/components.css index 6178eec..3c29cee 100644 --- a/l4d2web/static/css/components.css +++ b/l4d2web/static/css/components.css @@ -59,16 +59,20 @@ a:focus-visible { outline-offset: 2px; } -button { +button, +a.button { background: var(--color-primary); border: none; border-radius: var(--radius-s); color: #fff; padding: var(--space-s) var(--space-l); cursor: pointer; + display: inline-block; + text-decoration: none; } -button.danger { +button.danger, +a.button.danger { background: var(--color-danger); } diff --git a/l4d2web/templates/blueprints.html b/l4d2web/templates/blueprints.html index 852f9e2..996b447 100644 --- a/l4d2web/templates/blueprints.html +++ b/l4d2web/templates/blueprints.html @@ -9,16 +9,17 @@ - + {% for blueprint in blueprints %} + {% else %} - + {% endfor %}
NameCreatedUpdated
NameCreatedUpdatedActions
{{ blueprint.name }} {{ blueprint.created_at }} {{ blueprint.updated_at }}Create server
No blueprints configured.
No blueprints configured.
diff --git a/l4d2web/templates/server_detail.html b/l4d2web/templates/server_detail.html index 6b920a1..3c9a993 100644 --- a/l4d2web/templates/server_detail.html +++ b/l4d2web/templates/server_detail.html @@ -29,24 +29,6 @@ -
-

Reassign blueprint

-
- - -

Changes apply on the next server action.

-
- -
-
-
-

Recent Jobs

diff --git a/l4d2web/templates/servers.html b/l4d2web/templates/servers.html index 73f4d90..e106bba 100644 --- a/l4d2web/templates/servers.html +++ b/l4d2web/templates/servers.html @@ -6,11 +6,12 @@

Servers

- + {% if blueprints %} + + {% else %} + Create a blueprint first → + {% endif %}
- {% if not blueprints %} -

Create a blueprint before adding servers.

- {% endif %} @@ -46,7 +47,7 @@ @@ -58,4 +59,15 @@ {% endif %} + +{% if prefill_blueprint_id %} + +{% endif %} {% endblock %} diff --git a/l4d2web/tests/test_servers.py b/l4d2web/tests/test_servers.py index fdb87c6..939452a 100644 --- a/l4d2web/tests/test_servers.py +++ b/l4d2web/tests/test_servers.py @@ -37,6 +37,32 @@ def user_client_with_blueprints(tmp_path, monkeypatch): return client, payload +def test_servers_page_without_blueprints_shows_create_blueprint_cta(tmp_path, monkeypatch) -> None: + db_url = f"sqlite:///{tmp_path/'no_blueprints.db'}" + monkeypatch.setenv("DATABASE_URL", db_url) + app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"}) + init_db() + + with session_scope() as session: + user = User(username="solo", password_digest=hash_password("secret"), admin=False) + session.add(user) + session.flush() + user_id = user.id + + client = app.test_client() + with client.session_transaction() as sess: + sess["user_id"] = user_id + sess["csrf_token"] = "test-token" + + response = client.get("/servers") + text = response.get_data(as_text=True) + + assert response.status_code == 200 + assert 'href="/blueprints"' in text + assert "Create a blueprint first" in text + assert "disabled" not in text + + def test_create_server_from_blueprint(user_client_with_blueprints) -> None: client, data = user_client_with_blueprints payload = {"name": "alpha", "port": 27015, "blueprint_id": data["blueprint_id"]} @@ -213,59 +239,6 @@ def test_create_server_returns_409_when_port_range_exhausted(tmp_path, monkeypat assert second.status_code == 409 -def test_update_server_form_reassigns_blueprint(user_client_with_blueprints) -> None: - client, data = user_client_with_blueprints - create = client.post( - "/servers", - data={"name": "alpha", "port": "27015", "blueprint_id": str(data["blueprint_id"])}, - headers={"X-CSRF-Token": "test-token"}, - ) - assert create.status_code == 302 - server_id = int(create.headers["Location"].rsplit("/", 1)[-1]) - - response = client.post( - f"/servers/{server_id}", - data={"blueprint_id": str(data["other_blueprint_id"])}, - headers={"X-CSRF-Token": "test-token"}, - ) - assert response.status_code == 302 - assert response.headers["Location"] == f"/servers/{server_id}" - - from sqlalchemy import select - - from l4d2web.models import Server - - with session_scope() as session: - server = session.scalars(select(Server)).one() - assert server.blueprint_id == data["other_blueprint_id"] - - -def test_update_server_form_rejects_foreign_blueprint(user_client_with_blueprints, tmp_path) -> None: - client, data = user_client_with_blueprints - create = client.post( - "/servers", - data={"name": "alpha", "port": "27015", "blueprint_id": str(data["blueprint_id"])}, - headers={"X-CSRF-Token": "test-token"}, - ) - server_id = int(create.headers["Location"].rsplit("/", 1)[-1]) - - with session_scope() as session: - other = User(username="bob", password_digest=hash_password("secret"), admin=False) - session.add(other) - session.flush() - foreign = Blueprint(user_id=other.id, name="foreign", arguments="[]", config="[]") - session.add(foreign) - session.flush() - foreign_id = foreign.id - - response = client.post( - f"/servers/{server_id}", - data={"blueprint_id": str(foreign_id)}, - headers={"X-CSRF-Token": "test-token"}, - ) - assert response.status_code == 404 - - def test_delete_operation_redirects_to_index(user_client_with_blueprints) -> None: client, data = user_client_with_blueprints create = client.post( @@ -285,6 +258,54 @@ def test_delete_operation_redirects_to_index(user_client_with_blueprints) -> Non assert response.headers["Location"] == "/servers" +def test_servers_page_prefills_blueprint_when_owned(user_client_with_blueprints) -> None: + client, data = user_client_with_blueprints + response = client.get(f"/servers?blueprint_id={data['other_blueprint_id']}") + text = response.get_data(as_text=True) + + assert response.status_code == 200 + assert f'
NamePortBlueprintDesiredActual