From 6cee1135a8b9a620861fa805e9e20194995ec9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sun, 25 Oct 2026 10:42:58 +0100 Subject: [PATCH] feat: add option to disable pixel placement --- polylan_submitter/place/admin.py | 4 ++-- polylan_submitter/place/consumers.py | 14 +++++++++++++- .../0002_placesettings_placement_enabled.py | 18 ++++++++++++++++++ polylan_submitter/place/models.py | 3 +++ polylan_submitter/place/signals.py | 6 ++++-- polylan_submitter/place/wire.py | 17 +++++++++++------ polylan_submitter/src/Place.vue | 13 ++++++++----- .../src/composables/usePlaceSocket.ts | 11 +++++++++-- polylan_submitter/src/locales/en.json | 3 ++- 9 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 polylan_submitter/place/migrations/0002_placesettings_placement_enabled.py diff --git a/polylan_submitter/place/admin.py b/polylan_submitter/place/admin.py index 43739c7..c83b7a9 100644 --- a/polylan_submitter/place/admin.py +++ b/polylan_submitter/place/admin.py @@ -7,8 +7,8 @@ from .models import PixelPlacement, PlaceSettings @admin.register(PlaceSettings) class PlaceSettingsAdmin(DjangoObjectActions, admin.ModelAdmin): - list_display = ("event", "width", "height") - list_editable = ("width", "height") + list_display = ("event", "width", "height", "placement_enabled") + list_editable = ("width", "height", "placement_enabled") list_filter = ("event",) change_actions = ("resync_canvas",) diff --git a/polylan_submitter/place/consumers.py b/polylan_submitter/place/consumers.py index e3f9a33..386408a 100644 --- a/polylan_submitter/place/consumers.py +++ b/polylan_submitter/place/consumers.py @@ -45,6 +45,10 @@ class PlaceConsumer(AsyncWebsocketConsumer): await self.send(text_data=encode("r", "a")) return + if not place_settings.placement_enabled: + await self.send(text_data=encode("r", "d")) + return + parts = text_data.split("|") if len(parts) != 4: await self.send(text_data=encode("r", "i")) @@ -142,6 +146,7 @@ class PlaceConsumer(AsyncWebsocketConsumer): next_placement_at, place_settings.width, place_settings.height, + int(place_settings.placement_enabled), ) @database_sync_to_async @@ -152,4 +157,11 @@ class PlaceConsumer(AsyncWebsocketConsumer): await self.send(text_data=encode("p", event["x"], event["y"], event["c"])) async def settings_changed(self, event): - await self.send(text_data=encode("e", event["width"], event["height"])) + await self.send( + text_data=encode( + "e", + event["width"], + event["height"], + int(event["placement_enabled"]), + ) + ) diff --git a/polylan_submitter/place/migrations/0002_placesettings_placement_enabled.py b/polylan_submitter/place/migrations/0002_placesettings_placement_enabled.py new file mode 100644 index 0000000..420f9bd --- /dev/null +++ b/polylan_submitter/place/migrations/0002_placesettings_placement_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.5 on 2026-10-25 09:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("place", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="placesettings", + name="placement_enabled", + field=models.BooleanField(default=True), + ), + ] diff --git a/polylan_submitter/place/models.py b/polylan_submitter/place/models.py index 2cb1401..3eb3ef8 100644 --- a/polylan_submitter/place/models.py +++ b/polylan_submitter/place/models.py @@ -47,6 +47,9 @@ class PlaceSettings(models.Model): ) width = models.PositiveSmallIntegerField(default=DEFAULT_GRID_WIDTH) height = models.PositiveSmallIntegerField(default=DEFAULT_GRID_HEIGHT) + # Freezes the board without taking the game down: clients can still + # connect and watch, but every placement is rejected. + placement_enabled = models.BooleanField(default=True) def __str__(self): return f"PlaceSettings({self.event}, size={self.width}x{self.height})" diff --git a/polylan_submitter/place/signals.py b/polylan_submitter/place/signals.py index 9ecf6da..c2b607e 100644 --- a/polylan_submitter/place/signals.py +++ b/polylan_submitter/place/signals.py @@ -1,11 +1,12 @@ -"""Reacts to an admin editing a PlaceSettings row (grid size) by: +"""Reacts to an admin editing a PlaceSettings row (grid size, placement +toggle) by: 1. Rebuilding that event's Redis canvas from history at the (possibly new) dimensions - a resize invalidates the old Redis bytes (wrong length, wrong meaning), so this keeps Redis self-healing instead of relying on the admin remembering to click "Resync canvas from DB" after every resize. -2. Pushing the new size to that event's already-connected clients. Unlike +2. Pushing the new settings to that event's already-connected clients. Unlike the clock app, place has no periodic heartbeat broadcast to piggyback this onto, so it's broadcast explicitly here instead. """ @@ -33,5 +34,6 @@ def on_settings_saved(sender, instance, **kwargs): "type": "settings_changed", "width": instance.width, "height": instance.height, + "placement_enabled": instance.placement_enabled, }, ) diff --git a/polylan_submitter/place/wire.py b/polylan_submitter/place/wire.py index e6e4143..3d9dcef 100644 --- a/polylan_submitter/place/wire.py +++ b/polylan_submitter/place/wire.py @@ -6,8 +6,10 @@ either). Each message is a short string: a one-letter type tag followed by '|'-separated fields, in a fixed order the client already knows from this table: - s||| initial personal state, once per connection - (next = epoch ms cooldown ends, or empty if free now) + s|||| + initial personal state, once per connection + (next = epoch ms cooldown ends, or empty if free now; + on = 1 if placing pixels is allowed, 0 if frozen) p||| a pixel was placed - broadcast to everyone, including the placer (no separate ack needed) k| your own placement was accepted; carries your @@ -15,10 +17,13 @@ table: r|c| rejected: still on cooldown r|a rejected: not logged in r|i rejected: invalid x/y/color - e|| an admin resized the grid for this event - - broadcast to everyone. A client that sees this - should re-fetch /api/.../place/canvas rather than - try to remap its local pixels. + r|d rejected: placement is disabled by an admin + e||| + an admin edited this event's settings (grid size + and/or placement toggle) - broadcast to everyone. + If the size changed, a client should re-fetch + /api/.../place/canvas rather than try to remap its + local pixels. Client -> server has exactly one message shape, the same as the broadcast: p||| diff --git a/polylan_submitter/src/Place.vue b/polylan_submitter/src/Place.vue index c8122fc..ebdd5b3 100644 --- a/polylan_submitter/src/Place.vue +++ b/polylan_submitter/src/Place.vue @@ -43,6 +43,7 @@ function drawPixel(x: number, y: number, colorIndex: number) { const { connected, cooldownRemainingMs, + placementEnabled, cooldownDurationSeconds, gridWidth, gridHeight, @@ -55,6 +56,7 @@ const overlayHeight = computed(() => gridHeight.value * PX_PER_CELL); const cooldownSecondsDisplay = computed(() => (cooldownRemainingMs.value / 1000).toFixed(1)); const onCooldown = computed(() => cooldownRemainingMs.value > 0); +const canPlace = computed(() => placementEnabled.value && !onCooldown.value); // The overlay canvas never touches the pixel colors - it only draws grid // lines plus a preview of the hovered cell, so it can freely redraw on @@ -86,7 +88,7 @@ function drawOverlay() { } } - if (hoveredCell.value && !onCooldown.value) { + if (hoveredCell.value && canPlace.value) { const { x, y } = hoveredCell.value; gridCtx.fillStyle = `${PALETTE[selectedColorIndex.value]}80`; gridCtx.fillRect(x * PX_PER_CELL, y * PX_PER_CELL, PX_PER_CELL, PX_PER_CELL); @@ -96,7 +98,7 @@ function drawOverlay() { } } -watch([selectedColorIndex, onCooldown, showGrid, gridWidth, gridHeight], drawOverlay); +watch([selectedColorIndex, canPlace, showGrid, gridWidth, gridHeight], drawOverlay); onMounted(() => { pixelCtx = pixelCanvasRef.value?.getContext("2d") ?? null; @@ -125,7 +127,7 @@ function handleMouseLeave() { } function handleClick(event: MouseEvent) { - if (onCooldown.value) return; + if (!canPlace.value) return; const { x, y } = cellFromEvent(event); place(x, y, selectedColorIndex.value); } @@ -142,7 +144,7 @@ function handleClick(event: MouseEvent) { @@ -154,7 +156,8 @@ function handleClick(event: MouseEvent) { -