From eee082a4157f131bdd89097eee923d94a5d795b3 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+mvr320@users.noreply.github.com> Date: Thu, 4 Nov 2021 23:58:27 +0100 Subject: [PATCH 01/13] Create stackhawk-analysis.yml --- .github/workflows/stackhawk-analysis.yml | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/stackhawk-analysis.yml diff --git a/.github/workflows/stackhawk-analysis.yml b/.github/workflows/stackhawk-analysis.yml new file mode 100644 index 0000000000..b8e3775bed --- /dev/null +++ b/.github/workflows/stackhawk-analysis.yml @@ -0,0 +1,57 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# 🦅 STACKHAWK https://stackhawk.com + +# The StackHawk HawkScan action makes it easy to integrate dynamic application security testing (DAST) into your +# CI pipeline. See the Getting Started guide (https://docs.stackhawk.com/hawkscan/) to get up and running with +# StackHawk quickly. + +# To use this workflow, you must: +# +# 1. Create an API Key and Application: Sign up for a free StackHawk account to obtain an API Key and +# create your first app and configuration file at https://app.stackhawk.com. +# +# 2. Save your API Key as a Secret: Save your API key as a GitHub Secret named HAWK_API_KEY. +# +# 3. Add your Config File: Add your stackhawk.yml configuration file to the base of your repository directory. +# +# 4. Set the Scan Failure Threshold: Add the hawk.failureThreshold configuration option +# (https://docs.stackhawk.com/hawkscan/configuration/#hawk) to your stackhawk.yml configuration file. If your scan +# produces alerts that meet or exceed the hawk.failureThreshold alert level, the scan will return exit code 42 +# and trigger a Code Scanning alert with a link to your scan results. +# +# 5. Update the "Start your service" Step: Update the "Start your service" step in the StackHawk workflow below to +# start your service so that it can be scanned with the "Run HawkScan" step. + + +name: "StackHawk" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '27 14 * * 1' + +jobs: + stackhawk: + name: StackHawk + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Start your service + run: ./your-service.sh & # ✏️ Update this to run your own service to be scanned + + - name: Run HawkScan + uses: stackhawk/hawkscan-action@4c3258cd62248dac6d9fe91dd8d45928c697dee0 + continue-on-error: true # ✏️ Set to false to break your build on scan errors + with: + apiKey: ${{ secrets.HAWK_API_KEY }} + codeScanningAlerts: true + githubToken: ${{ github.token }} From 87e37b0f6e94de4968d2b548eebd3afc3d4967ca Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+mvr320@users.noreply.github.com> Date: Thu, 4 Nov 2021 12:28:45 +0100 Subject: [PATCH 02/13] Update SAST languages For the current list see: https://docs.gitlab.com/ee/user/application_security/sast/analyzers.html --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 000cb4ff55..3b2e531a0e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - SAST_EXCLUDED_ANALYZERS: "brakeman,kubesec,mobsf,nodejs-scan,pmd-apex,security-code-scan,sobelow,spotbugs" + SAST_EXCLUDED_ANALYZERS: "brakeman,eslint,flawfinder,kubesec,mobsf,nodejs-scan,pmd-apex,security-code-scan,sobelow,spotbugs" stages: - visual_pre From c7314891a68e0df6a40d9b191650818ea0468a68 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+mvr320@users.noreply.github.com> Date: Thu, 4 Nov 2021 12:54:16 +0100 Subject: [PATCH 03/13] Show logging of the files SAST scanners parse --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3b2e531a0e..1399192919 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,6 @@ variables: SAST_EXCLUDED_ANALYZERS: "brakeman,eslint,flawfinder,kubesec,mobsf,nodejs-scan,pmd-apex,security-code-scan,sobelow,spotbugs" + SECURE_LOG_LEVEL: debug stages: - visual_pre From 7a1e10df4025b09caaa6601e36860528b7f88b65 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+mvr320@users.noreply.github.com> Date: Thu, 4 Nov 2021 12:55:21 +0100 Subject: [PATCH 04/13] Ignore the artifacts of earlier jobs. Overall ignore directories only used for CI. --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1399192919..46fce9ab98 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,8 @@ variables: SAST_EXCLUDED_ANALYZERS: "brakeman,eslint,flawfinder,kubesec,mobsf,nodejs-scan,pmd-apex,security-code-scan,sobelow,spotbugs" SECURE_LOG_LEVEL: debug + SAST_EXCLUDED_PATHS: "html,tests,localhost,gitlab" + SAST_FLAWFINDER_LEVEL: 5 stages: - visual_pre From ddf7c61f7e8f1b1461f1d48a1fa0b9e1ed1191e2 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+mvr320@users.noreply.github.com> Date: Thu, 4 Nov 2021 13:00:59 +0100 Subject: [PATCH 05/13] [BugFix] Pin SemGrep to a working version --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 46fce9ab98..063e9c7b4f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,7 @@ variables: SECURE_LOG_LEVEL: debug SAST_EXCLUDED_PATHS: "html,tests,localhost,gitlab" SAST_FLAWFINDER_LEVEL: 5 + SAST_ANALYZER_IMAGE_TAG: "2.13.1" stages: - visual_pre From 3890e8e0007cede6d861098bd1da57455f138c63 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+mvr320@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:02:21 +0100 Subject: [PATCH 06/13] Only pinpoint for the failing job. --- .gitlab-ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 063e9c7b4f..9bc3e7761d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,10 @@ variables: SECURE_LOG_LEVEL: debug SAST_EXCLUDED_PATHS: "html,tests,localhost,gitlab" SAST_FLAWFINDER_LEVEL: 5 - SAST_ANALYZER_IMAGE_TAG: "2.13.1" + +semgrep-sast: + variables: + SAST_ANALYZER_IMAGE_TAG: "2.13.1" stages: - visual_pre From bcdfc506fb3bcaa34181cf19eebc32e9064848f3 Mon Sep 17 00:00:00 2001 From: Tobias Werth Date: Fri, 5 Nov 2021 11:29:51 +0100 Subject: [PATCH 07/13] Fix blatant bug in prioritization in judge queue. Found when investigating #1303. Looks ok now: ``` +-------------+--------+-------+----------+--------------+-----------+ | queuetaskid | teamid | jobid | priority | teampriority | starttime | +-------------+--------+-------+----------+--------------+-----------+ | 2021 | 1 | 2016 | 0 | 1636109289 | NULL | | 2022 | 1 | 2017 | 0 | 1636109349 | NULL | | 2023 | 1 | 2018 | 0 | 1636109409 | NULL | | 2024 | 1 | 2019 | 0 | 1636109470 | NULL | | 2025 | 1 | 2020 | 0 | 1636109530 | NULL | | 2026 | 1 | 2021 | 0 | 1636109590 | NULL | | 2027 | 1 | 2022 | 0 | 1636109650 | NULL | | 2028 | 1 | 2023 | 0 | 1636109710 | NULL | ``` --- webapp/src/Service/DOMJudgeService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index 1e47dc6ab5..4a9ed09617 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -1219,7 +1219,7 @@ public function maybeCreateJudgeTasks(Judging $judging, int $priority = JudgeTas ->from(QueueTask::class, 'qt') ->select('MAX(qt.teamPriority) AS max, COUNT(qt.jobid) AS count') ->andWhere('qt.team = :team') - ->andWhere('qt.teamPriority = :priority') + ->andWhere('qt.priority = :priority') ->setParameter(':team', $team) ->setParameter(':priority', $priority) ->getQuery() From 9d90b4fdcf1028bf04103ed5adb2caeab0ef5d44 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 5 Nov 2021 14:40:30 +0100 Subject: [PATCH 08/13] Use main ports mirror since it seems to use a local mirror anyway. --- misc-tools/dj_make_chroot.in | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/misc-tools/dj_make_chroot.in b/misc-tools/dj_make_chroot.in index 409d69f9fc..7f5351d396 100755 --- a/misc-tools/dj_make_chroot.in +++ b/misc-tools/dj_make_chroot.in @@ -181,14 +181,10 @@ DEBOOTDEB="debootstrap_1.0.118ubuntu1_all.deb" if [ -z "$DEBMIRROR" ]; then # x86_64 can use the main Ubuntu repo's, other # architectures need to use a mirror from ubuntu-ports. - # Besides the main mirror (ports.ubuntu.com) currently - # only the one from kumi.systems seems to have most ports - # and it is faster than ports.ubuntu.com so we use that as - # default. if [ "$(uname -m)" = "x86_64" ]; then DEBMIRROR="http://us.archive.ubuntu.com./ubuntu/" else - DEBMIRROR="http://mirror.kumi.systems/ubuntu-ports/" + DEBMIRROR="http://ports.ubuntu.com/ubuntu-ports/" fi fi From 1926c3145ae4c66e77875f102913d4c1d9bcfbf0 Mon Sep 17 00:00:00 2001 From: MCJ Vasseur <14887731+mvr320@users.noreply.github.com> Date: Tue, 21 Sep 2021 16:17:58 +0200 Subject: [PATCH 09/13] Allow extending of the tests Test unit tests against MySQL --- .gitlab-ci.yml | 88 ++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9bc3e7761d..c4fa3f2b98 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,6 +23,17 @@ include: - template: SAST.gitlab-ci.yml - template: License-Scanning.gitlab-ci.yml +.db_config: &mysql_db + services: + - name: mysql + command: ["--default-authentication-plugin=mysql_native_password"] + alias: sqlserver + +.db_config: &maria_db + services: + - name: mariadb + alias: sqlserver + .retry_config: &retry_job retry: max: 0 #Max is 2, set when gitlab is flacky @@ -56,9 +67,36 @@ include: timeout: 30m # Due to the retry this will be worst case 3*timeout before the job fails +.unit_job_template: &unit_job + stage: unit + image: domjudge/gitlabci:2.1 + # Disabled for now as it drastically speeds up running unit tests and we don't use it yet + # before_script: + # - apt-get update -yqq + # - apt-get install php-xdebug -yqq + variables: + MYSQL_ROOT_PASSWORD: password + MARIADB_PORT_3306_TCP_ADDR: sqlserver + script: + - ./gitlab/unit-tests.sh + artifacts: + when: always + paths: + - unit-tests.xml + - coverage-html + - deprecation.txt + reports: + junit: + - unit-tests.xml + cache: + key: unit-tests + paths: + - lib/vendor/ + webstandard_check_role: <<: *matrix_retry_job <<: *short_job + <<: *maria_db parallel: matrix: - ROLE: public @@ -71,8 +109,6 @@ webstandard_check_role: TEST: [w3cval] stage: accessibility image: domjudge/gitlabci:2.1 - services: - - mariadb variables: MYSQL_ROOT_PASSWORD: password script: @@ -104,39 +140,21 @@ check static codecov: run unit tests: <<: *retry_job <<: *normal_job - stage: unit - image: domjudge/gitlabci:2.1 - # Disabled for now as it drastically speeds up running unit tests and we don't use it yet - # before_script: - # - apt-get update -yqq - # - apt-get install php-xdebug -yqq - services: - - mariadb - variables: - MYSQL_ROOT_PASSWORD: password - script: - - ./gitlab/unit-tests.sh - artifacts: - when: always - paths: - - unit-tests.xml - - coverage-html - - deprecation.txt - reports: - junit: - - unit-tests.xml - cache: - key: unit-tests - paths: - - lib/vendor/ + <<: *maria_db + <<: *unit_job + +run unit tests (MySQL): + <<: *retry_job + <<: *normal_job + <<: *mysql_db + <<: *unit_job visual_pr: <<: *retry_job <<: *long_job + <<: *maria_db stage: visual_pre image: domjudge/gitlabci:2.1 - services: - - mariadb variables: MYSQL_ROOT_PASSWORD: password DOCKER_HOST: tcp://docker:2375/ @@ -166,10 +184,9 @@ visual_pr: visual_main: <<: *retry_job <<: *long_job + <<: *maria_db stage: visual_pre image: domjudge/gitlabci:2.1 - services: - - mariadb variables: MYSQL_ROOT_PASSWORD: password DOCKER_HOST: tcp://docker:2375/ @@ -235,23 +252,18 @@ visual_compare: integration_mysql: <<: *job_integration + <<: *mysql_db variables: MYSQL_ROOT_PASSWORD: password MARIADB_PORT_3306_TCP_ADDR: sqlserver MYSQL_REQUIRE_PRIMARY_KEY: 1 - services: - - name: mysql - command: ["--default-authentication-plugin=mysql_native_password"] - alias: sqlserver integration_mariadb: <<: *job_integration + <<: *maria_db variables: MYSQL_ROOT_PASSWORD: password MARIADB_PORT_3306_TCP_ADDR: sqlserver - services: - - name: mariadb - alias: sqlserver phpcs_compatibility: <<: *tiny_job From fce0d5178d987bf2fc37fb751bb32a9c9d49d71e Mon Sep 17 00:00:00 2001 From: Jaap Eldering Date: Sat, 6 Nov 2021 00:58:10 +0100 Subject: [PATCH 10/13] Also install create_cgroups script in maintainer-install mode. This allows removing a workaround in the ansible code that modifies the path to this script in the systemd unit file. --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 8c6f72c2b9..7ea8e2ba17 100644 --- a/Makefile +++ b/Makefile @@ -212,6 +212,7 @@ maintainer-install: build domserver-create-dirs judgehost-create-dirs webapp/.en ln -sf $(CURDIR)/judge/judgedaemon $(judgehost_bindir) ln -sf $(CURDIR)/judge/runguard $(judgehost_bindir) ln -sf $(CURDIR)/judge/runpipe $(judgehost_bindir) + ln -sf $(CURDIR)/judge/create_cgroups $(judgehost_bindir) ln -sf $(CURDIR)/sql/dj_setup_database $(domserver_bindir) $(MAKE) -C misc-tools maintainer-install $(MAKE) -C doc/manual maintainer-install From 5ff5dd6472908e934e6b983f2562f215104ce1de Mon Sep 17 00:00:00 2001 From: Thijs Kinkhorst Date: Fri, 5 Nov 2021 20:52:36 +0000 Subject: [PATCH 11/13] Show jury-initiated clar to a specific team under General clarifications Before, these messages were not findable anywhere after sending them. Closes: #1306 --- .../Jury/ClarificationController.php | 2 +- .../Test/ClarificationFixture.php | 57 +++++++++++++++++++ .../API/ClarificationControllerTest.php | 4 +- .../Jury/ClarificationControllerTest.php | 31 ++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 webapp/src/DataFixtures/Test/ClarificationFixture.php diff --git a/webapp/src/Controller/Jury/ClarificationController.php b/webapp/src/Controller/Jury/ClarificationController.php index 21d67b9a4f..317952a073 100644 --- a/webapp/src/Controller/Jury/ClarificationController.php +++ b/webapp/src/Controller/Jury/ClarificationController.php @@ -110,7 +110,7 @@ public function indexAction(Request $request): Response $wheres = [ 'new' => 'clar.sender IS NOT NULL AND clar.answered = 0', 'old' => 'clar.sender IS NOT NULL AND clar.answered != 0', - 'general' => 'clar.sender IS NULL AND clar.recipient IS NULL', + 'general' => 'clar.sender IS NULL AND clar.in_reply_to IS NULL', ]; foreach ($wheres as $type => $where) { $clarifications = (clone $queryBuilder) diff --git a/webapp/src/DataFixtures/Test/ClarificationFixture.php b/webapp/src/DataFixtures/Test/ClarificationFixture.php new file mode 100644 index 0000000000..cb875e6549 --- /dev/null +++ b/webapp/src/DataFixtures/Test/ClarificationFixture.php @@ -0,0 +1,57 @@ +getRepository(Contest::class)->findOneBy(['shortname' => 'demo']); + /** @var Team $team */ + $team = $manager->getRepository(Team::class)->findOneBy(['name' => 'Example teamname']); + /** @var Problem $problem */ + $problem = $manager->getRepository(Problem::class)->findOneBy(['externalid' => 'hello']); + + $unhandledClarification = new Clarification(); + $unhandledClarification + ->setContest($contest) + ->setSubmittime(1518385738.901348000) + ->setSender($team) + ->setProblem($problem) + ->setBody('Is it necessary to read the problem statement carefully?') + ->setAnswered(false); + + $juryGeneral = new Clarification(); + $juryGeneral + ->setContest($contest) + ->setSubmittime(1518386000) + ->setJuryMember('admin') + ->setBody("Lunch is served") + ->setAnswered(true); + + $juryGeneralToTeam = new Clarification(); + $juryGeneralToTeam + ->setContest($contest) + ->setSubmittime(15183856633.689197000) + ->setRecipient($team) + ->setJuryMember('admin') + ->setProblem($problem) + ->setBody("There was a mistake in judging this problem. Please try again") + ->setAnswered(true); + + $manager->persist($unhandledClarification); + $manager->persist($juryGeneral); + $manager->persist($juryGeneralToTeam); + $manager->flush(); + } +} diff --git a/webapp/tests/Unit/Controller/API/ClarificationControllerTest.php b/webapp/tests/Unit/Controller/API/ClarificationControllerTest.php index 03f2a4fa66..c232f7dfac 100644 --- a/webapp/tests/Unit/Controller/API/ClarificationControllerTest.php +++ b/webapp/tests/Unit/Controller/API/ClarificationControllerTest.php @@ -2,6 +2,7 @@ namespace App\Tests\Unit\Controller\API; +use App\DataFixtures\Test\ClarificationFixture; use App\DataFixtures\Test\RemoveTeamFromDemoUserFixture; use App\Entity\Clarification; use App\Entity\Problem; @@ -14,7 +15,8 @@ class ClarificationControllerTest extends BaseTest protected $apiUser = 'admin'; - // These come from the ExampleData\ClarificationFixture class + protected static $fixtures = [ ClarificationFixture::class ]; + protected $expectedObjects = [ '1' => [ "problem_id" => "1", diff --git a/webapp/tests/Unit/Controller/Jury/ClarificationControllerTest.php b/webapp/tests/Unit/Controller/Jury/ClarificationControllerTest.php index f052920ccc..893e808b20 100644 --- a/webapp/tests/Unit/Controller/Jury/ClarificationControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/ClarificationControllerTest.php @@ -4,6 +4,8 @@ use App\Tests\Unit\BaseTest; +use App\DataFixtures\Test\ClarificationFixture; + class ClarificationControllerTest extends BaseTest { protected $roles = ['jury']; @@ -27,6 +29,35 @@ public function testClarificationRequestIndex() : void self::assertSelectorExists('html:contains("21:47")'); } + /** + * Test that unanswered and answered clarifications are under the right header + */ + public function testClarificationRequestIndexNewAndOldUnderRightHeader() : void + { + $this->loadFixture(ClarificationFixture::class); + + $this->verifyPageResponse('GET', '/jury/clarifications', 200); + $crawler = $this->getCurrentCrawler(); + + self::assertSelectorTextContains('h3#newrequests ~ div.table-wrapper', 'Is it necessary to'); + self::assertSelectorTextContains('h3#oldrequests ~ div.table-wrapper', 'Can you tell me how'); + } + /** + * Test that general clarification is under general clarifications header + */ + public function testClarificationRequestIndexHasGeneralClarifications() : void + { + $this->loadFixture(ClarificationFixture::class); + + $this->verifyPageResponse('GET', '/jury/clarifications', 200); + $crawler = $this->getCurrentCrawler(); + + // general clarification to all + self::assertSelectorTextContains('h3#clarifications ~ div.table-wrapper', 'Lunch is served'); + // jury initiated message to specific team + self::assertSelectorTextContains('h3#clarifications ~ div.table-wrapper', 'There was a mistake'); + } + /** * Test that the jury can view a clarification */ From eea2632e3def397b6d2df1f695870e519514f081 Mon Sep 17 00:00:00 2001 From: Thijs Kinkhorst Date: Fri, 5 Nov 2021 20:54:45 +0000 Subject: [PATCH 12/13] Ensure $team not null in rejudging test --- webapp/src/DataFixtures/Test/RejudgingStatesFixture.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/DataFixtures/Test/RejudgingStatesFixture.php b/webapp/src/DataFixtures/Test/RejudgingStatesFixture.php index d177b4500a..cd6653daad 100644 --- a/webapp/src/DataFixtures/Test/RejudgingStatesFixture.php +++ b/webapp/src/DataFixtures/Test/RejudgingStatesFixture.php @@ -51,7 +51,7 @@ public function load(ObjectManager $manager): void /** @var Contest $contest */ $contest = $manager->getRepository(Contest::class)->findOneBy(['shortname' => $contestName]); /** @var Team $team */ - $team = $manager->getRepository(Team::class)->findOneBy(['name' => 'demo']); + $team = $manager->getRepository(Team::class)->findOneBy(['name' => 'Example teamname']); /** @var Language $language */ $language = $manager->getRepository(Language::class)->find('java'); // A rejudging has both judgings todo and finished From de0936bf6f9a4573bb77baaabd05b179ea7affe5 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Tue, 2 Nov 2021 18:25:59 +0100 Subject: [PATCH 13/13] Disable processing medals by default. Fixes #1309. Also rename awards to medals in the UI, since it better represents the content. --- webapp/public/style_domjudge.css | 6 +- .../src/Controller/API/AwardsController.php | 8 +- .../src/Controller/Jury/ContestController.php | 6 +- .../ExampleData/ContestFixture.php | 4 +- webapp/src/Entity/Contest.php | 130 ++++++++---------- webapp/src/Entity/TeamCategory.php | 26 ++-- webapp/src/Form/Type/ContestType.php | 32 ++--- .../src/Migrations/Version20210611141202.php | 14 +- webapp/src/Service/ImportExportService.php | 4 +- webapp/templates/jury/contest.html.twig | 18 +-- .../jury/partials/contest_form.html.twig | 24 ++-- .../partials/scoreboard_table.html.twig | 42 +++--- .../Controller/API/AwardsControllerTest.php | 1 - .../Controller/Jury/ContestControllerTest.php | 32 ++--- 14 files changed, 165 insertions(+), 182 deletions(-) diff --git a/webapp/public/style_domjudge.css b/webapp/public/style_domjudge.css index 64dfdaceb9..2d81c63cfc 100644 --- a/webapp/public/style_domjudge.css +++ b/webapp/public/style_domjudge.css @@ -261,9 +261,9 @@ img.affiliation-logo { .score_pending { background: #6666FF; } .score_incorrect { background: #e87272; } -.gold-award { background-color: #EEC710 } -.silver-award { background-color: #AAA } -.bronze-award { background-color: #C08E55 } +.gold-medal { background-color: #EEC710 } +.silver-medal { background-color: #AAA } +.bronze-medal { background-color: #C08E55 } #scoresolv,#scoretotal { width: 2.5em; } .scorenc,.scorett,.scorepl { text-align: center; width: 2ex; } diff --git a/webapp/src/Controller/API/AwardsController.php b/webapp/src/Controller/API/AwardsController.php index 0872aa351a..35491b1dc7 100644 --- a/webapp/src/Controller/API/AwardsController.php +++ b/webapp/src/Controller/API/AwardsController.php @@ -155,12 +155,12 @@ protected function getAwardsData(Request $request, string $requestedType = null) if ($rank === 1) { $overall_winners[] = $teamid; } - if ($contest->getProcessAwards() && $contest->getAwardsCategories()->contains($teamScore->team->getCategory())) { - if ($rank <= $contest->getGoldAwards()) { + if ($contest->getMedalsEnabled() && $contest->getMedalCategories()->contains($teamScore->team->getCategory())) { + if ($rank <= $contest->getGoldMedals()) { $medal_winners['gold'][] = $teamid; - } elseif ($rank <= $contest->getGoldAwards() + $contest->getSilverAwards()) { + } elseif ($rank <= $contest->getGoldMedals() + $contest->getSilverMedals()) { $medal_winners['silver'][] = $teamid; - } elseif ($rank <= $contest->getGoldAwards() + $contest->getSilverAwards() + $contest->getBronzeAwards() + $additionalBronzeMedals) { + } elseif ($rank <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $additionalBronzeMedals) { $medal_winners['bronze'][] = $teamid; } } diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index d088c51803..2118aa545c 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -253,7 +253,7 @@ public function indexAction(Request $request, KernelInterface $kernel): Response $table_fields = array_merge($table_fields, [ 'process_balloons' => ['title' => 'process balloons?', 'sort' => true], - 'process_awards' => ['title' => 'process awards?', 'sort' => true], + 'medals_enabled' => ['title' => 'medals?', 'sort' => true], 'public' => ['title' => 'public?', 'sort' => true], 'num_teams' => ['title' => '# teams', 'sort' => true], 'num_problems' => ['title' => '# problems', 'sort' => true], @@ -299,8 +299,8 @@ public function indexAction(Request $request, KernelInterface $kernel): Response $contestdata['process_balloons'] = [ 'value' => $contest->getProcessBalloons() ? 'yes' : 'no' ]; - $contestdata['process_awards'] = [ - 'value' => $contest->getProcessAwards() ? 'yes' : 'no' + $contestdata['medals_enabled'] = [ + 'value' => $contest->getMedalsEnabled() ? 'yes' : 'no' ]; $contestdata['public'] = ['value' => $contest->getPublic() ? 'yes' : 'no']; if ($contest->isOpenToAllTeams()) { diff --git a/webapp/src/DataFixtures/ExampleData/ContestFixture.php b/webapp/src/DataFixtures/ExampleData/ContestFixture.php index 43403023d4..ddd1bbc5cc 100644 --- a/webapp/src/DataFixtures/ExampleData/ContestFixture.php +++ b/webapp/src/DataFixtures/ExampleData/ContestFixture.php @@ -35,7 +35,7 @@ public function load(ObjectManager $manager) ->setPublic(false) ->setOpenToAllTeams(false) ->addTeam($this->getReference(TeamFixture::TEAM_REFERENCE)) - ->addAwardsCategory($this->getReference(TeamCategoryFixture::PARTICIPANTS_REFERENCE)); + ->addMedalCategory($this->getReference(TeamCategoryFixture::PARTICIPANTS_REFERENCE)); $demoContest = new Contest(); $demoContest @@ -73,7 +73,7 @@ public function load(ObjectManager $manager) date('Y') + 2 ) ) - ->addAwardsCategory($this->getReference(TeamCategoryFixture::PARTICIPANTS_REFERENCE)); + ->addMedalCategory($this->getReference(TeamCategoryFixture::PARTICIPANTS_REFERENCE)); $manager->persist($demoPracticeContest); $manager->persist($demoContest); diff --git a/webapp/src/Entity/Contest.php b/webapp/src/Entity/Contest.php index 02b1d547b6..7d90f01a86 100644 --- a/webapp/src/Entity/Contest.php +++ b/webapp/src/Entity/Contest.php @@ -172,49 +172,49 @@ class Contest extends BaseApiEntity /** * @var boolean|null - * @ORM\Column(type="boolean", name="process_awards", - * options={"comment"="Are there awards for this contest?","default"=1}, + * @ORM\Column(type="boolean", name="medals_enabled", + * options={"default"=0}, * nullable=false) * @Serializer\Exclude() */ - private $processAwards = true; + private $medalsEnabled = false; /** - * @ORM\ManyToMany(targetEntity="App\Entity\TeamCategory", inversedBy="contests_for_awards") - * @ORM\JoinTable(name="contestteamcategoryforawards", + * @ORM\ManyToMany(targetEntity="App\Entity\TeamCategory", inversedBy="contests_for_medals") + * @ORM\JoinTable(name="contestteamcategoryformedals", * joinColumns={@ORM\JoinColumn(name="cid", referencedColumnName="cid", onDelete="CASCADE")}, * inverseJoinColumns={@ORM\JoinColumn(name="categoryid", referencedColumnName="categoryid", onDelete="CASCADE")} * ) * @Serializer\Exclude() */ - private $awards_categories; + private $medal_categories; /** * @var int|null - * @ORM\Column(type="smallint", length=3, name="gold_awards", + * @ORM\Column(type="smallint", length=3, name="gold_medals", * options={"comment"="Number of gold medals","unsigned"="true","default"=4}, * nullable=false) * @Serializer\Exclude() */ - private $goldAwards = 4; + private $goldMedals = 4; /** * @var int|null - * @ORM\Column(type="smallint", length=3, name="silver_awards", + * @ORM\Column(type="smallint", length=3, name="silver_medals", * options={"comment"="Number of silver medals","unsigned"="true","default"=4}, * nullable=false) * @Serializer\Exclude() */ - private $silverAwards = 4; + private $silverMedals = 4; /** * @var int|null - * @ORM\Column(type="smallint", length=3, name="bronze_awards", + * @ORM\Column(type="smallint", length=3, name="bronze_medals", * options={"comment"="Number of bronze medals","unsigned"="true","default"=4}, * nullable=false) * @Serializer\Exclude() */ - private $bronzeAwards = 4; + private $bronzeMedals = 4; /** * @var double @@ -387,7 +387,7 @@ public function __construct() $this->submissions = new ArrayCollection(); $this->internal_errors = new ArrayCollection(); $this->team_categories = new ArrayCollection(); - $this->awards_categories = new ArrayCollection(); + $this->medal_categories = new ArrayCollection(); } public function getCid(): int @@ -682,121 +682,121 @@ public function getProcessBalloons(): bool } /** - * Set processAwards + * Set medalsEnabled * - * @param boolean $processAwards + * @param boolean $medalsEnabled * * @return Contest */ - public function setProcessAwards(bool $processAwards): Contest + public function setMedalsEnabled(bool $medalsEnabled): Contest { - $this->processAwards = $processAwards; + $this->medalsEnabled = $medalsEnabled; return $this; } /** - * Get processAwards + * Get medalsEnabled * * @return boolean */ - public function getProcessAwards(): bool + public function getMedalsEnabled(): bool { - return $this->processAwards; + return $this->medalsEnabled; } /** * @return Collection|TeamCategory[] */ - public function getAwardsCategories(): Collection + public function getMedalCategories(): Collection { - return $this->awards_categories; + return $this->medal_categories; } - public function addAwardsCategory(TeamCategory $awardsCategory): Contest + public function addMedalCategory(TeamCategory $medalCategory): Contest { - if (!$this->awards_categories->contains($awardsCategory)) { - $this->awards_categories[] = $awardsCategory; + if (!$this->medal_categories->contains($medalCategory)) { + $this->medal_categories[] = $medalCategory; } return $this; } - public function removeAwardsCategories(TeamCategory $awardsCategory): Contest + public function removeMedalCategories(TeamCategory $medalCategory): Contest { - if ($this->awards_categories->contains($awardsCategory)) { - $this->awards_categories->removeElement($awardsCategory); + if ($this->medal_categories->contains($medalCategory)) { + $this->medal_categories->removeElement($medalCategory); } return $this; } /** - * Set goldAwards + * Set goldMedals * - * @param int|null $goldAwards + * @param int|null $goldMedals * * @return Contest */ - public function setGoldAwards(?int $goldAwards): Contest + public function setGoldMedals(?int $goldMedals): Contest { - $this->goldAwards = $goldAwards; + $this->goldMedals = $goldMedals; return $this; } /** - * Get goldAwards + * Get goldMedals * * @return int|null */ - public function getGoldAwards(): ?int + public function getGoldMedals(): ?int { - return $this->goldAwards; + return $this->goldMedals; } /** - * Set silverAwards + * Set silverMedals * - * @param int|null $silverAwards + * @param int|null $silverMedals * * @return Contest */ - public function setSilverAwards(?int $silverAwards): Contest + public function setSilverMedals(?int $silverMedals): Contest { - $this->silverAwards = $silverAwards; + $this->silverMedals = $silverMedals; return $this; } /** - * Get silverAwards + * Get silverMedals * * @return int|null */ - public function getSilverAwards(): ?int + public function getSilverMedals(): ?int { - return $this->silverAwards; + return $this->silverMedals; } /** - * Set bronzeAwards + * Set bronzeMedals * - * @param int|null $bronzeAwards + * @param int|null $bronzeMedals * * @return Contest */ - public function setBronzeAwards(?int $bronzeAwards): Contest + public function setBronzeMedals(?int $bronzeMedals): Contest { - $this->bronzeAwards = $bronzeAwards; + $this->bronzeMedals = $bronzeMedals; return $this; } /** - * Get bronzeAwards + * Get bronzeMedals * * @return int|null */ - public function getBronzeAwards(): int + public function getBronzeMedals(): int { - return $this->bronzeAwards; + return $this->bronzeMedals; } public function setPublic(bool $public): Contest @@ -1211,29 +1211,19 @@ public function validate(ExecutionContextInterface $context) } } - if ($this->processAwards) { - if ($this->goldAwards === null) { - $context - ->buildViolation('This field is required when \'Process awards\' is set.') - ->atPath('goldAwards') - ->addViolation(); - } - if ($this->silverAwards === null) { - $context - ->buildViolation('This field is required when \'Process awards\' is set.') - ->atPath('silverAwards') - ->addViolation(); - } - if ($this->bronzeAwards === null) { - $context - ->buildViolation('This field is required when \'Process awards\' is set.') - ->atPath('bronzeAwards') - ->addViolation(); + if ($this->medalsEnabled) { + foreach (['goldMedals', 'silverMedals', 'bronzeMedals'] as $field) { + if ($this->$field === null) { + $context + ->buildViolation('This field is required when \'Enable medals\' is set.') + ->atPath($field) + ->addViolation(); + } } - if ($this->awards_categories === null || $this->awards_categories->isEmpty()) { + if ($this->medal_categories === null || $this->medal_categories->isEmpty()) { $context - ->buildViolation('This field is required when \'Process awards\' is set.') - ->atPath('awardsCategories') + ->buildViolation('This field is required when \'Process medals\' is set.') + ->atPath('medalCategories') ->addViolation(); } } diff --git a/webapp/src/Entity/TeamCategory.php b/webapp/src/Entity/TeamCategory.php index 69225ce705..d243c138d6 100644 --- a/webapp/src/Entity/TeamCategory.php +++ b/webapp/src/Entity/TeamCategory.php @@ -100,16 +100,16 @@ class TeamCategory extends BaseApiEntity private $contests; /** - * @ORM\ManyToMany(targetEntity="Contest", mappedBy="awards_categories") + * @ORM\ManyToMany(targetEntity="Contest", mappedBy="medal_categories") * @Serializer\Exclude() */ - private $contests_for_awards; + private $contests_for_medals; public function __construct() { $this->teams = new ArrayCollection(); $this->contests = new ArrayCollection(); - $this->contests_for_awards = new ArrayCollection(); + $this->contests_for_medals = new ArrayCollection(); } public function __toString(): string @@ -235,26 +235,26 @@ public function removeContest(Contest $contest): self /** * @return Collection|Contest[] */ - public function getContestsForAwards(): Collection + public function getContestsForMedals(): Collection { - return $this->contests_for_awards; + return $this->contests_for_medals; } - public function addContestForAwards(Contest $contest): self + public function addContestForMedals(Contest $contest): self { - if (!$this->contests_for_awards->contains($contest)) { - $this->contests_for_awards[] = $contest; - $contest->addAwardsCategory($this); + if (!$this->contests_for_medals->contains($contest)) { + $this->contests_for_medals[] = $contest; + $contest->addMedalCategory($this); } return $this; } - public function removeContestForAwards(Contest $contest): self + public function removeContestForMedals(Contest $contest): self { - if ($this->contests_for_awards->contains($contest)) { - $this->contests_for_awards->removeElement($contest); - $contest->removeAwardsCategories($this); + if ($this->contests_for_medals->contains($contest)) { + $this->contests_for_medals->removeElement($contest); + $contest->removeMedalCategories($this); } return $this; diff --git a/webapp/src/Form/Type/ContestType.php b/webapp/src/Form/Type/ContestType.php index 82684855e0..125a5a69d1 100644 --- a/webapp/src/Form/Type/ContestType.php +++ b/webapp/src/Form/Type/ContestType.php @@ -76,35 +76,33 @@ public function buildForm(FormBuilderInterface $builder, array $options) ], 'help' => 'Disable this to stop recording balloons. Usually you can just leave this enabled.', ]); - $builder->add('processAwards', ChoiceType::class, [ + $builder->add('medalsEnabled', ChoiceType::class, [ 'expanded' => true, 'choices' => [ 'Yes' => true, 'No' => false, ], - 'help' => 'Whether to process medal (gold, silver, bronze) awards for this contest.', + 'help' => 'Whether to enable medals (gold, silver, bronze) for this contest.', ]); - $builder->add('awardsCategories', EntityType::class, [ + $builder->add('medalCategories', EntityType::class, [ 'required' => false, 'class' => TeamCategory::class, 'multiple' => true, 'choice_label' => function (TeamCategory $category) { return $category->getName(); }, - 'help' => 'List of team categories that will receive awards for this contest.', - ]); - $builder->add('goldAwards', IntegerType::class, [ - 'required' => false, - 'help' => 'The number of gold awards for this contest.', - ]); - $builder->add('silverAwards', IntegerType::class, [ - 'required' => false, - 'help' => 'The number of silver awards for this contest.', - ]); - $builder->add('bronzeAwards', IntegerType::class, [ - 'required' => false, - 'help' => 'The number of bronze awards for this contest. Note that when finalizing a contest, the "Additional Bronze Medals" will be added to this.', - ]); + 'help' => 'List of team categories that will receive medals for this contest.', + ]); + foreach (['gold', 'silver', 'bronze'] as $medalType) { + $help = "The number of $medalType medals for this contest."; + if ($medalType === 'bronze') { + $help .= ' Note that when finalizing a contest, the "Additional Bronze Medals" will be added to this.'; + } + $builder->add($medalType . 'Medals', IntegerType::class, [ + 'required' => false, + 'help' => $help, + ]); + } $builder->add('public', ChoiceType::class, [ 'expanded' => true, 'label' => 'Enable public scoreboard', diff --git a/webapp/src/Migrations/Version20210611141202.php b/webapp/src/Migrations/Version20210611141202.php index 3573cede5c..dd392bfcb3 100644 --- a/webapp/src/Migrations/Version20210611141202.php +++ b/webapp/src/Migrations/Version20210611141202.php @@ -14,7 +14,7 @@ final class Version20210611141202 extends AbstractMigration { public function getDescription() : string { - return ''; + return 'Create medal fields'; } public function up(Schema $schema) : void @@ -22,10 +22,10 @@ public function up(Schema $schema) : void // this up() migration is auto-generated, please modify it to your needs $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); - $this->addSql('CREATE TABLE contestteamcategoryforawards (cid INT UNSIGNED NOT NULL COMMENT \'Contest ID\', categoryid INT UNSIGNED NOT NULL COMMENT \'Team category ID\', INDEX IDX_40B1F5544B30D9C4 (cid), INDEX IDX_40B1F5549B32FD3 (categoryid), PRIMARY KEY(cid, categoryid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('ALTER TABLE contestteamcategoryforawards ADD CONSTRAINT FK_40B1F5544B30D9C4 FOREIGN KEY (cid) REFERENCES contest (cid) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE contestteamcategoryforawards ADD CONSTRAINT FK_40B1F5549B32FD3 FOREIGN KEY (categoryid) REFERENCES team_category (categoryid) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE contest ADD process_awards TINYINT(1) DEFAULT \'1\' NOT NULL COMMENT \'Are there awards for this contest?\', ADD gold_awards SMALLINT UNSIGNED DEFAULT 4 NOT NULL COMMENT \'Number of gold medals\', ADD silver_awards SMALLINT UNSIGNED DEFAULT 4 NOT NULL COMMENT \'Number of silver medals\', ADD bronze_awards SMALLINT UNSIGNED DEFAULT 4 NOT NULL COMMENT \'Number of bronze medals\''); + $this->addSql('CREATE TABLE contestteamcategoryformedals (cid INT UNSIGNED NOT NULL COMMENT \'Contest ID\', categoryid INT UNSIGNED NOT NULL COMMENT \'Team category ID\', INDEX IDX_40B1F5544B30D9C4 (cid), INDEX IDX_40B1F5549B32FD3 (categoryid), PRIMARY KEY(cid, categoryid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE contestteamcategoryformedals ADD CONSTRAINT FK_40B1F5544B30D9C4 FOREIGN KEY (cid) REFERENCES contest (cid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE contestteamcategoryformedals ADD CONSTRAINT FK_40B1F5549B32FD3 FOREIGN KEY (categoryid) REFERENCES team_category (categoryid) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE contest ADD medals_enabled TINYINT(1) DEFAULT \'0\' NOT NULL, ADD gold_medals SMALLINT UNSIGNED DEFAULT 4 NOT NULL COMMENT \'Number of gold medals\', ADD silver_medals SMALLINT UNSIGNED DEFAULT 4 NOT NULL COMMENT \'Number of silver medals\', ADD bronze_medals SMALLINT UNSIGNED DEFAULT 4 NOT NULL COMMENT \'Number of bronze medals\''); } public function down(Schema $schema) : void @@ -33,7 +33,7 @@ public function down(Schema $schema) : void // this down() migration is auto-generated, please modify it to your needs $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); - $this->addSql('DROP TABLE contestteamcategoryforawards'); - $this->addSql('ALTER TABLE contest DROP process_awards, DROP gold_awards, DROP silver_awards, DROP bronze_awards'); + $this->addSql('DROP TABLE contestteamcategoryformedals'); + $this->addSql('ALTER TABLE contest DROP medals_enabled, DROP gold_medals, DROP silver_medals, DROP bronze_medals'); } } diff --git a/webapp/src/Service/ImportExportService.php b/webapp/src/Service/ImportExportService.php index 29a47ea15a..46408d0fc7 100644 --- a/webapp/src/Service/ImportExportService.php +++ b/webapp/src/Service/ImportExportService.php @@ -200,10 +200,10 @@ public function importContestData($data, ?string &$message = null, string &$cid $visibleCategories = $this->em->getRepository(TeamCategory::class)->findBy(['visible' => true]); if (empty($visibleCategories)) { - $contest->setProcessAwards(false); + $contest->setMedalsEnabled(false); } else { foreach ($visibleCategories as $visibleCategory) { - $contest->addAwardsCategory($visibleCategory); + $contest->addMedalCategory($visibleCategory); } } diff --git a/webapp/templates/jury/contest.html.twig b/webapp/templates/jury/contest.html.twig index 4b814abf0c..8220a389c4 100644 --- a/webapp/templates/jury/contest.html.twig +++ b/webapp/templates/jury/contest.html.twig @@ -107,31 +107,31 @@ {{ contest.processBalloons | printYesNo }} - Process awards - {{ contest.processAwards | printYesNo }} + Process medals + {{ contest.medalsEnabled | printYesNo }} - Awards + Medals - {% if contest.processAwards %} + {% if contest.medalsEnabled %}