diff --git a/.github/jobs/configure-checks/all.bats b/.github/jobs/configure-checks/all.bats index 8f82797f8a..666d391564 100755 --- a/.github/jobs/configure-checks/all.bats +++ b/.github/jobs/configure-checks/all.bats @@ -67,9 +67,9 @@ repo-remove () { assert_line "checking for gcc... no" assert_line "checking for cc... no" assert_line "checking for cl.exe... no" - assert_line "configure: error: in \`${test_path}':" + assert_regex "configure: error: in .${test_path}':" assert_line 'configure: error: no acceptable C compiler found in $PATH' - assert_line "See \`config.log' for more details" + assert_regex "See [\`']config.log' for more details" } compiler_assertions () { @@ -111,6 +111,10 @@ compile_assertions_finished () { } @test "Install GNU C only" { + if [ "$distro_id" = "ID=fedora" ]; then + # Fedora ships with a gcc with enough C++ support + skip + fi repo-remove clang g++ repo-install gcc libcgroup-dev compiler_assertions gcc '' diff --git a/.github/workflows/autoconf-check-different-distro.yml b/.github/workflows/autoconf-check-different-distro.yml index 8216eb3a26..9db54dfaa8 100644 --- a/.github/workflows/autoconf-check-different-distro.yml +++ b/.github/workflows/autoconf-check-different-distro.yml @@ -20,7 +20,7 @@ jobs: image: ${{ matrix.os }}:${{ matrix.version }} steps: - name: Install git so we get the .github directory - run: dnf install -y git + run: dnf install -y git composer - uses: actions/checkout@v4 - name: Setup image and run bats tests run: .github/jobs/configure-checks/setup_configure_image.sh diff --git a/.github/workflows/autoconf-check.yml b/.github/workflows/autoconf-check.yml index 016ec024f7..58f6f691d1 100644 --- a/.github/workflows/autoconf-check.yml +++ b/.github/workflows/autoconf-check.yml @@ -32,7 +32,7 @@ jobs: image: ${{ matrix.os }}:${{ matrix.version }} steps: - name: Install git so we get the .github directory - run: apt-get update; apt-get install -y git + run: apt-get update; apt-get install -y git composer - uses: actions/checkout@v4 - name: Setup image and run bats tests run: .github/jobs/configure-checks/setup_configure_image.sh diff --git a/.github/workflows/mayhem-api-template.yml b/.github/workflows/mayhem-api-template.yml deleted file mode 100644 index 9556bb3718..0000000000 --- a/.github/workflows/mayhem-api-template.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: "Mayhem API analysis (Template)" - -on: - workflow_call: - inputs: - version: - required: true - type: string - duration: - required: true - type: string - secrets: - MAPI_TOKEN: - required: true - -jobs: - mayhem: - name: Mayhem API analysis - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - env: - DB_DATABASE: domjudge - DB_USER: user - DB_PASSWORD: password - steps: - - uses: actions/checkout@v4 - - - name: Install DOMjudge - run: .github/jobs/baseinstall.sh ${{ inputs.version }} - - - name: Dump the OpenAPI - run: .github/jobs/getapi.sh - - - uses: actions/upload-artifact@v3 - if: ${{ inputs.version == 'guest' }} - with: - name: all-apispec - path: | - /home/runner/work/domjudge/domjudge/openapi.json - - - name: Mayhem for API - uses: ForAllSecure/mapi-action@v1 - if: ${{ inputs.version == 'guest' }} - continue-on-error: true - with: - mapi-token: ${{ secrets.MAPI_TOKEN }} - api-url: http://localhost/domjudge - api-spec: http://localhost/domjudge/api/doc.json # swagger/openAPI doc hosted here - duration: "auto" # Only spend time if we need to recheck issues from last time or find issues - sarif-report: mapi.sarif - run-args: | - --config - .github/jobs/data/mapi.config - --ignore-endpoint - ".*strict=true.*" - --ignore-endpoint - ".*strict=True.*" - - - name: Mayhem for API (For application role) - uses: ForAllSecure/mapi-action@v1 - if: ${{ inputs.version != 'guest' }} - continue-on-error: true - with: - mapi-token: ${{ secrets.MAPI_TOKEN }} - target: domjudge-${{ inputs.version }} - api-url: http://localhost/domjudge - api-spec: http://localhost/domjudge/api/doc.json # swagger/openAPI doc hosted here - duration: "${{ inputs.duration }}" - sarif-report: mapi.sarif - run-args: | - --config - .github/jobs/data/mapi.config - --basic-auth - admin:password - --ignore-endpoint - ".*strict=true.*" - --ignore-endpoint - ".*strict=True.*" - - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: mapi.sarif - - - uses: actions/upload-artifact@v3 - with: - name: ${{ inputs.version }}-logs - path: | - /var/log/nginx - /opt/domjudge/domserver/webapp/var/log/*.log diff --git a/.github/workflows/mayhem-daily.yml b/.github/workflows/mayhem-daily.yml deleted file mode 100644 index 2118bf6920..0000000000 --- a/.github/workflows/mayhem-daily.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: "Mayhem API daily (admin role only)" - -on: - schedule: - - cron: '0 23 * * *' - -jobs: - mayhem-template: - uses: ./.github/workflows/mayhem-api-template.yml - with: - version: "admin" - duration: "auto" - secrets: - MAPI_TOKEN: ${{ secrets.MAPI_TOKEN }} diff --git a/.github/workflows/mayhem-weekly.yml b/.github/workflows/mayhem-weekly.yml deleted file mode 100644 index 71cc90ecba..0000000000 --- a/.github/workflows/mayhem-weekly.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: "Mayhem API weekly (all roles)" - -on: - schedule: - - cron: '0 23 * * 0' - -jobs: - mayhem-template: - strategy: - matrix: - include: - - version: "team" - duration: "5m" - - version: "guest" - duration: "auto" - - version: "jury" - duration: "5min" - - version: "admin" - duration: "10m" - uses: ./.github/workflows/mayhem-api-template.yml - with: - version: "${{ matrix.version }}" - duration: "${{ matrix.duration }}" - secrets: - MAPI_TOKEN: ${{ secrets.MAPI_TOKEN }} diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index d2577b6018..94197d7421 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -16,8 +16,9 @@ jobs: - uses: actions/checkout@v4 - name: Install DOMjudge run: .github/jobs/baseinstall.sh admin - - uses: php-actions/phpstan@v3 + - uses: php-actions/phpstan@v3.0.2 with: configuration: phpstan.dist.neon path: webapp/src webapp/tests php_extensions: gd intl mysqli pcntl zip + version: composer diff --git a/ChangeLog b/ChangeLog index 30ebc035f2..841b02c982 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,14 @@ DOMjudge Programming Contest Judging System +Version 8.3.2 - 9 July 2025 +--------------------------- + - Expose samples/problemset after the contest start + +Version 8.3.1 - 13 September 2024 +--------------------------------- + - Create autoload_runtime.php as normal user to prevent a composer warning. + - Fix saving new problems with problem statement from web UI. + Version 8.3.0 - 31 May 2024 --------------------------- - [security] Close metadata file descriptor for the child in runguard. diff --git a/Makefile b/Makefile index df4f2069a0..1f8bea7047 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ endif domserver: domserver-configure paths.mk config judgehost: judgehost-configure paths.mk config docs: paths.mk config -install-domserver: domserver composer-dump-autoload domserver-create-dirs +install-domserver: domserver domserver-create-dirs install-judgehost: judgehost judgehost-create-dirs install-docs: docs-create-dirs dist: configure composer-dependencies @@ -76,12 +76,6 @@ endif composer-dependencies-dev: composer $(subst 1,-q,$(QUIET)) install --prefer-dist --no-scripts --no-plugins -# Dump autoload dependencies (including plugins) -# This is needed since symfony/runtime is a Composer plugin that runs while dumping -# the autoload file -composer-dump-autoload: - composer $(subst 1,-q,$(QUIET)) dump-autoload -o -a - composer-dump-autoload-dev: composer $(subst 1,-q,$(QUIET)) dump-autoload @@ -101,7 +95,7 @@ build-scripts: # List of SUBDIRS for recursive targets: build: SUBDIRS= lib misc-tools -domserver: SUBDIRS=etc sql misc-tools webapp +domserver: SUBDIRS=etc lib sql misc-tools webapp install-domserver: SUBDIRS=etc lib sql misc-tools webapp example_problems judgehost: SUBDIRS=etc judge misc-tools install-judgehost: SUBDIRS=etc lib judge misc-tools @@ -222,7 +216,7 @@ webapp/.env.local: # symlinks where necessary to let it work from the source tree. # This stuff is a hack! maintainer-install: inplace-install composer-dump-autoload-dev -inplace-install: build composer-dump-autoload domserver-create-dirs judgehost-create-dirs +inplace-install: build domserver-create-dirs judgehost-create-dirs inplace-install-l: # Replace libjudgedir with symlink to prevent lots of symlinks: -rmdir $(judgehost_libjudgedir) @@ -341,5 +335,5 @@ clean-autoconf: $(addprefix inplace-,conf conf-common install uninstall) \ $(addprefix maintainer-,conf install) clean-autoconf config distdocs \ composer-dependencies composer-dependencies-dev \ - composer-dump-autoload composer-dump-autoload-dev \ + composer-dump-autoload-dev \ coverity-conf coverity-build diff --git a/README.md b/README.md index 5499da3e0a..3361eb3ffc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ DOMjudge [![Coverity Scan Status](https://img.shields.io/coverity/scan/671.svg)](https://scan.coverity.com/projects/domjudge) [![CodeQL alerts](https://github.com/DOMjudge/domjudge/actions/workflows/codeql-analysis.yml/badge.svg?branch=main&event=push)](https://github.com/DOMjudge/domjudge/actions/workflows/codeql-analysis.yml) -This is the Programming Contest Jury System "DOMjudge" version 8.3.0 +This is the Programming Contest Jury System "DOMjudge" version 8.3.2 DOMjudge is a system for running a programming contest, like the ICPC regional and world championship programming contests. diff --git a/doc/manual/config-advanced.rst b/doc/manual/config-advanced.rst index 72b8016740..96150a8aeb 100644 --- a/doc/manual/config-advanced.rst +++ b/doc/manual/config-advanced.rst @@ -238,7 +238,7 @@ selected in the ``special_run`` and/or ``special_compare`` fields of the problem (an empty value means that the default run and compare scripts should be used; the defaults can be set in the global configuration settings). When creating custom run and compare -programs, we recommend re-using wrapper scripts that handle the +programs, we recommend reusing wrapper scripts that handle the tedious, standard part. See the boolfind example for details. Compare programs diff --git a/doc/manual/import.rst b/doc/manual/import.rst index 2b685943b4..6994210f16 100644 --- a/doc/manual/import.rst +++ b/doc/manual/import.rst @@ -405,11 +405,11 @@ and click `Import`. To import the file using the API run the following commands:: - http --check-status -b -f POST "/contests//problems" data@problems.yaml + http --check-status -b -f POST "/contests//problems/add-data" data@problems.yaml To import the file using the CLI run the following command:: - /bin/console api:call -m POST -f data=problems.yaml contests//problems + /bin/console api:call -m POST -f data=problems.yaml contests//problems/add-data Replace ```` with the contest ID that was returned when importing the contest metadata. diff --git a/doc/manual/install-judgehost.rst b/doc/manual/install-judgehost.rst index fafe28c7f8..454e38246f 100644 --- a/doc/manual/install-judgehost.rst +++ b/doc/manual/install-judgehost.rst @@ -68,6 +68,15 @@ example to install DOMjudge in the directory ``domjudge`` under `/opt`:: make judgehost sudo make install-judgehost +Example service files for the judgehost and the judgedaemon are provided in +``judge/create-cgroups.service`` and ``judge/domjudge-judgedaemon@.service``. The rest of the manual assumes you install those +in a location which is picked up by ``systemd``, for example ``/etc/systemd/system``. + +.. parsed-literal:: + + cp judge/domjudge-judgedaemon@.service /etc/systemd/system/ + cp judge/create-cgroups.service /etc/systemd/system/ + The judgedaemon can be run on various hardware configurations; - A virtual machine, typically these have 1 or 2 cores and no hyperthreading, because the kernel will schedule its own tasks on CPU 0, we advice CPU 1, @@ -172,7 +181,8 @@ any other tasks on the same CPU core the judgedaemon is using: On modern distros (e.g. Debian bullseye and Ubuntu Jammy Jellyfish) which have cgroup v2 enabled by default, you need to add ``systemd.unified_cgroup_hierarchy=0`` -as well. Then run ``update-grub`` and reboot. +as well. If you are running systemd v257, you also need to add `SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1`. +Then run ``update-grub`` and reboot. After rebooting check that ``/proc/cmdline`` actually contains the added kernel options. On VM hosting providers such as Google Cloud or DigitalOcean, ``GRUB_CMDLINE_LINUX_DEFAULT`` may be overwritten diff --git a/doc/manual/problem-format.rst b/doc/manual/problem-format.rst index e9f6cbc300..4439cedc2d 100644 --- a/doc/manual/problem-format.rst +++ b/doc/manual/problem-format.rst @@ -54,4 +54,4 @@ problem by uploading a zip file that contains only testcase files. Any jury solutions present will be automatically submitted when ``allow_submit`` is ``1`` and there's a team associated with the uploading user. -.. _ICPC problem package specification: https://icpc.io/problem-package-format/spec/problem_package_format +.. _ICPC problem package specification: https://icpc.io/problem-package-format/spec/legacy-icpc diff --git a/gitlab/ci/template.yml b/gitlab/ci/template.yml index 5b954aff68..1c75a358a9 100644 --- a/gitlab/ci/template.yml +++ b/gitlab/ci/template.yml @@ -36,7 +36,6 @@ - /bin/true services: - name: mysql - command: ["--mysql-native-password", "--authentication_policy=mysql_native_password"] alias: sqlserver .mariadb_job: diff --git a/lib/Makefile b/lib/Makefile index 5f2e2dea76..24cd0f82de 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -1,6 +1,9 @@ ifndef TOPDIR TOPDIR=.. endif + +REC_TARGETS = domserver + include $(TOPDIR)/Makefile.global OBJECTS = $(addsuffix $(OBJEXT),lib.error lib.misc) @@ -31,3 +34,5 @@ install-domserver: install-judgehost: $(INSTALL_DATA) -t $(DESTDIR)$(judgehost_libdir) *.php *.sh $(INSTALL_PROG) -t $(DESTDIR)$(judgehost_libdir) alert + +domserver: SUBDIRS=vendor diff --git a/lib/vendor/Makefile b/lib/vendor/Makefile new file mode 100644 index 0000000000..b6ee26a361 --- /dev/null +++ b/lib/vendor/Makefile @@ -0,0 +1,12 @@ +ifndef TOPDIR +TOPDIR=../.. +endif +include $(TOPDIR)/Makefile.global + +clean-l: + rm -f autoload_runtime.php + +autoload_runtime.php: + composer $(subst 1,-q,$(QUIET)) dump-autoload -o -a -d $(TOPDIR) + +domserver: autoload_runtime.php diff --git a/misc-tools/dj_make_chroot.in b/misc-tools/dj_make_chroot.in index aa9f80e772..3d03f9935c 100755 --- a/misc-tools/dj_make_chroot.in +++ b/misc-tools/dj_make_chroot.in @@ -178,7 +178,7 @@ if [ "$DISTRO" = 'Debian' ]; then REMOVEDEBS="" # Which debootstrap package to install on non-Debian systems: - DEBOOTDEB="debootstrap_1.0.128+nmu2+deb12u1_all.deb" + DEBOOTDEB="debootstrap_1.0.128+nmu2+deb12u2_all.deb" # The Debian mirror/proxy below can be passed as environment # variables; if none are given the following defaults are used. diff --git a/misc-tools/import-contest.in b/misc-tools/import-contest.in index 756c03d7f0..79d6ffaa9a 100755 --- a/misc-tools/import-contest.in +++ b/misc-tools/import-contest.in @@ -149,10 +149,10 @@ if import_file('organizations', ['organizations.json']): # Also import logos if we have any # We prefer the 64x64 logo. If it doesn't exist, accept a generic logo (which might be a SVG) # We also prefer PNG/SVG before JPG - import_images('organizations', 'logo', ['^logo\.64x\d+\.png$', '^logo\.(png|svg)$', '^logo\.64x\d+\.jpg$', '^logo\.jpg$']) + import_images('organizations', 'logo', ['^logo\\.64x\\d+\\.png$', '^logo\\.(png|svg)$', '^logo\\.64x\\d+\\.jpg$', '^logo\\.jpg$']) if import_file('teams', ['teams.json', 'teams2.tsv']): # Also import photos if we have any, but prefer JPG over SVG and PNG - import_images('teams', 'photo', ['^photo\.jpg$', '^photo\.(png|svg)$']) + import_images('teams', 'photo', ['^photo\\.jpg$', '^photo\\.(png|svg)$']) import_file('accounts', ['accounts.json', 'accounts.yaml', 'accounts.tsv']) problems_imported = False diff --git a/webapp/config/packages/nelmio_api_doc.yaml b/webapp/config/packages/nelmio_api_doc.yaml index 2453372b42..5904171595 100644 --- a/webapp/config/packages/nelmio_api_doc.yaml +++ b/webapp/config/packages/nelmio_api_doc.yaml @@ -1,9 +1,9 @@ nelmio_api_doc: documentation: servers: - - url: "%domjudge.baseurl%api" + - url: ~ # Will be set by App\NelmioApiDocBundle\ExternalDocDescriber description: API used at this contest - - url: https://www.domjudge.org/demoweb/api + - url: https://www.domjudge.org/demoweb description: New API in development info: title: DOMjudge diff --git a/webapp/src/Controller/API/AbstractApiController.php b/webapp/src/Controller/API/AbstractApiController.php index 163f000bba..b5c43a76d9 100644 --- a/webapp/src/Controller/API/AbstractApiController.php +++ b/webapp/src/Controller/API/AbstractApiController.php @@ -34,9 +34,11 @@ public function __construct( * Get the query builder used for getting contests. * * @param bool $onlyActive return only contests that are active + * @param bool $filterBeforeContest return only contests that have started */ - protected function getContestQueryBuilder(bool $onlyActive = false): QueryBuilder - { + protected function getContestQueryBuilder( + bool $onlyActive = false, bool $filterBeforeContest = true + ): QueryBuilder { $now = Utils::now(); $qb = $this->em->createQueryBuilder(); $qb @@ -63,6 +65,10 @@ protected function getContestQueryBuilder(bool $onlyActive = false): QueryBuilde } else { $qb->andWhere('c.public = 1'); } + if ($filterBeforeContest) { + $qb->andWhere('c.starttime <= :now') + ->setParameter('now', $now); + } } return $qb; @@ -77,7 +83,10 @@ protected function getContestId(Request $request): int throw new BadRequestHttpException('cid parameter missing'); } - $qb = $this->getContestQueryBuilder($request->query->getBoolean('onlyActive', false)); + $qb = $this->getContestQueryBuilder( + onlyActive: $request->query->getBoolean('onlyActive', false), + filterBeforeContest: false + ); $qb ->andWhere(sprintf('c.%s = :cid', $this->getContestIdField())) ->setParameter('cid', $request->attributes->get('cid')); diff --git a/webapp/src/Controller/API/ContestController.php b/webapp/src/Controller/API/ContestController.php index 317d49cdcb..b0f4ce8ceb 100644 --- a/webapp/src/Controller/API/ContestController.php +++ b/webapp/src/Controller/API/ContestController.php @@ -179,7 +179,7 @@ public function singleAction(Request $request, string $cid): Response public function bannerAction(Request $request, string $cid): Response { /** @var Contest|null $contest */ - $contest = $this->getQueryBuilder($request) + $contest = $this->getQueryBuilder($request, filterBeforeContest: false) ->andWhere(sprintf('%s = :id', $this->getIdField())) ->setParameter('id', $cid) ->getQuery() @@ -934,10 +934,10 @@ public function samplesDataZipAction(Request $request): Response return $this->dj->getSamplesZipForContest($contest); } - protected function getQueryBuilder(Request $request): QueryBuilder + protected function getQueryBuilder(Request $request, bool $filterBeforeContest = true): QueryBuilder { try { - return $this->getContestQueryBuilder($request->query->getBoolean('onlyActive', false)); + return $this->getContestQueryBuilder($request->query->getBoolean('onlyActive', false), $filterBeforeContest); } catch (TypeError) { throw new BadRequestHttpException('\'onlyActive\' must be a boolean.'); } @@ -954,7 +954,7 @@ protected function getIdField(): string */ protected function getContestWithId(Request $request, string $id): Contest { - $queryBuilder = $this->getQueryBuilder($request) + $queryBuilder = $this->getQueryBuilder($request, filterBeforeContest: false) ->andWhere(sprintf('%s = :id', $this->getIdField())) ->setParameter('id', $id); @@ -971,7 +971,7 @@ protected function getContestWithId(Request $request, string $id): Contest private function getContestAndCheckIfLocked(Request $request, string $cid): Contest { /** @var Contest|null $contest */ - $contest = $this->getQueryBuilder($request) + $contest = $this->getQueryBuilder($request, filterBeforeContest: false) ->andWhere(sprintf('%s = :id', $this->getIdField())) ->setParameter('id', $cid) ->getQuery() diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 56ed94d5f8..95d40eafdb 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -1087,7 +1087,6 @@ public function addAction(Request $request): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->em->persist($problem); $this->saveEntity($this->em, $this->eventLogService, $this->dj, $problem, null, true); return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); } diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 2cdba5b922..0cc075696c 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -250,7 +250,7 @@ public function requestRemainingRunsWholeTeamCategoryAction(string $categoryId): ->join('t.category', 'tc') ->andWhere('j.valid = true') ->andWhere('j.result != :compiler_error') - ->andWhere('tc.category = :categoryId') + ->andWhere('tc.categoryid = :categoryId') ->setParameter('compiler_error', 'compiler-error') ->setParameter('categoryId', $categoryId); if ($contestId > -1) { diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index 648b4bf727..e714d6737b 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -95,12 +95,10 @@ public function homeAction(Request $request): Response $clarifications = $this->em->createQueryBuilder() ->from(Clarification::class, 'c') ->leftJoin('c.problem', 'p') - ->leftJoin('p.contest_problems', 'cp') ->leftJoin('c.sender', 's') ->leftJoin('c.recipient', 'r') - ->select('c', 'cp', 'p') + ->select('c', 'p') ->andWhere('c.contest = :contest') - ->andWhere('cp.contest = :contest') ->andWhere('c.sender IS NULL') ->andWhere('c.recipient = :team OR c.recipient IS NULL') ->setParameter('contest', $contest) @@ -114,12 +112,10 @@ public function homeAction(Request $request): Response $clarificationRequests = $this->em->createQueryBuilder() ->from(Clarification::class, 'c') ->leftJoin('c.problem', 'p') - ->leftJoin('p.contest_problems', 'cp') ->leftJoin('c.sender', 's') ->leftJoin('c.recipient', 'r') - ->select('c', 'cp', 'p') + ->select('c', 'p') ->andWhere('c.contest = :contest') - ->andWhere('cp.contest = :contest') ->andWhere('c.sender = :team') ->setParameter('contest', $contest) ->setParameter('team', $team) diff --git a/webapp/src/DataTransferObject/Shadowing/ProblemEvent.php b/webapp/src/DataTransferObject/Shadowing/ProblemEvent.php index 2aa47e6e27..7c5b76d348 100644 --- a/webapp/src/DataTransferObject/Shadowing/ProblemEvent.php +++ b/webapp/src/DataTransferObject/Shadowing/ProblemEvent.php @@ -7,7 +7,7 @@ class ProblemEvent implements EventData public function __construct( public readonly string $id, public readonly string $name, - public readonly int $timeLimit, + public readonly float $timeLimit, public readonly ?string $label, public readonly ?string $rgb, ) {} diff --git a/webapp/src/Entity/Clarification.php b/webapp/src/Entity/Clarification.php index ed62d6013f..b52765b218 100644 --- a/webapp/src/Entity/Clarification.php +++ b/webapp/src/Entity/Clarification.php @@ -236,6 +236,14 @@ public function getProblem(): ?Problem return $this->problem; } + public function getContestProblem(): ?ContestProblem + { + if (!$this->problem) { + return null; + } + return $this->contest->getContestProblem($this->problem); + } + #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('problem_id')] diff --git a/webapp/src/Entity/Contest.php b/webapp/src/Entity/Contest.php index 0b88932cd6..9c5cfe6ab3 100644 --- a/webapp/src/Entity/Contest.php +++ b/webapp/src/Entity/Contest.php @@ -913,6 +913,16 @@ public function getProblems(): Collection return $this->problems; } + public function getContestProblem(Problem $problem): ?ContestProblem + { + foreach ($this->getProblems() as $contestProblem) { + if ($contestProblem->getProblem() === $problem) { + return $contestProblem; + } + } + return null; + } + public function addClarification(Clarification $clarification): Contest { $this->clarifications[] = $clarification; diff --git a/webapp/src/NelmioApiDocBundle/ExternalDocDescriber.php b/webapp/src/NelmioApiDocBundle/ExternalDocDescriber.php new file mode 100644 index 0000000000..2805d51b55 --- /dev/null +++ b/webapp/src/NelmioApiDocBundle/ExternalDocDescriber.php @@ -0,0 +1,29 @@ +requestStack->getCurrentRequest(); + $this->decorated->describe($api); + Util::merge($api->servers[0], ['url' => $request->getSchemeAndHttpHost(),], true); + } +} diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 7238f43682..3972b77ed0 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -108,7 +108,7 @@ public function getFilters(): array new TwigFilter('tsvField', $this->toTsvField(...)), new TwigFilter('fileTypeIcon', $this->fileTypeIcon(...)), new TwigFilter('problemBadge', $this->problemBadge(...), ['is_safe' => ['html']]), - new TwigFilter('problemBadgeForProblemAndContest', $this->problemBadgeForProblemAndContest(...), ['is_safe' => ['html']]), + new TwigFilter('problemBadgeForContest', $this->problemBadgeForContest(...), ['is_safe' => ['html']]), new TwigFilter('printMetadata', $this->printMetadata(...), ['is_safe' => ['html']]), new TwigFilter('printWarningContent', $this->printWarningContent(...), ['is_safe' => ['html']]), new TwigFilter('entityIdBadge', $this->entityIdBadge(...), ['is_safe' => ['html']]), @@ -1032,8 +1032,8 @@ public function hexColorToRGBA(string $text, float $opacity = 1): string if (is_null($col)) { return $text; } - preg_match_all("/[0-9A-Fa-f]{2}/", $col, $m); - if (!count($m)) { + $ret = preg_match_all("/[0-9A-Fa-f]{2}/", $col, $m); + if (!($ret && count($m[0]))) { return $text; } @@ -1093,14 +1093,11 @@ public function problemBadge(ContestProblem $problem): string ); } - public function problemBadgeForProblemAndContest(Problem $problem, ?Contest $contest): string + public function problemBadgeForContest(Problem $problem, ?Contest $contest = null): string { - foreach ($problem->getContestProblems() as $contestProblem) { - if ($contestProblem->getContest() === $contest) { - return $this->problemBadge($contestProblem); - } - } - return ''; + $contest ??= $this->dj->getCurrentContest(); + $contestProblem = $contest?->getContestProblem($problem); + return $contestProblem === null ? '' : $this->problemBadge($contestProblem); } public function printMetadata(?string $metadata): string diff --git a/webapp/templates/jury/executable.html.twig b/webapp/templates/jury/executable.html.twig index 95cbe3956e..0cf94b261c 100644 --- a/webapp/templates/jury/executable.html.twig +++ b/webapp/templates/jury/executable.html.twig @@ -48,14 +48,14 @@ {% if executable.type == 'compare' %} {% for problem in executable.problemsCompare %} - p{{ problem.probid }} {{ problem | problemBadgeForProblemAndContest(current_contest) }} + p{{ problem.probid }} {{ problem | problemBadgeForContest }} {% set used = true %} {% endfor %} {% elseif executable.type == 'run' %} {% for problem in executable.problemsRun %} - p{{ problem.probid }} {{ problem | problemBadgeForProblemAndContest(current_contest) }} + p{{ problem.probid }} {{ problem | problemBadgeForContest }} {% set used = true %} {% endfor %} diff --git a/webapp/templates/jury/partials/clarification_list.html.twig b/webapp/templates/jury/partials/clarification_list.html.twig index 337499bb1c..09874023be 100644 --- a/webapp/templates/jury/partials/clarification_list.html.twig +++ b/webapp/templates/jury/partials/clarification_list.html.twig @@ -71,7 +71,7 @@ {%- if clarification.problem -%} - problem {{ clarification.problem.contestProblems.first | problemBadge -}} + problem {{ clarification.contestProblem | problemBadge -}} {%- elseif clarification.category -%} {{- categories[clarification.category]|default('general') -}} {%- else -%} diff --git a/webapp/templates/partials/problem_list.html.twig b/webapp/templates/partials/problem_list.html.twig index f00ef14482..0733241ddd 100644 --- a/webapp/templates/partials/problem_list.html.twig +++ b/webapp/templates/partials/problem_list.html.twig @@ -94,7 +94,7 @@ data-bs-toggle="tooltip" data-placement="top" data-html="true" - title="Between {{ stat.start.timestamp | printtime(null, contest) }} and {{ stat.end.timestamp | printtime(null, contest) }}:
{{ label }}"> + title="Between {{ stat.start.timestamp | printtime(null, contest) }} and {{ stat.end.timestamp | printtime(null, contest) }}:{{ '\n' }}{{ label }}"> {% endfor %} diff --git a/webapp/templates/team/partials/clarification.html.twig b/webapp/templates/team/partials/clarification.html.twig index eda98ce293..5004e4c0d4 100644 --- a/webapp/templates/team/partials/clarification.html.twig +++ b/webapp/templates/team/partials/clarification.html.twig @@ -4,7 +4,7 @@
Subject: {% if clarification.problem %} - Problem {{ clarification.problem.contestProblems.first.shortname }}: {{ clarification.problem.name }} + Problem {{ clarification.contestProblem.shortname }}: {{ clarification.problem.name }} {% elseif clarification.category %} {{ categories[clarification.category]|default('general') }} {% else %} diff --git a/webapp/templates/team/partials/clarification_list.html.twig b/webapp/templates/team/partials/clarification_list.html.twig index a36c1e0f30..47b154c542 100644 --- a/webapp/templates/team/partials/clarification_list.html.twig +++ b/webapp/templates/team/partials/clarification_list.html.twig @@ -44,7 +44,7 @@ {%- if clarification.problem -%} - problem {{ clarification.problem.contestProblems.first | problemBadge -}} + problem {{ clarification.contestProblem | problemBadge -}} {%- elseif clarification.category -%} {{- categories[clarification.category]|default('general') -}} {%- else -%} diff --git a/webapp/tests/Unit/Controller/Jury/JuryMiscControllerTest.php b/webapp/tests/Unit/Controller/Jury/JuryMiscControllerTest.php index 31e9b0bf89..bda8ae8583 100644 --- a/webapp/tests/Unit/Controller/Jury/JuryMiscControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/JuryMiscControllerTest.php @@ -91,7 +91,7 @@ public function testBalloonScoreboard(array $fixtures, bool $public, string $con $elements = ["3 tries",'Demo contest','Utrecht University']; } elseif (in_array($contestStage, ['preEnd','preUnfreeze'])) { $elements = ["0 + 4 tries","3 tries","2 + 1 tries",'Demo contest','Utrecht University']; - if ($contestStage === 'preFreeze') { + if ($contestStage === 'preUnfreeze') { $elements[] = 'contest over, waiting for results'; } } else {