ctfy.sdk.admin_resources.competition_boards

client.admin.competition_boards — a competition's extra boards (#1965 G).

A board is a named (rule, params) over the competition's facts. The primary is the competition's own scoring rule (set with client.admin.competitions.update); these are the others — a published standing beside it, or a shadow trial only the organisers can read.

  1"""``client.admin.competition_boards`` — a competition's extra boards (#1965 G).
  2
  3A board is a named ``(rule, params)`` over the competition's facts. The
  4primary is the competition's own scoring rule (set with
  5``client.admin.competitions.update``); these are the others — a published
  6standing beside it, or a ``shadow`` trial only the organisers can read.
  7"""
  8
  9from __future__ import annotations
 10
 11from typing import Any
 12
 13from ctfy.core.enums import BoardVisibility, ScoringRuleName
 14from ctfy.core.localized import LocalizedText
 15from ctfy.sdk._helpers import _raise_for_status
 16from ctfy.sdk.base import BaseHttpClient
 17from ctfy.server.models import CompetitionBoardInfo, CompetitionBoardsResponse
 18
 19
 20class AdminCompetitionBoardsResource:
 21    """Add, list, edit, publish and remove one competition's boards. For its
 22    organisers (global admins and the event's own competition admins)."""
 23
 24    def __init__(self, http: BaseHttpClient) -> None:
 25        self._http = http
 26
 27    def list(self, competition_id: str) -> CompetitionBoardsResponse:
 28        """Every board, the primary first and the shadow ones included —
 29        the organiser's read of ``GET /competitions/{id}/boards``."""
 30        resp = self._http.request("GET", f"/competitions/{competition_id}/boards")
 31        _raise_for_status(resp)
 32        return CompetitionBoardsResponse.model_validate(resp.json())
 33
 34    def create(
 35        self,
 36        competition_id: str,
 37        *,
 38        slug: str,
 39        rule: ScoringRuleName,
 40        params: dict[str, Any] | None = None,
 41        title: str | LocalizedText = "",
 42        visibility: BoardVisibility = "shadow",
 43        position: int = 0,
 44    ) -> CompetitionBoardInfo:
 45        """Add a board. It starts ``shadow`` — computed on the real facts and
 46        read by organisers alone — unless ``visibility="published"``.
 47        ``title`` is ``{locale: text}`` (a bare string is read as ``en``)."""
 48        payload: dict[str, Any] = {
 49            "slug": slug,
 50            "rule": rule,
 51            "params": params or {},
 52            "title": title,
 53            "visibility": visibility,
 54            "position": position,
 55        }
 56        resp = self._http.request(
 57            "POST", f"/admin/competitions/{competition_id}/boards", json=payload
 58        )
 59        _raise_for_status(resp)
 60        return CompetitionBoardInfo.model_validate(resp.json())
 61
 62    def update(
 63        self,
 64        competition_id: str,
 65        slug: str,
 66        *,
 67        rule: ScoringRuleName | None = None,
 68        params: dict[str, Any] | None = None,
 69        title: str | LocalizedText | None = None,
 70        visibility: BoardVisibility | None = None,
 71        position: int | None = None,
 72        scoring_change_reason: str | None = None,
 73    ) -> CompetitionBoardInfo:
 74        """Edit one board. Partial: only the arguments you pass are sent.
 75
 76        ⚠️ ``params`` **replaces** the board's params, and a new ``rule``
 77        without ``params`` starts from that rule's defaults. Once the
 78        competition is live, changing a *published* board's scoring needs
 79        ``scoring_change_reason``: the change is recorded and shown on the
 80        board (``422 scoring_change_needs_reason`` without one). A shadow
 81        board is edited freely."""
 82        payload: dict[str, Any] = {}
 83        if rule is not None:
 84            payload["rule"] = rule
 85        if params is not None:
 86            payload["params"] = params
 87        if title is not None:
 88            payload["title"] = title
 89        if visibility is not None:
 90            payload["visibility"] = visibility
 91        if position is not None:
 92            payload["position"] = position
 93        if scoring_change_reason is not None:
 94            payload["scoring_change_reason"] = scoring_change_reason
 95        resp = self._http.request(
 96            "PATCH", f"/admin/competitions/{competition_id}/boards/{slug}", json=payload
 97        )
 98        _raise_for_status(resp)
 99        return CompetitionBoardInfo.model_validate(resp.json())
100
101    def promote(self, competition_id: str, slug: str) -> CompetitionBoardInfo:
102        """Publish a shadow board: put it on the board page's tabs and every
103        surface. Allowed at any time — it adds a standing, it moves none."""
104        return self.update(competition_id, slug, visibility="published")
105
106    def delete(self, competition_id: str, slug: str) -> CompetitionBoardsResponse:
107        """Remove one extra board; returns the boards as they now stand."""
108        resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}/boards/{slug}")
109        _raise_for_status(resp)
110        return CompetitionBoardsResponse.model_validate(resp.json())
class AdminCompetitionBoardsResource:
 21class AdminCompetitionBoardsResource:
 22    """Add, list, edit, publish and remove one competition's boards. For its
 23    organisers (global admins and the event's own competition admins)."""
 24
 25    def __init__(self, http: BaseHttpClient) -> None:
 26        self._http = http
 27
 28    def list(self, competition_id: str) -> CompetitionBoardsResponse:
 29        """Every board, the primary first and the shadow ones included —
 30        the organiser's read of ``GET /competitions/{id}/boards``."""
 31        resp = self._http.request("GET", f"/competitions/{competition_id}/boards")
 32        _raise_for_status(resp)
 33        return CompetitionBoardsResponse.model_validate(resp.json())
 34
 35    def create(
 36        self,
 37        competition_id: str,
 38        *,
 39        slug: str,
 40        rule: ScoringRuleName,
 41        params: dict[str, Any] | None = None,
 42        title: str | LocalizedText = "",
 43        visibility: BoardVisibility = "shadow",
 44        position: int = 0,
 45    ) -> CompetitionBoardInfo:
 46        """Add a board. It starts ``shadow`` — computed on the real facts and
 47        read by organisers alone — unless ``visibility="published"``.
 48        ``title`` is ``{locale: text}`` (a bare string is read as ``en``)."""
 49        payload: dict[str, Any] = {
 50            "slug": slug,
 51            "rule": rule,
 52            "params": params or {},
 53            "title": title,
 54            "visibility": visibility,
 55            "position": position,
 56        }
 57        resp = self._http.request(
 58            "POST", f"/admin/competitions/{competition_id}/boards", json=payload
 59        )
 60        _raise_for_status(resp)
 61        return CompetitionBoardInfo.model_validate(resp.json())
 62
 63    def update(
 64        self,
 65        competition_id: str,
 66        slug: str,
 67        *,
 68        rule: ScoringRuleName | None = None,
 69        params: dict[str, Any] | None = None,
 70        title: str | LocalizedText | None = None,
 71        visibility: BoardVisibility | None = None,
 72        position: int | None = None,
 73        scoring_change_reason: str | None = None,
 74    ) -> CompetitionBoardInfo:
 75        """Edit one board. Partial: only the arguments you pass are sent.
 76
 77        ⚠️ ``params`` **replaces** the board's params, and a new ``rule``
 78        without ``params`` starts from that rule's defaults. Once the
 79        competition is live, changing a *published* board's scoring needs
 80        ``scoring_change_reason``: the change is recorded and shown on the
 81        board (``422 scoring_change_needs_reason`` without one). A shadow
 82        board is edited freely."""
 83        payload: dict[str, Any] = {}
 84        if rule is not None:
 85            payload["rule"] = rule
 86        if params is not None:
 87            payload["params"] = params
 88        if title is not None:
 89            payload["title"] = title
 90        if visibility is not None:
 91            payload["visibility"] = visibility
 92        if position is not None:
 93            payload["position"] = position
 94        if scoring_change_reason is not None:
 95            payload["scoring_change_reason"] = scoring_change_reason
 96        resp = self._http.request(
 97            "PATCH", f"/admin/competitions/{competition_id}/boards/{slug}", json=payload
 98        )
 99        _raise_for_status(resp)
100        return CompetitionBoardInfo.model_validate(resp.json())
101
102    def promote(self, competition_id: str, slug: str) -> CompetitionBoardInfo:
103        """Publish a shadow board: put it on the board page's tabs and every
104        surface. Allowed at any time — it adds a standing, it moves none."""
105        return self.update(competition_id, slug, visibility="published")
106
107    def delete(self, competition_id: str, slug: str) -> CompetitionBoardsResponse:
108        """Remove one extra board; returns the boards as they now stand."""
109        resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}/boards/{slug}")
110        _raise_for_status(resp)
111        return CompetitionBoardsResponse.model_validate(resp.json())

Add, list, edit, publish and remove one competition's boards. For its organisers (global admins and the event's own competition admins).

AdminCompetitionBoardsResource(http: ctfy.sdk.base.BaseHttpClient)
25    def __init__(self, http: BaseHttpClient) -> None:
26        self._http = http
def list( self, competition_id: str) -> ctfy.server.models.CompetitionBoardsResponse:
28    def list(self, competition_id: str) -> CompetitionBoardsResponse:
29        """Every board, the primary first and the shadow ones included —
30        the organiser's read of ``GET /competitions/{id}/boards``."""
31        resp = self._http.request("GET", f"/competitions/{competition_id}/boards")
32        _raise_for_status(resp)
33        return CompetitionBoardsResponse.model_validate(resp.json())

Every board, the primary first and the shadow ones included — the organiser's read of GET /competitions/{id}/boards.

def create( self, competition_id: str, *, slug: str, rule: Literal['flag_count', 'static_points', 'dynamic_points', 'awd', 'exercise', 'peer_authoring', 'authoring'], params: dict[str, typing.Any] | None = None, title: str | dict[str, str] = '', visibility: Literal['published', 'shadow'] = 'shadow', position: int = 0) -> ctfy.server.models.CompetitionBoardInfo:
35    def create(
36        self,
37        competition_id: str,
38        *,
39        slug: str,
40        rule: ScoringRuleName,
41        params: dict[str, Any] | None = None,
42        title: str | LocalizedText = "",
43        visibility: BoardVisibility = "shadow",
44        position: int = 0,
45    ) -> CompetitionBoardInfo:
46        """Add a board. It starts ``shadow`` — computed on the real facts and
47        read by organisers alone — unless ``visibility="published"``.
48        ``title`` is ``{locale: text}`` (a bare string is read as ``en``)."""
49        payload: dict[str, Any] = {
50            "slug": slug,
51            "rule": rule,
52            "params": params or {},
53            "title": title,
54            "visibility": visibility,
55            "position": position,
56        }
57        resp = self._http.request(
58            "POST", f"/admin/competitions/{competition_id}/boards", json=payload
59        )
60        _raise_for_status(resp)
61        return CompetitionBoardInfo.model_validate(resp.json())

Add a board. It starts shadow — computed on the real facts and read by organisers alone — unless visibility="published". title is {locale: text} (a bare string is read as en).

def update( self, competition_id: str, slug: str, *, rule: Optional[Literal['flag_count', 'static_points', 'dynamic_points', 'awd', 'exercise', 'peer_authoring', 'authoring']] = None, params: dict[str, typing.Any] | None = None, title: str | dict[str, str] | None = None, visibility: Optional[Literal['published', 'shadow']] = None, position: int | None = None, scoring_change_reason: str | None = None) -> ctfy.server.models.CompetitionBoardInfo:
 63    def update(
 64        self,
 65        competition_id: str,
 66        slug: str,
 67        *,
 68        rule: ScoringRuleName | None = None,
 69        params: dict[str, Any] | None = None,
 70        title: str | LocalizedText | None = None,
 71        visibility: BoardVisibility | None = None,
 72        position: int | None = None,
 73        scoring_change_reason: str | None = None,
 74    ) -> CompetitionBoardInfo:
 75        """Edit one board. Partial: only the arguments you pass are sent.
 76
 77        ⚠️ ``params`` **replaces** the board's params, and a new ``rule``
 78        without ``params`` starts from that rule's defaults. Once the
 79        competition is live, changing a *published* board's scoring needs
 80        ``scoring_change_reason``: the change is recorded and shown on the
 81        board (``422 scoring_change_needs_reason`` without one). A shadow
 82        board is edited freely."""
 83        payload: dict[str, Any] = {}
 84        if rule is not None:
 85            payload["rule"] = rule
 86        if params is not None:
 87            payload["params"] = params
 88        if title is not None:
 89            payload["title"] = title
 90        if visibility is not None:
 91            payload["visibility"] = visibility
 92        if position is not None:
 93            payload["position"] = position
 94        if scoring_change_reason is not None:
 95            payload["scoring_change_reason"] = scoring_change_reason
 96        resp = self._http.request(
 97            "PATCH", f"/admin/competitions/{competition_id}/boards/{slug}", json=payload
 98        )
 99        _raise_for_status(resp)
100        return CompetitionBoardInfo.model_validate(resp.json())

Edit one board. Partial: only the arguments you pass are sent.

⚠️ params replaces the board's params, and a new rule without params starts from that rule's defaults. Once the competition is live, changing a published board's scoring needs scoring_change_reason: the change is recorded and shown on the board (422 scoring_change_needs_reason without one). A shadow board is edited freely.

def promote( self, competition_id: str, slug: str) -> ctfy.server.models.CompetitionBoardInfo:
102    def promote(self, competition_id: str, slug: str) -> CompetitionBoardInfo:
103        """Publish a shadow board: put it on the board page's tabs and every
104        surface. Allowed at any time — it adds a standing, it moves none."""
105        return self.update(competition_id, slug, visibility="published")

Publish a shadow board: put it on the board page's tabs and every surface. Allowed at any time — it adds a standing, it moves none.

def delete( self, competition_id: str, slug: str) -> ctfy.server.models.CompetitionBoardsResponse:
107    def delete(self, competition_id: str, slug: str) -> CompetitionBoardsResponse:
108        """Remove one extra board; returns the boards as they now stand."""
109        resp = self._http.request("DELETE", f"/admin/competitions/{competition_id}/boards/{slug}")
110        _raise_for_status(resp)
111        return CompetitionBoardsResponse.model_validate(resp.json())

Remove one extra board; returns the boards as they now stand.