diff --git a/.github/jobs/baseinstall.sh b/.github/jobs/baseinstall.sh index 3fe155a0cd..55fdf0ccf2 100755 --- a/.github/jobs/baseinstall.sh +++ b/.github/jobs/baseinstall.sh @@ -1,54 +1,20 @@ #!/bin/sh -# Functions to annotate the Github actions logs -alias trace_on='set -x' -alias trace_off='{ set +x; } 2>/dev/null' - -section_start_internal () { - echo "::group::$1" - trace_on -} - -section_end_internal () { - echo "::endgroup::" - trace_on -} - -alias section_start='trace_off ; section_start_internal ' -alias section_end='trace_off ; section_end_internal ' +. .github/jobs/ci_settings.sh export version="$1" +db=${2:-install} set -eux -section_start "Update packages" -sudo apt update -section_end - -section_start "Install needed packages" -sudo apt install -y acl zip unzip nginx php php-fpm php-gd \ - php-cli php-intl php-mbstring php-mysql php-curl php-json \ - php-xml php-zip ntp make sudo debootstrap \ - libcgroup-dev lsof php-cli php-curl php-json php-xml \ - php-zip procps gcc g++ default-jre-headless \ - default-jdk-headless ghc fp-compiler autoconf automake bats \ - python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig \ - python3-yaml latexmk curl -section_end - PHPVERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION."\n";') export PHPVERSION -section_start "Install composer" -php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" -HASH="$(wget -q -O - https://composer.github.io/installer.sig)" -php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" -sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer -section_end - section_start "Run composer" export APP_ENV="dev" -composer install --no-scripts +cd webapp +composer install --no-scripts |tee "$ARTIFACTS"/composer_out.txt +cd .. section_end section_start "Set simple admin password" @@ -58,59 +24,98 @@ section_end section_start "Install domserver" make configure -./configure --with-baseurl='https://localhost/domjudge/' --enable-doc-build=no --prefix="/opt/domjudge" +./configure \ + --with-baseurl='https://localhost/domjudge/' \ + --with-domjudge-user=root \ + --enable-doc-build=no \ + --enable-judgehost-build=no | tee "$ARTIFACTS"/configure.txt make domserver -sudo make install-domserver +make install-domserver section_end -section_start "Explicit start mysql + install DB" -sudo /etc/init.d/mysql start +section_start "SQL settings" +cat > ~/.my.cnf < /opt/domjudge/domserver/etc/dbpasswords.secret +mysql_user "SELECT CURRENT_USER();" +mysql_user "SELECT USER();" +section_end +section_start "Install DOMjudge database" /opt/domjudge/domserver/bin/dj_setup_database -uroot -proot bare-install section_end +section_start "Show PHP config" +php -v | tee -a "$ARTIFACTS"/php.txt +php -m | tee -a "$ARTIFACTS"/php.txt +section_end + +section_start "Show general config" +printenv | tee -a "$ARTIFACTS"/environment.txt +cp /etc/os-release "$ARTIFACTS"/os-release.txt +cp /proc/cmdline "$ARTIFACTS"/cmdline.txt +section_end + section_start "Setup webserver" -sudo cp /opt/domjudge/domserver/etc/domjudge-fpm.conf /etc/php/$PHPVERSION/fpm/pool.d/domjudge.conf +cp /opt/domjudge/domserver/etc/domjudge-fpm.conf /etc/php/"$PHPVERSION"/fpm/pool.d/domjudge.conf -sudo rm -f /etc/nginx/sites-enabled/* -sudo cp /opt/domjudge/domserver/etc/nginx-conf /etc/nginx/sites-enabled/domjudge +rm -f /etc/nginx/sites-enabled/* +cp /opt/domjudge/domserver/etc/nginx-conf /etc/nginx/sites-enabled/domjudge openssl req -nodes -new -x509 -keyout /tmp/server.key -out /tmp/server.crt -subj "/C=NL/ST=Noord-Holland/L=Amsterdam/O=TestingForPR/CN=localhost" -sudo cp /tmp/server.crt /usr/local/share/ca-certificates/ -sudo update-ca-certificates +cp /tmp/server.crt /usr/local/share/ca-certificates/ +update-ca-certificates # shellcheck disable=SC2002 -cat "$(pwd)/.github/jobs/data/nginx_extra" | sudo tee -a /etc/nginx/sites-enabled/domjudge -sudo nginx -t +cat "$(pwd)/.github/jobs/data/nginx_extra" | tee -a /etc/nginx/sites-enabled/domjudge +nginx -t section_end section_start "Show webserver is up" for service in nginx php${PHPVERSION}-fpm; do - sudo systemctl restart $service - sudo systemctl status $service + service "$service" restart + service "$service" status done section_end -section_start "Install the example data" -/opt/domjudge/domserver/bin/dj_setup_database -uroot -proot install-examples -section_end +if [ "${db}" = "install" ]; then + section_start "Install the example data" + /opt/domjudge/domserver/bin/dj_setup_database -uroot -proot install-examples | tee -a "$ARTIFACTS/mysql.txt" + section_end +fi section_start "Setup user" # We're using the admin user in all possible roles -echo "DELETE FROM userrole WHERE userid=1;" | mysql -uroot -proot domjudge +mysql_root "DELETE FROM userrole WHERE userid=1;" domjudge if [ "$version" = "team" ]; then # Add team to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 3);" | mysql -uroot -proot domjudge - echo "UPDATE user SET teamid = 1 WHERE userid = 1;" | mysql -uroot -proot domjudge + mysql_root "INSERT INTO userrole (userid, roleid) VALUES (1, 3);" domjudge + mysql_root "UPDATE user SET teamid = 1 WHERE userid = 1;" domjudge elif [ "$version" = "jury" ]; then # Add jury to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 2);" | mysql -uroot -proot domjudge + mysql_root "INSERT INTO userrole (userid, roleid) VALUES (1, 2);" domjudge elif [ "$version" = "balloon" ]; then # Add balloon to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 4);" | mysql -uroot -proot domjudge + mysql_root "INSERT INTO userrole (userid, roleid) VALUES (1, 4);" domjudge elif [ "$version" = "admin" ]; then # Add admin to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 1);" | mysql -uroot -proot domjudge + mysql_root "INSERT INTO userrole (userid, roleid) VALUES (1, 1);" domjudge fi section_end diff --git a/.github/jobs/ci_settings.sh b/.github/jobs/ci_settings.sh new file mode 100644 index 0000000000..350b07cf09 --- /dev/null +++ b/.github/jobs/ci_settings.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +# Store artifacts/logs +export ARTIFACTS="/tmp/artifacts" +mkdir -p "$ARTIFACTS" + +# Functions to annotate the Github actions logs +trace_on () { + set -x +} +trace_off () { + { + set +x + } 2>/dev/null +} + +section_start_internal () { + echo "::group::$1" + trace_on +} + +section_end_internal () { + echo "::endgroup::" + trace_on +} + +mysql_root () { + # shellcheck disable=SC2086 + echo "$1" | mysql -uroot -proot ${2:-} | tee -a "$ARTIFACTS"/mysql.txt +} + +mysql_user () { + # shellcheck disable=SC2086 + echo "$1" | mysql -udomjudge -pdomjudge ${2:-} | tee -a "$ARTIFACTS"/mysql.txt +} + +section_start () { + if [ "$#" -ne 1 ]; then + echo "Only 1 argument is needed for GHA, 2 was needed for GitLab." + exit 1 + fi + trace_off + section_start_internal "$1" +} + +section_end () { + trace_off + section_end_internal +} diff --git a/.github/jobs/composer_setup.sh b/.github/jobs/composer_setup.sh new file mode 100755 index 0000000000..5e5213e42b --- /dev/null +++ b/.github/jobs/composer_setup.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -eux + +. .github/jobs/ci_settings.sh + +section_start "Configure PHP" +PHPVERSION=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION."\n";') +export PHPVERSION +echo "$PHPVERSION" | tee -a "$ARTIFACTS"/phpversion.txt +section_end + +section_start "Run composer" +cd webapp +composer install --no-scripts 2>&1 | tee -a "$ARTIFACTS/composer_log.txt" +section_end diff --git a/.github/jobs/configure-checks/all.bats b/.github/jobs/configure-checks/all.bats index 8f82797f8a..207650895f 100755 --- a/.github/jobs/configure-checks/all.bats +++ b/.github/jobs/configure-checks/all.bats @@ -37,7 +37,7 @@ setup() { if [ "$distro_id" = "ID=fedora" ]; then repo-install httpd fi - repo-install gcc g++ libcgroup-dev + repo-install gcc g++ libcgroup-dev composer } run_configure () { @@ -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 () { @@ -231,7 +231,6 @@ compile_assertions_finished () { assert_line " - bin..............: /opt/domjudge/domserver/bin" assert_line " - etc..............: /opt/domjudge/domserver/etc" assert_line " - lib..............: /opt/domjudge/domserver/lib" - assert_line " - libvendor........: /opt/domjudge/domserver/lib/vendor" assert_line " - log..............: /opt/domjudge/domserver/log" assert_line " - run..............: /opt/domjudge/domserver/run" assert_line " - sql..............: /opt/domjudge/domserver/sql" @@ -248,7 +247,6 @@ compile_assertions_finished () { assert_line " - tmp..............: /opt/domjudge/judgehost/tmp" assert_line " - judge............: /opt/domjudge/judgehost/judgings" assert_line " - chroot...........: /chroot/domjudge" - assert_line " - cgroup...........: /sys/fs/cgroup" } @test "Prefix configured" { @@ -258,7 +256,6 @@ compile_assertions_finished () { refute_line " * documentation.......: /opt/domjudge/doc" refute_line " * domserver...........: /opt/domjudge/domserver" refute_line " - bin..............: /opt/domjudge/domserver/bin" - refute_line " - libvendor........: /opt/domjudge/domserver/lib/vendor" refute_line " - tmp..............: /opt/domjudge/domserver/tmp" refute_line " - example_problems.: /opt/domjudge/domserver/example_problems" refute_line " * judgehost...........: /opt/domjudge/judgehost" @@ -270,7 +267,6 @@ compile_assertions_finished () { assert_line " * prefix..............: /tmp" assert_line " * documentation.......: /tmp/doc" assert_line " * domserver...........: /tmp/domserver" - assert_line " - libvendor........: /tmp/domserver/lib/vendor" assert_line " * judgehost...........: /tmp/judgehost" assert_line " - judge............: /tmp/judgehost/judgings" } @@ -292,7 +288,6 @@ compile_assertions_finished () { assert_line " - bin..............: /usr/local/bin" assert_line " - etc..............: /usr/local/etc/domjudge" assert_line " - lib..............: /usr/local/lib/domjudge" - assert_line " - libvendor........: /usr/local/lib/domjudge/vendor" assert_line " - log..............: /usr/local/var/log/domjudge" assert_line " - run..............: /usr/local/var/run/domjudge" assert_line " - sql..............: /usr/local/share/domjudge/sql" @@ -309,19 +304,17 @@ compile_assertions_finished () { assert_line " - tmp..............: /tmp" assert_line " - judge............: /usr/local/var/lib/domjudge/judgings" assert_line " - chroot...........: /chroot/domjudge" - assert_line " - cgroup...........: /sys/fs/cgroup" } @test "Alternative dirs together with FHS" { setup - run run_configure --enable-fhs --with-domserver_webappdir=/run/webapp --with-domserver_tmpdir=/tmp/domserver --with-judgehost_tmpdir=/srv/tmp --with-judgehost_judgedir=/srv/judgings --with-judgehost_chrootdir=/srv/chroot/domjudge --with-judgehost_cgroupdir=/sys/fs/altcgroup + run run_configure --enable-fhs --with-domserver_webappdir=/run/webapp --with-domserver_tmpdir=/tmp/domserver --with-judgehost_tmpdir=/srv/tmp --with-judgehost_judgedir=/srv/judgings --with-judgehost_chrootdir=/srv/chroot/domjudge assert_line " * prefix..............: /usr/local" assert_line " * documentation.......: /usr/local/share/doc/domjudge" assert_line " * domserver...........: " assert_line " - bin..............: /usr/local/bin" assert_line " - etc..............: /usr/local/etc/domjudge" assert_line " - lib..............: /usr/local/lib/domjudge" - assert_line " - libvendor........: /usr/local/lib/domjudge/vendor" assert_line " - log..............: /usr/local/var/log/domjudge" assert_line " - run..............: /usr/local/var/run/domjudge" assert_line " - sql..............: /usr/local/share/domjudge/sql" @@ -343,13 +336,11 @@ compile_assertions_finished () { assert_line " - judge............: /srv/judgings" refute_line " - chroot...........: /chroot/domjudge" assert_line " - chroot...........: /srv/chroot/domjudge" - refute_line " - cgroup...........: /sys/fs/cgroup" - assert_line " - cgroup...........: /sys/fs/altcgroup" } @test "Alternative dirs together with defaults" { setup - run run_configure "--with-judgehost_tmpdir=/srv/tmp --with-judgehost_judgedir=/srv/judgings --with-judgehost_chrootdir=/srv/chroot --with-judgehost_cgroupdir=/sys/fs/altcgroup --with-domserver_logdir=/log" + run run_configure "--with-judgehost_tmpdir=/srv/tmp --with-judgehost_judgedir=/srv/judgings --with-judgehost_chrootdir=/srv/chroot --with-domserver_logdir=/log" assert_line " * prefix..............: /opt/domjudge" assert_line " * documentation.......: /opt/domjudge/doc" assert_line " * domserver...........: /opt/domjudge/domserver" @@ -362,8 +353,6 @@ compile_assertions_finished () { assert_line " - judge............: /srv/judgings" refute_line " - chroot...........: /chroot/domjudge" assert_line " - chroot...........: /srv/chroot" - refute_line " - cgroup...........: /sys/fs/cgroup" - assert_line " - cgroup...........: /sys/fs/altcgroup" } @test "Default URL not set, docs mention" { diff --git a/.github/jobs/data/codespellignorefiles.txt b/.github/jobs/data/codespellignorefiles.txt index 4130997934..e94815b62b 100644 --- a/.github/jobs/data/codespellignorefiles.txt +++ b/.github/jobs/data/codespellignorefiles.txt @@ -10,7 +10,7 @@ ./config.guess ./gitlab/codespell.yml ./.github/jobs/uploadcodecov.sh -./lib/vendor +./webapp/vendor ./webapp/public/bundles ./webapp/public/js/ace ./webapp/templates/bundles @@ -26,3 +26,4 @@ nv.d3.min* composer* ./doc/logos ./m4 +./webapp/tests/Unit/Fixtures diff --git a/.github/jobs/pa11y_config.json b/.github/jobs/pa11y_config.json new file mode 100644 index 0000000000..af7ef12151 --- /dev/null +++ b/.github/jobs/pa11y_config.json @@ -0,0 +1,9 @@ +{ + "chromeLaunchConfig": { + "args": [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage" + ] + } +} diff --git a/.github/jobs/syntax-check b/.github/jobs/syntax-check index 609846369f..ac9d5a8950 100755 --- a/.github/jobs/syntax-check +++ b/.github/jobs/syntax-check @@ -23,7 +23,7 @@ if [ ! -x /usr/bin/shellcheck ]; then fi find . \( \ - -path ./lib/vendor -prune \ + -path ./webapp/vendor -prune \ -o -path ./webapp/var -prune \ -o -path ./output -prune \ -o -path ./.git -prune \ @@ -41,14 +41,14 @@ while read -r i ; do fi if grep -q "^#\\!.*/bin/sh" "$i" && \ [ "${i##*.}" != "zip" ] && \ - echo "$i" | grep -qvE '(^\./(misc-tools/dj_judgehost_cleanup.in|misc-tools/dj_make_chroot.in|config|autom4te|install-sh|sql/files/defaultdata/hs/run|sql/files/defaultdata/kt/run|lib/vendor/|output|judge/judgedaemon))'; then + echo "$i" | grep -qvE '(^\./(misc-tools/dj_judgehost_cleanup.in|misc-tools/dj_make_chroot.in|config|autom4te|install-sh|sql/files/defaultdata/hs/run|sql/files/defaultdata/kt/run|webapp/vendor/|output|judge/judgedaemon))'; then # shellcheck disable=SC2001 echo "$i" | sed -e 's|^./|check for bashisms: |' checkbashisms "$i" fi if grep -qE "^#\\!.*/bin/(ba)?sh" "$i" && \ [ "${i##*.}" != "zip" ] && \ - echo "$i" | grep -qvE '(^\./(config|autom4te|install-sh|sql/files/defaultdata/hs/run|lib/vendor/|output|judge/judgedaemon))'; then + echo "$i" | grep -qvE '(^\./(config|autom4te|install-sh|sql/files/defaultdata/hs/run|webapp/vendor/|output|judge/judgedaemon))'; then # We ignore the following shellcheck warnings, for more details see: # https://github.com/koalaman/shellcheck/wiki/ # diff --git a/.github/jobs/webstandard.sh b/.github/jobs/webstandard.sh new file mode 100755 index 0000000000..4f2f1db026 --- /dev/null +++ b/.github/jobs/webstandard.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +. .github/jobs/ci_settings.sh + +DIR="$PWD" + +if [ "$#" -ne "2" ]; then + exit 2 +fi + +TEST="$1" +ROLE="$2" + +cd /opt/domjudge/domserver + +section_start "Setup pa11y" +/home/domjudge/node_modules/.bin/pa11y --version +section_end + +section_start "Setup the test user" +ADMINPASS=$(cat etc/initial_admin_password.secret) +export COOKIEJAR +COOKIEJAR=$(mktemp --tmpdir) +export CURLOPTS="--fail -sq -m 30 -b $COOKIEJAR" +if [ "$ROLE" = "public" ]; then + ADMINPASS="failedlogin" +fi + +# Make an initial request which will get us a session id, and grab the csrf token from it +CSRFTOKEN=$(curl $CURLOPTS -c $COOKIEJAR "http://localhost/domjudge/login" 2>/dev/null | sed -n 's/.*_csrf_token.*value="\(.*\)".*/\1/p') +# Make a second request with our session + csrf token to actually log in +# shellcheck disable=SC2086 +curl $CURLOPTS -c "$COOKIEJAR" -F "_csrf_token=$CSRFTOKEN" -F "_username=admin" -F "_password=$ADMINPASS" "http://localhost/domjudge/login" + +# Move back to the default directory +cd "$DIR" + +cp "$COOKIEJAR" cookies.txt +sed -i 's/#HttpOnly_//g' cookies.txt +sed -i 's/\t0\t/\t1999999999\t/g' cookies.txt +section_end + +# Could try different entrypoints +FOUNDERR=0 +URL=public +mkdir "$URL" +cd "$URL" +cp "$DIR"/cookies.txt ./ +section_start "Scrape the site with the rebuild admin user" +set +e +wget \ + --reject-regex logout \ + --recursive \ + --no-clobber \ + --page-requisites \ + --html-extension \ + --convert-links \ + --restrict-file-names=windows \ + --domains localhost \ + --no-parent \ + --load-cookies cookies.txt \ + http://localhost/domjudge/"$URL" +set -e +RET=$? +section_end + +section_start "Archive downloaded site" +cp -r localhost $ARTIFACTS/ +section_end + +section_start "Analyse failures" +#https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html +# Exit code 4 is network error which we can ignore +# Exit code 8 can also be because of HTTP404 or 400 +if [ $RET -ne 4 ] && [ $RET -ne 0 ] && [ $RET -ne 8 ]; then + exit $RET +fi + +EXPECTED_HTTP_CODES="200\|302\|400\|404\|403" +if [ "$ROLE" = "public" ]; then + # It's expected to encounter a 401 for the login page as we supply the wrong password + EXPECTED_HTTP_CODES="$EXPECTED_HTTP_CODES\|401" +fi +set +e +NUM_ERRORS=$(grep -v "HTTP/1.1\" \($EXPECTED_HTTP_CODES\)" /var/log/nginx/domjudge.log | grep -v "robots.txt" -c; if [ "$?" -gt 1 ]; then exit 127; fi) +set -e +echo "$NUM_ERRORS" + +if [ "$NUM_ERRORS" -ne 0 ]; then + grep -v "HTTP/1.1\" \($EXPECTED_HTTP_CODES\)" /var/log/nginx/domjudge.log | grep -v "robots.txt" + exit 1 +fi +section_end + +if [ "$TEST" = "w3cval" ]; then + section_start "Remove files from upstream with problems" + rm -rf localhost/domjudge/doc + rm -rf localhost/domjudge/css/fontawesome-all.min.css* + rm -rf localhost/domjudge/bundles/nelmioapidoc* + rm -f localhost/domjudge/css/bootstrap.min.css* + rm -f localhost/domjudge/css/select2-bootstrap*.css* + rm -f localhost/domjudge/css/dataTables*.css* + rm -f localhost/domjudge/jury/config/check/phpinfo* + section_end + + section_start "Install testsuite" + cd "$DIR" + wget https://github.com/validator/validator/releases/latest/download/vnu.linux.zip + unzip -q vnu.linux.zip + section_end + + FLTR='--filterpattern .*autocomplete.*|.*style.*|.*role=tab.*|.*descendant.*|.*Stray.*|.*attribute.*|.*Forbidden.*|.*stream.*|.*obsolete.*' + for typ in html css svg + do + section_start "Analyse with $typ" + # shellcheck disable=SC2086 + "$DIR"/vnu-runtime-image/bin/vnu --errors-only --exit-zero-always --skip-non-$typ --format json $FLTR "$URL" 2> result.json + # shellcheck disable=SC2086 + NEWFOUNDERRORS=$("$DIR"/vnu-runtime-image/bin/vnu --errors-only --exit-zero-always --skip-non-$typ --format gnu $FLTR "$URL" 2>&1 | wc -l) + FOUNDERR=$((NEWFOUNDERRORS+FOUNDERR)) + python3 -m "json.tool" < result.json > "$ARTIFACTS/w3c$typ$URL.json" + trace_off; python3 gitlab/jsontogitlab.py "$ARTIFACTS/w3c$typ$URL.json"; trace_on + section_end + done +else + section_start "Remove files from upstream with problems" + rm -rf localhost/domjudge/{doc,api} + section_end + + if [ "$TEST" == "axe" ]; then + STAN="-e $TEST" + FLTR="" + else + STAN="-s $TEST" + FLTR0="-E '#DataTables_Table_0 > tbody > tr > td > a','#menuDefault > a','#filter-card > div > div > div > span > span:nth-child(1) > span > ul > li > input',.problem-badge" + FLTR1="'html > body > div > div > div > div > div > div > table > tbody > tr > td > a > span','html > body > div > div > div > div > div > div > form > div > div > div > label'" + FLTR="$FLTR0,$FLTR1" + fi + chown -R domjudge:domjudge "$DIR" + cd "$DIR" + ACCEPTEDERR=5 + # shellcheck disable=SC2044,SC2035 + for file in $(find $URL -name "*.html") + do + section_start "$file" + su domjudge -c "/home/domjudge/node_modules/.bin/pa11y --config .github/jobs/pa11y_config.json $STAN -r json -T $ACCEPTEDERR $FLTR $file" | python3 -m json.tool + ERR=$(su domjudge -c "/home/domjudge/node_modules/.bin/pa11y --config .github/jobs/pa11y_config.json $STAN -r csv -T $ACCEPTEDERR $FLTR $file" | wc -l) + FOUNDERR=$((ERR+FOUNDERR-1)) # Remove header row + section_end + done +fi + +echo "Found: " $FOUNDERR +[ "$FOUNDERR" -eq 0 ] diff --git a/.github/workflows/autoconf-check-different-distro.yml b/.github/workflows/autoconf-check-different-distro.yml index 6c0cefc5b4..8216eb3a26 100644 --- a/.github/workflows/autoconf-check-different-distro.yml +++ b/.github/workflows/autoconf-check-different-distro.yml @@ -21,6 +21,6 @@ jobs: steps: - name: Install git so we get the .github directory run: dnf install -y git - - uses: actions/checkout@v3 + - 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 c9d84f5991..016ec024f7 100644 --- a/.github/workflows/autoconf-check.yml +++ b/.github/workflows/autoconf-check.yml @@ -22,6 +22,8 @@ jobs: include: - os: debian version: stable + - os: debian + version: testing runs-on: ubuntu-latest env: DEBIAN_FRONTEND: noninteractive @@ -31,6 +33,6 @@ jobs: steps: - name: Install git so we get the .github directory run: apt-get update; apt-get install -y git - - uses: actions/checkout@v3 + - 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/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b4c717d39a..645826c186 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,12 +12,14 @@ on: jobs: analyze: - # We can not run with our gitlab container - # CodeQL has missing .so files otherwise + container: + image: domjudge/gitlabci:24.04 + options: --user domjudge name: Analyze runs-on: ubuntu-latest env: COMPILED: "cpp" + USER: "domjudge" permissions: actions: read contents: read @@ -30,33 +32,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - - name: Install required tools - if: ${{ contains(env.COMPILED, matrix.language) }} - run: | - sudo apt update - sudo apt install -y acl zip unzip apache2 php php-fpm php-gd \ - php-cli php-intl php-mbstring php-mysql php-curl php-json \ - php-xml php-zip ntp make sudo debootstrap \ - libcgroup-dev lsof php-cli php-curl php-json php-xml \ - php-zip procps gcc g++ default-jre-headless \ - default-jdk-headless ghc fp-compiler autoconf automake bats \ - python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig \ - python3-yaml latexmk - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - HASH="$(wget -q -O - https://composer.github.io/installer.sig)" - php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" - sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer - - name: Install composer files if: ${{ contains(env.COMPILED, matrix.language) }} run: | + cd webapp composer install --no-scripts - name: Configure Makefile @@ -92,4 +78,4 @@ jobs: run: sudo chown -R ${USER} ./installdir - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 56c2dfac7f..6ac2a9f5c8 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -14,7 +14,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Rewrite Changelog to find new mistakes run: awk '1;/Version 7.2.1 - 6 May 2020/{exit}' ChangeLog > latest_Changelog - name: Get dirs to skip diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index 9b3edc5990..978bd4bb3d 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -13,16 +13,16 @@ jobs: syntax-job: runs-on: ubuntu-latest container: - image: domjudge/gitlabci:2.1 + image: domjudge/gitlabci:24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run the syntax checks run: .github/jobs/syntax.sh detect-dump: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: "Search for leftover dump( statements" run: .github/jobs/detect_dump.sh @@ -31,7 +31,7 @@ jobs: container: image: pipelinecomponents/php-linter:latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Detect PHP linting issues run: > parallel-lint --colors @@ -49,10 +49,10 @@ jobs: image: pipelinecomponents/php-codesniffer:latest strategy: matrix: - PHPVERSION: ["8.1", "8.2"] + PHPVERSION: ["8.1", "8.2", "8.3"] steps: - run: apk add git - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Various fixes to this image run: .github/jobs/fix_pipelinecomponents_image.sh - name: Detect compatibility with supported PHP version diff --git a/.github/workflows/mayhem-api-template.yml b/.github/workflows/mayhem-api-template.yml deleted file mode 100644 index 445d6311c6..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@v3 - - - 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/phpcodesniffer.yml b/.github/workflows/phpcodesniffer.yml index 49f8f76f69..e733a25c3f 100644 --- a/.github/workflows/phpcodesniffer.yml +++ b/.github/workflows/phpcodesniffer.yml @@ -14,7 +14,7 @@ jobs: phpcs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # important! - name: Install PHP_CodeSniffer diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index c550309f34..b5772ef711 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -13,11 +13,16 @@ jobs: phpstan: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install DOMjudge - run: .github/jobs/baseinstall.sh admin + - uses: actions/checkout@v4 + - name: Setup composer dependencies + run: .github/jobs/composer_setup.sh - uses: php-actions/phpstan@v3 with: - configuration: phpstan.dist.neon + configuration: webapp/phpstan.dist.neon path: webapp/src webapp/tests php_extensions: gd intl mysqli pcntl zip + autoload_file: webapp/vendor/autoload.php + - uses: actions/upload-artifact@v4 + if: always() + with: + path: /tmp/artifacts diff --git a/.github/workflows/runpipe.yml b/.github/workflows/runpipe.yml index 551b821757..415510c2c3 100644 --- a/.github/workflows/runpipe.yml +++ b/.github/workflows/runpipe.yml @@ -15,9 +15,9 @@ jobs: runpipe: runs-on: ubuntu-latest container: - image: domjudge/gitlabci:2.1 + image: domjudge/gitlabci:24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Create the configure file run: make configure - name: Do the default configure diff --git a/.github/workflows/shiftleft.yml b/.github/workflows/shiftleft.yml index be08719bff..af212548e8 100644 --- a/.github/workflows/shiftleft.yml +++ b/.github/workflows/shiftleft.yml @@ -12,7 +12,7 @@ jobs: Scan-Build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Perform Scan uses: ShiftLeftSecurity/scan-action@master diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c4119722b7..b6b6afd1a4 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -13,7 +13,7 @@ jobs: check-static-codecov: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download latest codecov upload script run: wget https://codecov.io/bash -O newcodecov - name: Detect changes to manually verify diff --git a/.github/workflows/webstandard.yml b/.github/workflows/webstandard.yml new file mode 100644 index 0000000000..ac9bc25756 --- /dev/null +++ b/.github/workflows/webstandard.yml @@ -0,0 +1,68 @@ +name: Webstandard (WCAG, W3C) +on: + push: + branches: + - main + - '[0-9]+.[0-9]+' + pull_request: + branches: + - main + - '[0-9]+.[0-9]+' + +jobs: + standards: + runs-on: ubuntu-latest + container: + image: domjudge/gitlabci:24.04 + services: + sqlserver: + image: mariadb + ports: + - 3306:3306 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_USER: domjudge + MYSQL_PASSWORD: domjudge + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 + strategy: + matrix: + role: [public, team, balloon, jury, admin] + test: [w3cval, WCAG2A, WCAG2AA] + db: [bare-install, install] + releaseBranch: + - ${{ contains(github.ref, 'main') }} + exclude: + - releaseBranch: false + - role: jury + test: WCAG2AA + - role: jury + test: WCAG2A + - role: admin + test: WCAG2AA + - role: admin + test: WCAG2A + include: + - role: public + test: WCAG2AA + db: install + - role: public + test: w3cval + db: install + - role: admin + test: w3cval + db: install + steps: + - uses: actions/checkout@v4 + - name: Install DOMjudge + run: .github/jobs/baseinstall.sh ${{ matrix.role }} + - name: Run webstandard tests (W3C, WCAG) + run: .github/jobs/webstandard.sh ${{ matrix.test }} ${{ matrix.role }} + - name: Upload all logs/artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.role }}-${{ matrix.test }}-${{ matrix.db }}-logs + path: | + /tmp/artifacts + /var/log/nginx + /opt/domjudge/domserver/webapp/var/log/*.log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d9ad2b5b02..f5e87061a9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,17 +1,15 @@ include: - '/gitlab/ci/unit.yml' - '/gitlab/ci/integration.yml' - - '/gitlab/ci/webstandard.yml' - '/gitlab/ci/template.yml' - '/gitlab/ci/misc.yml' stages: - test - integration - - accessibility - chroot_checks - unit - style - ci_checks -image: domjudge/gitlabci:22.04 +image: domjudge/gitlabci:24.04 diff --git a/ChangeLog b/ChangeLog index b32771f786..f38584f8e5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,12 @@ DOMjudge Programming Contest Judging System -Version 8.3.0DEV ----------------------------- +Version 8.4.0DEV +--------------------------- + - Get rid of 'internal' data source mode, always requiring - but auto + generating - external ID's for all entities to simplify event logic. + +Version 8.3.0 - 31 May 2024 +--------------------------- - [security] Close metadata file descriptor for the child in runguard. - Document that minimum PHP version is now 8.1.0. - Upgrade Symfony to 6.2 and upgrade other library dependencies. @@ -29,6 +34,30 @@ Version 8.3.0DEV - Add direct button to public page for registering when enabled. - Scale batch size of judge tasks dynamically. - Rename room to location for teams. + - Add option to upload contest problemset document. + - Improve on the advice for rejuding on config changes, only when relevant + advice to start a rejudging, similar for updating the scoreboard cache. + - For interactive problems: add explicit statement to when one of sides of + the interactions ran closed their output. + - Allow importing a (generic) contest warning which is displayed on the scoreboard. + - Added option in the dj_setup_database to make dumps, useful before major + upgrades and during the contest before problem imports. + - Fixed some bugs to become OpenAPI spec compliant again. + - Prevent a race condition leading to two submissions receiving the same + colour balloon. + - Compile executables with `-std=gnu++20` now. + - Record the testcase directory for judging runs, this makes investigating + easier. + - Set default C/C++ standards in configure script, we intend to ship all + scripts free of warnings/errors. + - Respect basic authentication in the URL for the import-contest when .netrc + exists. + - Fix CORS header for API to make extending with other tools easier. + - Fix eventfeed when SQL commits happen out of order to send all events. + - Lazy load the team pictures for the scoreboard, to prevent spamming access + log with HTTP404s + - Make TLE detection more robust for interactive problems. + - Added option for ICPC online scoreboard with no rank displays. Version 8.2.0 - 6 March 2023 ---------------------------- diff --git a/Makefile b/Makefile index df4f2069a0..8584274dec 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ export TOPDIR = $(shell pwd) REC_TARGETS=build domserver install-domserver judgehost install-judgehost \ - docs install-docs inplace-install inplace-uninstall + docs install-docs inplace-install inplace-uninstall maintainer-conf \ + maintainer-install composer-dependencies composer-dependencies-dev # Global Makefile definitions include $(TOPDIR)/Makefile.global @@ -47,7 +48,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 @@ -64,27 +65,6 @@ ifneq "$(JUDGEHOST_BUILD_ENABLED)" "yes" @exit 1 endif -# Install PHP dependencies -composer-dependencies: -ifeq (, $(shell command -v composer 2> /dev/null)) - $(error "'composer' command not found in $(PATH), install it via your package manager or https://getcomposer.org/download/") -endif -# We use --no-scripts here because at this point the autoload.php file is -# not generated yet, which is needed to run the post-install scripts. - composer $(subst 1,-q,$(QUIET)) install --prefer-dist -o -a --no-scripts --no-plugins - -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 - # Generate documentation for distribution. Remove this dependency from # dist above for quicker building from git sources. distdocs: @@ -100,19 +80,23 @@ build-scripts: $(MAKE) -C sql build-scripts # List of SUBDIRS for recursive targets: -build: SUBDIRS= lib misc-tools -domserver: SUBDIRS=etc 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 -docs: SUBDIRS= doc -install-docs: SUBDIRS= doc -inplace-install: SUBDIRS= doc misc-tools -inplace-uninstall: SUBDIRS= doc misc-tools -dist: SUBDIRS= lib sql misc-tools -clean: SUBDIRS=etc doc lib sql judge misc-tools webapp -distclean: SUBDIRS=etc doc lib sql judge misc-tools webapp -maintainer-clean: SUBDIRS=etc doc lib sql judge misc-tools webapp +build: SUBDIRS= lib misc-tools +domserver: SUBDIRS=etc 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 +docs: SUBDIRS= doc +install-docs: SUBDIRS= doc +maintainer-conf: SUBDIRS= webapp +maintainer-install: SUBDIRS= webapp +inplace-install: SUBDIRS= doc misc-tools +inplace-uninstall: SUBDIRS= doc misc-tools +dist: SUBDIRS= lib sql misc-tools +clean: SUBDIRS=etc doc lib sql judge misc-tools webapp +distclean: SUBDIRS=etc doc lib sql judge misc-tools webapp +maintainer-clean: SUBDIRS=etc doc lib sql judge misc-tools webapp +composer-dependencies: SUBDIRS= webapp +composer-dependencies-dev: SUBDIRS= webapp domserver-create-dirs: $(INSTALL_DIR) $(addprefix $(DESTDIR),$(domserver_dirs)) @@ -167,6 +151,17 @@ endif # Fix permissions and ownership for password files: -$(INSTALL_USER) -m 0600 -t $(DESTDIR)$(judgehost_etcdir) \ etc/restapi.secret + @echo "" + @echo "========== Judgehost Install Completed ==========" + @echo "" + @echo "Optionally:" + @echo " - Install the create-cgroup service to setup the secure judging restrictions:" + @echo " cp judge/create-cgroups.service /etc/systemd/system/" + @echo " - Install the judgehost service:" + @echo " cp judge/domjudge-judgedaemon@.service /etc/systemd/system/" + @echo " - You can enable the judgehost on CPU core 1 with:" + @echo " systemctl enable domjudge-judgedaemon@1" + @echo "" check-root: @if [ `id -u` -ne 0 -a -z "$(QUIET)" ]; then \ @@ -193,7 +188,7 @@ paths.mk: @exit 1 # Configure for running in source tree, not meant for normal use: -maintainer-conf: inplace-conf-common composer-dependencies-dev webapp/.env.local +maintainer-conf: inplace-conf-common composer-dependencies-dev inplace-conf: inplace-conf-common composer-dependencies inplace-conf-common: dist ./configure $(subst 1,-q,$(QUIET)) --prefix=$(CURDIR) \ @@ -211,18 +206,11 @@ inplace-conf-common: dist --with-baseurl='http://localhost/domjudge/' \ $(CONFIGURE_FLAGS) -# Run Symfony in dev mode (for maintainer-mode): -webapp/.env.local: - @echo "Creating file '$@'..." - @echo "# This file was automatically created by 'make maintainer-conf' to run" > $@ - @echo "# the DOMjudge Symfony application in developer mode. Adjust as needed." >> $@ - @echo "APP_ENV=dev" >> $@ - # Install the system in place: don't really copy stuff, but create # 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 +maintainer-install: inplace-install +inplace-install: build domserver-create-dirs judgehost-create-dirs inplace-install-l: # Replace libjudgedir with symlink to prevent lots of symlinks: -rmdir $(judgehost_libjudgedir) @@ -240,7 +228,7 @@ inplace-install-l: $(MKDIR_P) $(domserver_tmpdir) chmod a+rwx $(domserver_tmpdir) # Make sure we're running from a clean state: - composer auto-scripts + (cd webapp && composer auto-scripts) @echo "" @echo "========== Maintainer Install Completed ==========" @echo "" @@ -314,7 +302,7 @@ coverity-conf: coverity-build: paths.mk $(MAKE) build build-scripts # Secondly, delete all upstream PHP libraries to not analyze those: - -rm -rf lib/vendor/* + -rm -rf webapp/vendor/* @VERSION=` grep '^VERSION =' paths.mk | sed 's/^VERSION = *//'` ; \ PUBLISHED=`grep '^PUBLISHED =' paths.mk | sed 's/^PUBLISHED = *//'` ; \ if [ "$$PUBLISHED" = release ]; then DESC="release" ; \ @@ -341,5 +329,4 @@ 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 \ coverity-conf coverity-build diff --git a/README.md b/README.md index 58b0fa0c77..e86c74860d 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.0DEV +This is the Programming Contest Jury System "DOMjudge" version 8.4.0DEV DOMjudge is a system for running a programming contest, like the ICPC regional and world championship programming contests. @@ -72,7 +72,7 @@ The M4 autoconf macros are licensed under all-permissive and GPL3+ licences; see the respective files under m4/ for details. The DOMjudge tarball ships external library dependencies in the -lib/vendor directory. These are covered by their individual licenses +webapp/vendor directory. These are covered by their individual licenses as specified in the file composer.lock. Contact diff --git a/SECURITY.md b/SECURITY.md index e67564f72d..ead247d37f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,10 +4,10 @@ | DOMjudge Version | Supported | PHP version supported | |------------------| ------------------ |-----------------------| -| main branch | :warning: | 8.1-8.2 | +| main branch | :warning: | 8.1-8.3 | +| 8.3.x | :white_check_mark: | 8.1-8.3 | | 8.2.x | :white_check_mark: | 7.4-8.2 | -| 8.1.x | :white_check_mark: | 7.4-8.2 | -| < 8.1 | :x: | :x: | +| < 8.2 | :x: | :x: | ## Reporting a Vulnerability diff --git a/configure.ac b/configure.ac index 5e1bac1529..f5eed5c774 100644 --- a/configure.ac +++ b/configure.ac @@ -170,7 +170,6 @@ if test "x$FHS_ENABLED" = xyes ; then AC_SUBST(domserver_webappdir, $datadir/${PACKAGE_TARNAME}/webapp) AC_SUBST(domserver_sqldir, $datadir/${PACKAGE_TARNAME}/sql) AC_SUBST(domserver_libdir, $libdir/${PACKAGE_TARNAME}) - AC_SUBST(domserver_libvendordir, $libdir/${PACKAGE_TARNAME}/vendor) AC_SUBST(domserver_logdir, $localstatedir/log/${PACKAGE_TARNAME}) AC_SUBST(domserver_rundir, $localstatedir/run/${PACKAGE_TARNAME}) AC_SUBST(domserver_tmpdir, /tmp) @@ -189,7 +188,6 @@ if test "x$FHS_ENABLED" = xyes ; then AC_SUBST(judgehost_tmpdir, /tmp) AC_SUBST(judgehost_judgedir, $localstatedir/lib/${PACKAGE_TARNAME}/judgings) AC_SUBST(judgehost_chrootdir, /chroot/${PACKAGE_TARNAME}) - AC_SUBST(judgehost_cgroupdir, /sys/fs/cgroup) fi AC_SUBST(domjudge_docdir, $docdir) @@ -223,7 +221,6 @@ AX_PATH(domserver_etcdir, [$domserver_root/etc]) AX_PATH(domserver_webappdir, [$domserver_root/webapp]) AX_PATH(domserver_sqldir, [$domserver_root/sql]) AX_PATH(domserver_libdir, [$domserver_root/lib]) -AX_PATH(domserver_libvendordir, [$domserver_root/lib/vendor]) AX_PATH(domserver_logdir, [$domserver_root/log]) AX_PATH(domserver_rundir, [$domserver_root/run]) AX_PATH(domserver_tmpdir, [$domserver_root/tmp]) @@ -241,32 +238,11 @@ AX_PATH(judgehost_rundir, [$judgehost_root/run]) AX_PATH(judgehost_tmpdir, [$judgehost_root/tmp]) AX_PATH(judgehost_judgedir, [$judgehost_root/judgings]) AX_PATH(judgehost_chrootdir, [/chroot/${PACKAGE_TARNAME}]) -AX_PATH(judgehost_cgroupdir, [/sys/fs/cgroup]) fi AX_WITH_COMMENT(7,[ ]) # }}} -# {{{ Directory for systemd unit files - -PKG_PROG_PKG_CONFIG() -AC_ARG_WITH([systemdsystemunitdir], - [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd service files])],, - [with_systemdsystemunitdir=auto]) -AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"], [ - AS_IF([test "x$PKG_CONFIG" = "x"],AC_MSG_ERROR([systemd support requested but no pkg-config available to query systemd package])) - def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) - - AS_IF([test "x$def_systemdsystemunitdir" = "x"], - [AS_IF([test "x$with_systemdsystemunitdir" = "xyes"], - [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) - with_systemdsystemunitdir=no], - [with_systemdsystemunitdir="$def_systemdsystemunitdir"])]) -AS_IF([test "x$with_systemdsystemunitdir" != "xno"], - [AC_SUBST([systemd_unitdir], [$with_systemdsystemunitdir])]) - -# }}} - AC_MSG_CHECKING([baseurl]) AC_ARG_WITH([baseurl], [AS_HELP_STRING([--with-baseurl=URL], [Base URL of the DOMjudge web interfaces. Example default: @@ -384,7 +360,6 @@ echo " * domserver...........: AX_VAR_EXPAND($domserver_root)" echo " - bin..............: AX_VAR_EXPAND($domserver_bindir)" echo " - etc..............: AX_VAR_EXPAND($domserver_etcdir)" echo " - lib..............: AX_VAR_EXPAND($domserver_libdir)" -echo " - libvendor........: AX_VAR_EXPAND($domserver_libvendordir)" echo " - log..............: AX_VAR_EXPAND($domserver_logdir)" echo " - run..............: AX_VAR_EXPAND($domserver_rundir)" echo " - sql..............: AX_VAR_EXPAND($domserver_sqldir)" @@ -405,11 +380,7 @@ echo " - run..............: AX_VAR_EXPAND($judgehost_rundir)" echo " - tmp..............: AX_VAR_EXPAND($judgehost_tmpdir)" echo " - judge............: AX_VAR_EXPAND($judgehost_judgedir)" echo " - chroot...........: AX_VAR_EXPAND($judgehost_chrootdir)" -echo " - cgroup...........: AX_VAR_EXPAND($judgehost_cgroupdir)" fi -echo "" -echo " * systemd unit files..: AX_VAR_EXPAND($systemd_unitdir)" -echo "" echo "Run 'make' without arguments to get a list of (build) targets." echo "" if test "x$BASEURL_UNCONFIGURED" = x1 ; then diff --git a/doc/logos/DOMjudgelogo-black-on-white.png b/doc/logos/DOMjudgelogo-black-on-white.png new file mode 100644 index 0000000000..2b3c31d50b Binary files /dev/null and b/doc/logos/DOMjudgelogo-black-on-white.png differ diff --git a/doc/logos/DOMjudgelogo-with-white-background.png b/doc/logos/DOMjudgelogo-with-white-background.png index 113bc65879..090f36d2f9 100644 Binary files a/doc/logos/DOMjudgelogo-with-white-background.png and b/doc/logos/DOMjudgelogo-with-white-background.png differ diff --git a/doc/logos/DOMjudgelogo.png b/doc/logos/DOMjudgelogo.png index 108e4f95f4..cfc4b93f99 100644 Binary files a/doc/logos/DOMjudgelogo.png and b/doc/logos/DOMjudgelogo.png differ diff --git a/doc/logos/DOMjudgelogo.svg b/doc/logos/DOMjudgelogo.svg index 75dde9b7ac..bc3175b19d 100644 --- a/doc/logos/DOMjudgelogo.svg +++ b/doc/logos/DOMjudgelogo.svg @@ -2,13 +2,6 @@ image/svg+xmlDOM + transform="matrix(-0.00528445,0.99706308,0.99998604,0.00519989,463.70996,56.28613)" + style="font-variant:normal;font-weight:bold;font-size:201.63111877px;font-family:Verdana, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, Geneva, Arial, sans-serif;-inkscape-font-specification:Verdana-Bold;writing-mode:lr-tb;fill:#c1c4cb;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text41">DOM DOM + transform="matrix(-0.00528445,0.99706308,0.99998604,0.00519989,457.20703,62.77148)" + style="font-variant:normal;font-weight:bold;font-size:201.63111877px;font-family:Verdana, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, Geneva, Arial, sans-serif;-inkscape-font-specification:Verdana-Bold;writing-mode:lr-tb;fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text45">DOM judge + transform="matrix(-0.00529202,0.99852359,1.001451,0.00520758,0,0)" + style="font-style:italic;font-variant:normal;font-weight:bold;font-size:71.296px;font-family:Verdana, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, Geneva, Arial, sans-serif;-inkscape-font-specification:Verdana-BoldItalic;writing-mode:lr-tb;fill:#c1c4cb;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.998537" + id="text49" + x="591.8446" + y="366.89072">judge judge + transform="matrix(-0.00529202,0.99852359,1.001451,0.00520758,0,0)" + style="font-style:italic;font-variant:normal;font-weight:bold;font-size:71.296px;font-family:Verdana, 'Bitstream Vera Sans', 'DejaVu Sans', Tahoma, Geneva, Arial, sans-serif;-inkscape-font-specification:Verdana-BoldItalic;writing-mode:lr-tb;fill:#676e72;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.998537" + id="text53" + x="594.1568" + y="364.60156">judge / + d="M 212.91504,836.43604 187.46143,771.91748 H 168.37061 V 587.12256 H 153.25732 V 383.20996 H 136.5542 V 83.7168 h 89.8833" + style="fill:none;stroke:#231f20;stroke-width:1.65658998;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" + id="path55" + inkscape:connector-curvature="0" />/ / + transform="matrix(0.99862206,0,0,-1,195.41504,773.50928)" + style="font-variant:normal;font-weight:bold;font-size:29.47159004px;font-family:Courier;-inkscape-font-specification:CourierNewPS-BoldMT;writing-mode:lr-tb;fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text69">/ \ + transform="matrix(0.99862206,0,0,-1,213.71094,772.71338)" + style="font-variant:normal;font-weight:bold;font-size:29.47159004px;font-family:Courier;-inkscape-font-specification:CourierNewPS-BoldMT;writing-mode:lr-tb;fill:#c1c4cb;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text73">\ \ + transform="matrix(0.99862206,0,0,-1,212.91504,773.50928)" + style="font-variant:normal;font-weight:bold;font-size:29.47159004px;font-family:Courier;-inkscape-font-specification:CourierNewPS-BoldMT;writing-mode:lr-tb;fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="text77">\ \ No newline at end of file + d="m 216.26563,587.27588 v 51.16846 c 0,30.1582 0.72656,59.14355 2.1875,86.96533 1.45312,27.81201 3.54687,41.72314 6.27343,41.72314 1.5625,0 2.88282,-4.10156 3.96875,-12.28418 1.07813,-8.18261 2.10938,-23.38525 3.09375,-45.58789 0.98438,-22.20263 1.47657,-45.81445 1.47657,-70.8164 v -51.16846 m -4,0.0195 v 51.12842 c 0,25.28857 -0.57813,47.40234 -1.73438,66.34131 -0.65625,10.45068 -1.58594,15.67578 -2.77344,15.67578 -1.15625,0 -2.10156,-5.75781 -2.83593,-17.26319 -1.10938,-17.43066 -1.65625,-39.01171 -1.65625,-64.7539 v -51.12842" + style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path79" + inkscape:connector-curvature="0" /> diff --git a/doc/manual/config-advanced.rst b/doc/manual/config-advanced.rst index 72b8016740..9a471ac6ed 100644 --- a/doc/manual/config-advanced.rst +++ b/doc/manual/config-advanced.rst @@ -16,30 +16,25 @@ path of your installation) as follows: - *Affiliation logos*: these will be shown with the teams that are part of the affiliation, if the ``show_affiliation_logos`` configuration option is enabled. They can be placed in - `public/images/affiliations/1234.png` where *1234* is the numeric ID + `public/images/affiliations/1234.png` where *1234* is the :ref:`external ID ` of the affiliation as shown in the DOMjudge interface. There is a separate option ``show_affiliations`` that independently controls where the affiliation *names* are shown on the scoreboard. These logos should be square and be at least 64x64 pixels, but not much bigger. - *Team pictures*: a photo of the team will be shown in the team details page if `public/images/teams/456.jpg` exists, where *456* is the - team's numeric ID as shown in the DOMjudge interface. DOMjudge will not + team's :ref:`external ID ` as shown in the DOMjudge interface. DOMjudge will not modify the photos in any way or form, so make sure you don't upload photos that are too big, since that will incur a lot of network traffic. - *Contest Banners*: a page-wide banner can be shown on the public scoreboard if that image is placed in `public/images/banners/1.png` where *1* is the - contest's numeric ID as shown in the DOMjudge interface. Alternatively, you + contest's :ref:`external ID ` as shown in the DOMjudge interface. Alternatively, you can place a file at `public/images/banner.png` which will be used as a banner for all contests. Contest-specific banners always have priority. Contest banners usually are rectangular, having a width of around 1920 pixels and a height of around 300 pixels. Other ratio's and sizes are supported, but check the public scoreboard to see how it looks. -.. note:: - - The IDs for affiliations, teams and contests need to be the *external ID* - if the ``data_source`` setting of DOMjudge is set to external. - It is also possible to load custom CSS and/or JavaScript files. To do so, place files ending in `.css` under `public/css/custom/` and/or files ending in `.js` under `public/js/custom/`. See the Config checker in the admin interface for the diff --git a/doc/manual/develop.rst b/doc/manual/develop.rst index e5912f6a63..13f41477c9 100644 --- a/doc/manual/develop.rst +++ b/doc/manual/develop.rst @@ -39,11 +39,6 @@ already listed under :ref:`judgehost ` and :ref:`submit client ` requirements):: - sudo apt install autoconf automake bats \ - python-sphinx python-sphinx-rtd-theme rst2pdf fontconfig python3-yaml latexmk - -On Debian 11 (Bullseye) and above, instead install:: - sudo apt install autoconf automake bats \ python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig python3-yaml \ latexmk texlive-latex-recommended texlive-latex-extra tex-gyre diff --git a/doc/manual/import.rst b/doc/manual/import.rst index 2b685943b4..b727551c46 100644 --- a/doc/manual/import.rst +++ b/doc/manual/import.rst @@ -13,6 +13,22 @@ installation. To use the CLI, you need to replace ```` with the path to the ``webapp`` directory of the DOMserver. +.. _external-ids: + +External ID's +------------- + +Most entities in DOMjudge have an `eternal ID`. External ID's are used to link +entities in DOMjudge to entities in an external system, e.g. the ICPC CMS. The +API uses the external ID's to expose entities to other systems. When you create +an entity in DOMjudge, specifying the external ID is optional; DOMjudge will +use an automatically generated ID if you don't provide one. However, if this ID +is not unique, you will get a message telling you that you need to provide your +own external ID. + +When importing entities in bulk as described below, the external ID will be +populated with the ID as specified in the files you import. + Importing team categories ------------------------- @@ -33,9 +49,7 @@ fields: - ``sortorder`` (defaults to ``0``): the sort order of the team category to use on the scoreboard. Categories with the same sortorder will be grouped together. -If the ``data_source`` setting of DOMjudge is set to external, the ``id`` field will be the -ID used for the group. Otherwise, it will be exposed as ``externalid`` and a group ID will be -generated by DOMjudge. +The ``id`` field will be the ID used for the group. Example ``groups.json``:: @@ -73,9 +87,7 @@ Each of the following lines must contain the following elements separated by tab - the category ID. Must be unique - the name of the team category as shown on the scoreboard -If the ``data_source`` setting of DOMjudge is set to external, the category ID field will be -the ID used for the group. Otherwise, it will be exposed as ``externalid`` and a group ID will -be generated by DOMjudge. +The ``id`` field will be the ID used for the group. Example ``groups.tsv``:: @@ -114,9 +126,7 @@ fields: - ``formal_name``: the affiliation name as used on the scoreboard - ``country``: the country code in form of ISO 3166-1 alpha-3 -If the ``data_source`` setting of DOMjudge is set to external, the ``id`` field will be the -ID used for the affiliation. Otherwise, it will be exposed as ``externalid`` and an affiliation -ID will be generated by DOMjudge. +The ``id`` field will be the ID used for the affiliation. Example ``organizations.json``:: @@ -168,11 +178,8 @@ fields: - ``organization_id``: the ID of the team affiliation this team belongs to - ``location.description`` (optional): the location of the team -If the ``data_source`` setting of DOMjudge is set to external, the ``id`` field will be the -ID used for the team and the ``group_ids`` and ``organization_id`` fields are the values as -provided during the import of the other files listed above. Otherwise, the ``id`` will be -exposed as ``externalid``, a team ID will be generated by DOMjudge and you need to use the -ID's as generated by DOMjudge for ``group_ids`` as well as ``organization_id``. +The ``id`` field will be the ID used for the team and the ``group_ids`` and ``organization_id`` +fields are the values as provided during the import of the other files listed above. Example ``teams.json``:: @@ -221,11 +228,8 @@ Each of the following lines must contain the following elements separated by tab - a country code in form of ISO 3166-1 alpha-3 - an external institution ID, e.g. from the ICPC CMS, may be empty -If the ``data_source`` setting of DOMjudge is set to external, the team ID field will be the -ID used for the team and the category ID field is the value as provided during the import of -the other files listed above. Otherwise, the team ID will be exposed as ``externalid``, a -team ID will be generated by DOMjudge and you need to use the ID as generated by DOMjudge -for the category ID. +The team `id` field will be the ID used for the team and the category ID field is the value +as provided during the import of the other files listed above. Example ``teams2.tsv``:: @@ -265,10 +269,8 @@ fields: - ``name``: (optional) the full name of the account - ``ip`` (optional): IP address to link to this account -If the ``data_source`` setting of DOMjudge is set to external, the ``id`` field will be the ID -used for the user and the ``team_id`` field is the value as provided during the team import. -Otherwise, the ``id`` will be exposed as ``externalid``, a user ID will be generated by DOMjudge -and you need to use the ID as generated by DOMjudge for ``team_id``. +The ``id`` field will be the ID used for the user and the ``team_id`` field is the value as provided during +the team import. Example ``accounts.yaml``:: diff --git a/doc/manual/index.rst b/doc/manual/index.rst index 2af6aeffa8..1ba568179c 100644 --- a/doc/manual/index.rst +++ b/doc/manual/index.rst @@ -26,3 +26,4 @@ Appendices problem-format shadow configuration-reference + install-language diff --git a/doc/manual/install-judgehost.rst b/doc/manual/install-judgehost.rst index fafe28c7f8..4066cad4ee 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, diff --git a/doc/manual/install-language.rst b/doc/manual/install-language.rst new file mode 100644 index 0000000000..61ec9b2557 --- /dev/null +++ b/doc/manual/install-language.rst @@ -0,0 +1,97 @@ +Appendix: Installing the example languages +========================================== + +DOMjudge ships with some default languages with a default configuration. +As you might set up contests with those languages we provide how those languages were +installed in the past as guideline. Use ``dj_run_chroot`` for most of those packages, and see +the section :ref:`make-chroot` for more information. + +Most of the languages can be installed from the table below as there is a package available +to install inside the judging chroot. Given that you can install your own chroot we only provide the +packages for Ubuntu as that is the most used at the moment of writing. + +.. list-table:: Packages for languages + :header-rows: 1 + + * - Language + - Ubuntu package + - Remarks + * - Ada + - `gnat` + - + * - AWK + - `mawk`/`gawk` + - `mawk` is default installed + * - Bash + - `bash` + - Default installed in the chroot + * - C + - `gcc` + - Default installed in the chroot + * - C++ + - `g++` + - Default installed in the chroot + * - C# + - `mono-mcs` + - + * - Fortran + - `gfortran` + - + * - Haskell + - `ghc` + - After installing you need to move these files + `/{usr->var}/lib/ghc/package.conf.d` as `/var` + is not mounted during compilation. + * - Java + - `default-jdk-headless` + - Default installed in the chroot + * - Javascript + - `nodejs` + - + * - Kotlin + - `kotlin` + - + * - Lua + - `lua5.4` + - Ubuntu does not ship a generic meta package (yet). + * - Pascal + - `fp-compiler` + - + * - Perl + - `perl-base` + - Default installed in the chroot + * - POSIX shell + - `dash` + - Default installed in the chroot + * - Prolog + - `swi-prolog-core-packages` + - + * - Python3 + - `pypy3`/`python3` + - Default installed in the chroot. + DOMjudge assumes `pypy3` as it runs faster in general. + Consider the `PyPy3 PPA`_ if you need the latest python3 features. PyPy3 does not have 100% + compatibility with all non-standard libraries. In case this is needed you should reconsider the default + CPython implementation. + * - OCaml + - `ocaml` + - + * - R + - `r-base-core` + - + * - Ruby + - `ruby` + - + * - Rust + - `rustc` + - + * - Scala + - `scala` + - + * - Swift + - + - See the `Swift instructions`_, unpack the directory in the chroot and install `libncurses6`. Depending + on where you install the directory you might need to extend the `PATH` in the `run` script. + +.. _PyPy3 PPA: https://launchpad.net/~pypy/+archive/ubuntu/ppa +.. _Swift instructions: https://www.swift.org/documentation/server/guides/deploying/ubuntu.html diff --git a/doc/manual/install-workstation.rst b/doc/manual/install-workstation.rst index 1b862cb7d7..de51ea9ebb 100644 --- a/doc/manual/install-workstation.rst +++ b/doc/manual/install-workstation.rst @@ -84,13 +84,10 @@ When DOMjudge is configured and site-specific configuration set, the team manual can be generated with the command ``make docs``. The following should do it on a Debian-like system:: - sudo apt install python-sphinx python-sphinx-rtd-theme rst2pdf fontconfig python3-yaml + sudo apt install python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig python3-yaml cd /doc/ make docs -On Debian 11 and above, install -``python3-sphinx python3-sphinx-rtd-theme rst2pdf fontconfig python3-yaml`` instead. - The resulting manual will then be found in the ``team/`` subdirectory. .. _netrc manual page: https://ec.haxx.se/usingcurl/usingcurl-netrc diff --git a/doc/manual/problem-format.rst b/doc/manual/problem-format.rst index e9f6cbc300..fd2d7990bd 100644 --- a/doc/manual/problem-format.rst +++ b/doc/manual/problem-format.rst @@ -44,14 +44,16 @@ interface): - ``special_compare`` - executable id of a special compare script - ``points`` - number of points for this problem (defaults to 1) - ``color`` - CSS color specification for this problem - -The basename of the ZIP-file will be used as the problem short name (e.g. "A"). -All keys are optional. If they are present, the respective value will be -overwritten; if not present, then the value will not be changed or a default -chosen when creating a new problem. Test data files are added to set of test -cases already present. Thus, one can easily add test cases to a configured -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 + - ``externalid`` - the external id of the problem + - ``short-name`` - the short name of the problem + +The basename of the ZIP-file will be used as the problem external id and short +name (e.g. "A"). All keys are optional. If they are present, the respective +value will be overwritten; if not present, then the value will not be changed +or a default chosen when creating a new problem. Test data files are added to +set of test cases already present. Thus, one can easily add test cases to a +configured 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/legacy-icpc diff --git a/doc/manual/shadow.rst b/doc/manual/shadow.rst index d5e9f0d850..986d3e2695 100644 --- a/doc/manual/shadow.rst +++ b/doc/manual/shadow.rst @@ -19,11 +19,9 @@ Configuring DOMjudge In the DOMjudge admin interface, go to *Configuration settings* page and modify the settings to mimic the system to shadow from. Also make sure to set -*data_source* to ``configuration and live data external``. This tells DOMjudge +*shadow_mode* to ``true``. This tells DOMjudge that it will be a shadow for an external system. This will: -* Expose external ID's in the API for both configuration and live data, i.e. - problems, teams, etc. as well as submissions, judgings and runs. * Add a *Shadow Differences* and *External Contest Sources* item to the jury menu and homepage for admins. * Expose additional information in the submission overview and detail pages. diff --git a/doc/manual/team-overview.png b/doc/manual/team-overview.png index e2f678b438..bed144a5bd 100644 Binary files a/doc/manual/team-overview.png and b/doc/manual/team-overview.png differ diff --git a/doc/manual/team-scoreboard.png b/doc/manual/team-scoreboard.png index ea1afa1b6e..2ba704fdf1 100644 Binary files a/doc/manual/team-scoreboard.png and b/doc/manual/team-scoreboard.png differ diff --git a/docker-compose.yml b/docker-compose.yml index 112065a42f..181596b96c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,6 @@ # It is recommended to use `docker compose up` to start this stack. Note, don't # use sudo or the legacy docker-compose. -version: '3' - services: mariadb: image: docker.io/mariadb diff --git a/etc/db-config.yaml b/etc/db-config.yaml index 9c6b0866b4..665a48a6e8 100644 --- a/etc/db-config.yaml +++ b/etc/db-config.yaml @@ -355,17 +355,11 @@ enum_class: App\Utils\EventFeedFormat public: false description: Format of the event feed to use. See [current draft](https://ccs-specs.icpc.io/draft/contest_api#event-feed) and [versions available](https://ccs-specs.icpc.io/). - - name: data_source - type: int - default_value: 0 + - name: shadow_mode + type: bool + default_value: false public: false - description: "Source of data: used to indicate whether internal or external IDs are exposed in the API. `configuration data external` is typically used when loading configuration data from the ICPC CMS, and `configuration and live data external` when running DOMjudge as \"shadow system\"." - options: - 0: all local - 1: configuration data external - 2: configuration and live data external - regex: /^\d+$/ - error_message: A value between 0 and 2 is required. + description: Is this system running as a shadow system? docdescription: See :doc:`the chapter on running DOMjudge as a shadow system` for more information. - name: external_contest_sources_allow_untrusted_certificates type: bool diff --git a/etc/domserver-static.php.in b/etc/domserver-static.php.in index 4181309167..68967266b4 100644 --- a/etc/domserver-static.php.in +++ b/etc/domserver-static.php.in @@ -7,14 +7,14 @@ define('DOMJUDGE_VERSION', '@DOMJUDGE_VERSION@'); -define('BINDIR', '@domserver_bindir@'); -define('ETCDIR', '@domserver_etcdir@'); -define('WEBAPPDIR', '@domserver_webappdir@'); -define('LIBDIR', '@domserver_libdir@'); -define('SQLDIR', '@domserver_sqldir@'); -define('LIBVENDORDIR','@domserver_libvendordir@'); -define('LOGDIR', '@domserver_logdir@'); -define('RUNDIR', '@domserver_rundir@'); -define('TMPDIR', '@domserver_tmpdir@'); +define('BINDIR', '@domserver_bindir@'); +define('ETCDIR', '@domserver_etcdir@'); +define('WEBAPPDIR', '@domserver_webappdir@'); +define('LIBDIR', '@domserver_libdir@'); +define('SQLDIR', '@domserver_sqldir@'); +define('VENDORDIR', '@domserver_webappdir@/vendor'); +define('LOGDIR', '@domserver_logdir@'); +define('RUNDIR', '@domserver_rundir@'); +define('TMPDIR', '@domserver_tmpdir@'); -define('BASEURL', '@BASEURL@'); +define('BASEURL', '@BASEURL@'); diff --git a/etc/judgehost-static.php.in b/etc/judgehost-static.php.in index f377d48d46..602b81445c 100644 --- a/etc/judgehost-static.php.in +++ b/etc/judgehost-static.php.in @@ -16,7 +16,6 @@ define('RUNDIR', '@judgehost_rundir@'); define('TMPDIR', '@judgehost_tmpdir@'); define('JUDGEDIR', '@judgehost_judgedir@'); define('CHROOTDIR', '@judgehost_chrootdir@'); -define('CGROUPDIR', '@judgehost_cgroupdir@'); define('RUNUSER', '@RUNUSER@'); define('RUNGROUP', '@RUNGROUP@'); diff --git a/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-submissions.R b/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-submissions.R new file mode 120000 index 0000000000..74404bdb3e --- /dev/null +++ b/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-submissions.R @@ -0,0 +1 @@ +fltcmp-test-write-files-between-testcases.R \ No newline at end of file diff --git a/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-testcases.R b/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-testcases.R new file mode 100755 index 0000000000..fc9a2fac88 --- /dev/null +++ b/example_problems/fltcmp/submissions/accepted/fltcmp-test-write-files-between-testcases.R @@ -0,0 +1,73 @@ +# This checks if we can write extra files +# between different testcase runs. R uses +# a temporary directory which DOMjudge should +# make unavailable between runs and in case this +# is forgotten teams can precalculate extra files for later +# testcases. We need a working solution to make sure +# we get to all testcases. +# +# This submission is symlinked to make sure we submit it twice +# to verify that we can't see the TMPDIR of the earlier submission. +# +# The script will be WRONG-ANSWER if any of the assumptions +# are wrong. +# The script will be NO-OUTPUT if there is a security violation. +# The verdict RUN-ERROR is expected if we correctly detect the +# violation and would be the correct verdict if we don't need a +# TMPDIR. +# +# @EXPECTED_RESULTS@: CORRECT + +# For R the TMPDIR needs to be set, if it's not +# we can skip this whole test. +tmpdir<-Sys.getenv(c("TMPDIR")) +if (tmpdir == '') { + print("TMPDIR not set, update installation instructions.") + quit() +} + +# We had 3 testcases in the past, try with double for when new testcases get added. +testcaseIds<-rep(1:6) +possibleTempDirs <- (function (x) sprintf("/testcase%05d/write_tmp", x))(testcaseIds) +if (!(tmpdir %in% possibleTempDirs)) { + print("Either TMPDIR format has changed, or too many testcases.") + print(sprintf("Current TMPDIR: %s", tmpdir)) + quit() +} + +for (possibleTempDir in possibleTempDirs) { + teamFile<-sprintf("%s/fileFromTeam.txt", possibleTempDir) + if (file.exists(teamFile)) { + # File should not be here + quit() + } +} + +# Try to write to our TMPDIR to read it in the next testcase. +currentTeamFile<-sprintf("%s/fileFromTeam.txt", tmpdir) +fileConn<-file(currentTeamFile) +writeLines(c("Calculate","all","floats"), fileConn) +close(fileConn) +# Make sure our test from earlier does work for this testcase +if (!(file.exists(currentTeamFile))) { + # File should be here + quit() +} + +# Now try the actual problem to advance to the next testcase. +input<-file("stdin") +lines<-readLines(input) + +# https://evolutionarygenetics.github.io/Chapter10.html +# a simple reciprocal example +reciprocal <- function(x){ + # calculate the reciprocal + y <- 1/x + return(y) +} + +for (l in lines[-1]) { + l<-as.numeric(l) + r<-reciprocal(l) + cat(r,"\n") +} diff --git a/example_problems/hello/submissions/accepted/extra-file-awk/test-hello.awk b/example_problems/hello/submissions/accepted/extra-file-awk/test-hello.awk index de2d0a8c25..e41d032f7f 100644 --- a/example_problems/hello/submissions/accepted/extra-file-awk/test-hello.awk +++ b/example_problems/hello/submissions/accepted/extra-file-awk/test-hello.awk @@ -3,4 +3,4 @@ # # @EXPECTED_RESULTS@: CORRECT -BEGIN { if ( DOMJUDGE ) print "Hello world!"; else print "variable DOMJUDGE not set" } +BEGIN { print "Hello world!" } diff --git a/example_problems/hello/submissions/accepted/extra-file-cs/test-hello.cs b/example_problems/hello/submissions/accepted/extra-file-cs/test-hello.cs index a70e0aae18..03133239e9 100644 --- a/example_problems/hello/submissions/accepted/extra-file-cs/test-hello.cs +++ b/example_problems/hello/submissions/accepted/extra-file-cs/test-hello.cs @@ -11,10 +11,6 @@ public class Hello { public static void Main(string[] args) { -#if ONLINE_JUDGE Console.Write("Hello world!\n"); -#else - Console.Write("ONLINE_JUDGE not defined\n"); -#endif } } diff --git a/example_problems/hello/submissions/accepted/extra-file-f95/test-hello.f95 b/example_problems/hello/submissions/accepted/extra-file-f95/test-hello.f95 index 85294d8957..58dc8e9b1b 100644 --- a/example_problems/hello/submissions/accepted/extra-file-f95/test-hello.f95 +++ b/example_problems/hello/submissions/accepted/extra-file-f95/test-hello.f95 @@ -5,10 +5,6 @@ ! @EXPECTED_RESULTS@: CORRECT ! program hello -#ifdef DOMJUDGE - write(*,"(A)") "Hello world!" -#else - write(*,"(A)") "DOMJUDGE not defined" -#endif +write(*,"(A)") "Hello world!" end program hello diff --git a/example_problems/hello/submissions/accepted/extra-file-js/test-hello.js b/example_problems/hello/submissions/accepted/extra-file-js/test-hello.js index a984f6dc1f..4645061197 100644 --- a/example_problems/hello/submissions/accepted/extra-file-js/test-hello.js +++ b/example_problems/hello/submissions/accepted/extra-file-js/test-hello.js @@ -3,9 +3,4 @@ // // @EXPECTED_RESULTS@: CORRECT -if ( process.env.DOMJUDGE ) { - console.log('Hello world!'); -} else { - console.log('DOMJUDGE not defined'); - process.exit(1); -} +console.log('Hello world!'); diff --git a/example_problems/hello/submissions/accepted/extra-file-rb/test-hello.rb b/example_problems/hello/submissions/accepted/extra-file-rb/test-hello.rb index d7e30f58f6..e4b09af8ae 100644 --- a/example_problems/hello/submissions/accepted/extra-file-rb/test-hello.rb +++ b/example_problems/hello/submissions/accepted/extra-file-rb/test-hello.rb @@ -3,9 +3,4 @@ # # @EXPECTED_RESULTS@: CORRECT -if ENV['DOMJUDGE'] != '' then - puts "Hello world!" -else - puts "DOMJUDGE not defined" - exit(1) -end +puts "Hello world!" diff --git a/example_problems/hello/submissions/accepted/extra-file-sh/test-hello.sh b/example_problems/hello/submissions/accepted/extra-file-sh/test-hello.sh index 94b887c8e1..8afe60aaa6 100644 --- a/example_problems/hello/submissions/accepted/extra-file-sh/test-hello.sh +++ b/example_problems/hello/submissions/accepted/extra-file-sh/test-hello.sh @@ -3,11 +3,6 @@ # # @EXPECTED_RESULTS@: CORRECT -if [ -z "$DOMJUDGE" -o -z "$ONLINE_JUDGE" ]; then - echo "Variable DOMJUDGE and/or ONLINE_JUDGE not defined." - exit 1 -fi - echo "Hello world!" exit 0 diff --git a/example_problems/hello/submissions/accepted/multifile-js-2/hello.js b/example_problems/hello/submissions/accepted/multifile-js-2/hello.js new file mode 100644 index 0000000000..7966b333a2 --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js-2/hello.js @@ -0,0 +1,7 @@ +// This should give CORRECT on the default problem 'hello'. +// It does include another file for the actual implementation +// +// @EXPECTED_RESULTS@: CORRECT + +import { hello } from './module.js'; +console.log(hello()); diff --git a/example_problems/hello/submissions/accepted/multifile-js-2/module.js b/example_problems/hello/submissions/accepted/multifile-js-2/module.js new file mode 100644 index 0000000000..94f777f1c9 --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js-2/module.js @@ -0,0 +1,4 @@ +// Extra included file with JS code +export function hello() { + return "Hello world!"; +} diff --git a/example_problems/hello/submissions/accepted/multifile-js-2/package.json b/example_problems/hello/submissions/accepted/multifile-js-2/package.json new file mode 100644 index 0000000000..6990891ff3 --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js-2/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/example_problems/hello/submissions/accepted/multifile-js/main.mjs b/example_problems/hello/submissions/accepted/multifile-js/main.mjs new file mode 100644 index 0000000000..a02377b4ec --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js/main.mjs @@ -0,0 +1,7 @@ +// This should give CORRECT on the default problem 'hello'. +// It does include another file for the actual implementation +// +// @EXPECTED_RESULTS@: CORRECT + +import { hello } from './module.mjs'; +console.log(hello()); diff --git a/example_problems/hello/submissions/accepted/multifile-js/module.mjs b/example_problems/hello/submissions/accepted/multifile-js/module.mjs new file mode 100644 index 0000000000..94f777f1c9 --- /dev/null +++ b/example_problems/hello/submissions/accepted/multifile-js/module.mjs @@ -0,0 +1,4 @@ +// Extra included file with JS code +export function hello() { + return "Hello world!"; +} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-2/hello.js b/example_problems/hello/submissions/compiler_error/multifile-js-2/hello.js new file mode 100644 index 0000000000..5f9e2f846f --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-2/hello.js @@ -0,0 +1,7 @@ +// This should give COMPILER-ERROR on the default problem 'hello'. +// The console.log is missing a `)`. +// +// @EXPECTED_RESULTS@: COMPILER-ERROR + +import { hello } from './module.js'; +console.log(hello(); diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-2/module.js b/example_problems/hello/submissions/compiler_error/multifile-js-2/module.js new file mode 100644 index 0000000000..94f777f1c9 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-2/module.js @@ -0,0 +1,4 @@ +// Extra included file with JS code +export function hello() { + return "Hello world!"; +} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-2/package.json b/example_problems/hello/submissions/compiler_error/multifile-js-2/package.json new file mode 100644 index 0000000000..6990891ff3 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-2/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-3/hello.js b/example_problems/hello/submissions/compiler_error/multifile-js-3/hello.js new file mode 100644 index 0000000000..901213d7f9 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-3/hello.js @@ -0,0 +1,9 @@ +// This should give CORRECT on the default problem 'hello'. +// The submission includes another file with invalid syntax +// which is never used. As we check all files for syntax we +// also check the unused but invalid other file resulting in +// the (unneeded) error. +// +// @EXPECTED_RESULTS@: COMPILER-ERROR + +console.log("Hello world!"); diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-3/module.js b/example_problems/hello/submissions/compiler_error/multifile-js-3/module.js new file mode 100644 index 0000000000..f88727f2c9 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-3/module.js @@ -0,0 +1,6 @@ +// Extra included file with (invalid) JS code. +// Invalid syntax on import instead of export. +// The file itself is never used in the submission. +import function hello() { + return "Hello world!" +} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js-3/package.json b/example_problems/hello/submissions/compiler_error/multifile-js-3/package.json new file mode 100644 index 0000000000..6990891ff3 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js-3/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js/hello.js b/example_problems/hello/submissions/compiler_error/multifile-js/hello.js new file mode 100644 index 0000000000..9d4cc4d81c --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js/hello.js @@ -0,0 +1,7 @@ +// This should give COMPILER-ERROR on the default problem 'hello'. +// The included file is invalid syntax. +// +// @EXPECTED_RESULTS@: COMPILER-ERROR + +import { hello } from './module.js'; +console.log(hello()); diff --git a/example_problems/hello/submissions/compiler_error/multifile-js/module.js b/example_problems/hello/submissions/compiler_error/multifile-js/module.js new file mode 100644 index 0000000000..1059b80c1c --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js/module.js @@ -0,0 +1,6 @@ +// Extra included file with JS code +// See the misspelling of let into letter. +export function hello() { + letter unused = 1 + return "Hello world!" +} diff --git a/example_problems/hello/submissions/compiler_error/multifile-js/package.json b/example_problems/hello/submissions/compiler_error/multifile-js/package.json new file mode 100644 index 0000000000..6990891ff3 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/multifile-js/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/example_problems/hello/submissions/compiler_error/test-syntax-error.R b/example_problems/hello/submissions/compiler_error/test-syntax-error.R new file mode 100644 index 0000000000..dbb00f4159 --- /dev/null +++ b/example_problems/hello/submissions/compiler_error/test-syntax-error.R @@ -0,0 +1,5 @@ +# This should give COMPILER-ERROR on the default problem 'hello'. +# +# @EXPECTED_RESULTS@: COMPILER-ERROR + +cat("Missing closing bracket!\n" diff --git a/example_problems/hello/submissions/run_time_error/multifile-js/hello.js b/example_problems/hello/submissions/run_time_error/multifile-js/hello.js new file mode 100644 index 0000000000..efebb3d22c --- /dev/null +++ b/example_problems/hello/submissions/run_time_error/multifile-js/hello.js @@ -0,0 +1,8 @@ +// This should give RUN-ERROR on the default problem 'hello'. +// It tries to include another file for the actual implementation +// with the wrong extension. +// +// @EXPECTED_RESULTS@: RUN-ERROR + +import { hello } from './module.mjs'; +console.log(hello()); diff --git a/example_problems/hello/submissions/run_time_error/multifile-js/module.js b/example_problems/hello/submissions/run_time_error/multifile-js/module.js new file mode 100644 index 0000000000..94f777f1c9 --- /dev/null +++ b/example_problems/hello/submissions/run_time_error/multifile-js/module.js @@ -0,0 +1,4 @@ +// Extra included file with JS code +export function hello() { + return "Hello world!"; +} diff --git a/example_problems/hello/submissions/run_time_error/multifile-js/package.json b/example_problems/hello/submissions/run_time_error/multifile-js/package.json new file mode 100644 index 0000000000..6990891ff3 --- /dev/null +++ b/example_problems/hello/submissions/run_time_error/multifile-js/package.json @@ -0,0 +1 @@ +{"type": "module"} diff --git a/gitlab/base.sh b/gitlab/base.sh index 00cdfb3576..a07f026d5b 100755 --- a/gitlab/base.sh +++ b/gitlab/base.sh @@ -48,21 +48,24 @@ parameters: domjudge.webappdir: /webapp domjudge.libdir: /lib domjudge.sqldir: /sql - domjudge.libvendordir: /lib/vendor + domjudge.vendordir: /webapp/vendor domjudge.logdir: /output/log domjudge.rundir: /output/run domjudge.tmpdir: /output/tmp domjudge.baseurl: http://localhost/domjudge EOF +# Composer steps +cd webapp # install check if the cache might be dirty set +e -composer install --no-scripts || rm -rf lib/vendor +composer install --no-scripts || rm -rf vendor set -e # install all php dependencies composer install --no-scripts echo -e "\033[0m" +cd $DIR # configure, make and install (but skip documentation) make configure diff --git a/gitlab/ci/integration.yml b/gitlab/ci/integration.yml index 6996ceca18..d4152ab687 100644 --- a/gitlab/ci/integration.yml +++ b/gitlab/ci/integration.yml @@ -3,9 +3,9 @@ stage: integration script: - set -eux - - if [ -z ${PHPVERSION+x} ]; then export PHPVERSION=8.1; fi + - if [ -z ${PHPVERSION+x} ]; then export PHPVERSION=8.3; fi - if [ "$TEST" = "E2E" ]; then exit 0; fi - - if [ "$CRAWL_DATASOURCES" != "0" ]; then exit 0; fi + - if [ "$CRAWL_SHADOW_MODE" != "0" ]; then exit 0; fi - timeout --signal=15 40m ./gitlab/integration.sh $PHPVERSION artifacts: when: always @@ -29,7 +29,7 @@ integration_mysql: MYSQL_REQUIRE_PRIMARY_KEY: 1 PIN_JUDGEDAEMON: 1 TEST: "Unit" - CRAWL_DATASOURCES: "0" + CRAWL_SHADOW_MODE: "0" integration_mariadb_pr: except: @@ -61,4 +61,4 @@ integration_unpinned_judgehost: MARIADB_PORT_3306_TCP_ADDR: sqlserver PIN_JUDGEDAEMON: 0 TEST: "Unit" - CRAWL_DATASOURCES: "0" + CRAWL_SHADOW_MODE: "0" diff --git a/gitlab/ci/template.yml b/gitlab/ci/template.yml index 902599d6e7..86521a2f22 100644 --- a/gitlab/ci/template.yml +++ b/gitlab/ci/template.yml @@ -27,16 +27,15 @@ .cached_vendor: extends: [.clean_ordering] cache: - key: libvendor-260522 + key: webappvendor-20240623 paths: - - lib/vendor/ + - webapp/vendor/ .mysql_job: script: - /bin/true services: - name: mysql - command: ["--default-authentication-plugin=mysql_native_password"] alias: sqlserver .mariadb_job: @@ -51,15 +50,15 @@ - /bin/true parallel: matrix: - - PHPVERSION: ["8.1","8.2"] + - PHPVERSION: ["8.1","8.2","8.3"] TEST: ["E2E","Unit"] - CRAWL_DATASOURCES: ["0","1","2"] + CRAWL_SHADOW_MODE: ["0","1"] .phpsupported_job_pr: script: - /bin/true parallel: matrix: - - PHPVERSION: ["8.2"] + - PHPVERSION: ["8.3"] TEST: ["E2E","Unit"] - CRAWL_DATASOURCES: ["0"] + CRAWL_SHADOW_MODE: ["0"] diff --git a/gitlab/ci/unit.yml b/gitlab/ci/unit.yml index 1d0eea3d70..d9a91557b1 100644 --- a/gitlab/ci/unit.yml +++ b/gitlab/ci/unit.yml @@ -12,9 +12,9 @@ - set -eux - if [ -z ${PHPVERSION+x} ]; then export PHPVERSION=8.1; fi - if [ -z ${TEST+x} ]; then export TEST="UNIT"; fi - - if [ "$TEST" = "UNIT" ] && [ "$CRAWL_DATASOURCES" != "0" ]; then exit 0; fi - - if [ "$TEST" = "E2E" ] && [ "$CRAWL_DATASOURCES" != "0" ] && [ "$CI_COMMIT_BRANCH" != "main" ]; then exit 0; fi - - export CRAWL_DATASOURCES + - if [ "$TEST" = "UNIT" ] && [ "$CRAWL_SHADOW_MODE" != "0" ]; then exit 0; fi + - if [ "$TEST" = "E2E" ] && [ "$CRAWL_SHADOW_MODE" != "0" ] && [ "$CI_COMMIT_BRANCH" != "main" ]; then exit 0; fi + - export CRAWL_SHADOW_MODE - ./gitlab/unit-tests.sh $PHPVERSION $TEST artifacts: when: always @@ -48,4 +48,4 @@ run unit tests (MySQL): parallel: matrix: - TEST: ["E2E","Unit"] - CRAWL_DATASOURCES: ["0"] + CRAWL_SHADOW_MODE: ["0"] diff --git a/gitlab/integration.sh b/gitlab/integration.sh index 946123bd58..a4678e19ea 100755 --- a/gitlab/integration.sh +++ b/gitlab/integration.sh @@ -81,11 +81,25 @@ mount mount -o remount,exec,dev /builds section_end mount +section_start check_cgroup_v1 "Checking for cgroup v1 availability" +grep cgroup$ /proc/filesystems +if [ $? -eq 0 ]; then + cgroupv1=1 +else + echo "Skipping tests that rely on cgroup v1" + cgroupv1=0 +fi +section_end check_cgroup_v1 + section_start judgehost "Configure judgehost" cd /opt/domjudge/judgehost/ sudo cp /opt/domjudge/judgehost/etc/sudoers-domjudge /etc/sudoers.d/ sudo chmod 400 /etc/sudoers.d/sudoers-domjudge -sudo bin/create_cgroups +if [ $cgroupv1 -ne 0 ]; then + # We allow this to go wrong as some gitlab runners do not have the + # swapaccount kernel option set. + sudo bin/create_cgroups || cgroupv1=0 +fi if [ ! -d ${DIR}/chroot/domjudge/ ]; then cd ${DIR}/misc-tools @@ -129,19 +143,23 @@ if [ $PIN_JUDGEDAEMON -eq 1 ]; then fi section_end more_setup -section_start runguard_tests "Running isolated runguard tests" -sudo addgroup domjudge-run-0 -sudo usermod -g domjudge-run-0 domjudge-run-0 -cd ${DIR}/judge/runguard_test -make test -section_end runguard_tests +if [ $cgroupv1 -ne 0 ]; then + section_start runguard_tests "Running isolated runguard tests" + sudo addgroup domjudge-run-0 + sudo usermod -g domjudge-run-0 domjudge-run-0 + cd ${DIR}/judge/runguard_test + make test + section_end runguard_tests +fi -section_start start_judging "Start judging" -cd /opt/domjudge/judgehost/ +if [ $cgroupv1 -ne 0 ]; then + section_start start_judging "Start judging" + cd /opt/domjudge/judgehost/ -sudo -u domjudge bin/judgedaemon $PINNING |& tee /tmp/judgedaemon.log & -sleep 5 -section_end start_judging + sudo -u domjudge bin/judgedaemon $PINNING |& tee /tmp/judgedaemon.log & + sleep 5 + section_end start_judging +fi section_start submitting "Importing Kattis examples" export SUBMITBASEURL='http://localhost/domjudge/' @@ -160,13 +178,11 @@ for i in hello_kattis different guess; do cd "$i" zip -r "../${i}.zip" -- * ) - curl --fail -X POST -n -N -F zip=@${i}.zip http://localhost/domjudge/api/contests/1/problems + curl --fail -X POST -n -N -F zip=@${i}.zip http://localhost/domjudge/api/contests/demo/problems done section_end submitting -section_start judging "Waiting until all submissions are judged" -# wait for and check results -NUMSUBS=$(curl --fail http://admin:$ADMINPASS@localhost/domjudge/api/contests/1/submissions | python3 -mjson.tool | grep -c '"id":') +section_start curlcookie "Preparing cookie jar for curl" export COOKIEJAR COOKIEJAR=$(mktemp --tmpdir) export CURLOPTS="--fail -sq -m 30 -b $COOKIEJAR" @@ -180,56 +196,64 @@ curl $CURLOPTS -c $COOKIEJAR -F "_csrf_token=$CSRFTOKEN" -F "_username=admin" -F curl $CURLOPTS -F "sendto=" -F "problem=1-" -F "bodytext=Testing" -F "submit=Send" \ "http://localhost/domjudge/jury/clarifications/send" -o /dev/null -# Don't spam the log. -set +x +section_end curlcookie -while /bin/true; do - sleep 30s - curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier?verify_multiple=1" -o /dev/null +if [ $cgroupv1 -ne 0 ]; then + section_start judging "Waiting until all submissions are judged" + # wait for and check results + NUMSUBS=$(curl --fail http://admin:$ADMINPASS@localhost/domjudge/api/contests/demo/submissions | python3 -mjson.tool | grep -c '"id":') - # Check if we are done, i.e. everything is judged or something got disabled by internal error... - if tail /tmp/judgedaemon.log | grep -q "No submissions in queue"; then - break - fi - # ... or something has crashed. - if ! pgrep -f judgedaemon; then - break - fi -done + # Don't spam the log. + set +x -NUMNOTVERIFIED=$(curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "submissions checked" | sed -r 's/^.* ([0-9]+) submissions checked.*$/\1/') -NUMVERIFIED=$( curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "submissions not checked" | sed -r 's/^.* ([0-9]+) submissions not checked.*$/\1/') -NUMNOMAGIC=$( curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "without magic string" | sed -r 's/^.* ([0-9]+) without magic string.*$/\1/') -section_end judging - -# We expect -# - two submissions with ambiguous outcome, -# - no submissions without magic string, -# - and all submissions to be judged. -if [ $NUMNOTVERIFIED -ne 2 ] || [ $NUMNOMAGIC -ne 0 ] || [ $NUMSUBS -gt $((NUMVERIFIED+NUMNOTVERIFIED)) ]; then - section_start error "Short error description" - # We error out below anyway, so no need to fail earlier than that. - set +e - echo "verified subs: $NUMVERIFIED, unverified subs: $NUMNOTVERIFIED, total subs: $NUMSUBS" - echo "(expected 2 submissions to be unverified, but all to be processed)" - echo "Of these $NUMNOMAGIC do not have the EXPECTED_RESULTS string (should be 0)." - curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier?verify_multiple=1" | w3m -dump -T text/html - section_end error - - section_start logfiles "All the more or less useful logfiles" - for i in /opt/domjudge/judgehost/judgings/*/*/*/*/*/compile.out; do - echo $i; - head -n 100 $i; - dir=$(dirname $i) - if [ -r $dir/testcase001/system.out ]; then - head $dir/testcase001/system.out - head $dir/testcase001/runguard.err - head $dir/testcase001/program.err - head $dir/testcase001/program.meta + while /bin/true; do + sleep 30s + curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier?verify_multiple=1" -o /dev/null + + # Check if we are done, i.e. everything is judged or something got disabled by internal error... + if tail /tmp/judgedaemon.log | grep -q "No submissions in queue"; then + break + fi + # ... or something has crashed. + if ! pgrep -f judgedaemon; then + break fi - echo; done - exit 1; + + NUMNOTVERIFIED=$(curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "submissions checked" | sed -r 's/^.* ([0-9]+) submissions checked.*$/\1/') + NUMVERIFIED=$( curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "submissions not checked" | sed -r 's/^.* ([0-9]+) submissions not checked.*$/\1/') + NUMNOMAGIC=$( curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier" | grep "without magic string" | sed -r 's/^.* ([0-9]+) without magic string.*$/\1/') + section_end judging + + # We expect + # - two submissions with ambiguous outcome, + # - one submissions submitted through the submit client, and thus the magic string ignored, + # - and all submissions to be judged. + if [ $NUMNOTVERIFIED -ne 2 ] || [ $NUMNOMAGIC -ne 1 ] || [ $NUMSUBS -gt $((NUMVERIFIED+NUMNOTVERIFIED)) ]; then + section_start error "Short error description" + # We error out below anyway, so no need to fail earlier than that. + set +e + echo "verified subs: $NUMVERIFIED, unverified subs: $NUMNOTVERIFIED, total subs: $NUMSUBS" + echo "(expected 2 submissions to be unverified, but all to be processed)" + echo "Of these $NUMNOMAGIC do not have the EXPECTED_RESULTS string (should be 1)." + curl $CURLOPTS "http://localhost/domjudge/jury/judging-verifier?verify_multiple=1" | w3m -dump -T text/html + section_end error + + section_start logfiles "All the more or less useful logfiles" + for i in /opt/domjudge/judgehost/judgings/*/*/*/*/*/compile.out; do + echo $i; + head -n 100 $i; + dir=$(dirname $i) + if [ -r $dir/testcase001/system.out ]; then + head $dir/testcase001/system.out + head $dir/testcase001/runguard.err + head $dir/testcase001/program.err + head $dir/testcase001/program.meta + fi + echo; + done + exit 1; + fi fi section_start api_check "Performing API checks" @@ -249,7 +273,13 @@ if cat /opt/domjudge/domserver/webapp/var/log/prod.log | egrep '(CRITICAL|ERROR) fi # Check the Contest API: -$CHECK_API -n -C -e -a 'strict=1' http://admin:$ADMINPASS@localhost/domjudge/api +if [ $cgroupv1 -ne 0 ]; then + $CHECK_API -n -C -e -a 'strict=1' http://admin:$ADMINPASS@localhost/domjudge/api +else + # With cgroup v1 not being available we don't judge, so we cannot do + # consistency checks, so running the above command without -C. + $CHECK_API -n -e -a 'strict=1' http://admin:$ADMINPASS@localhost/domjudge/api +fi section_end api_check |& tee "$GITLABARTIFACTS/check_api.log" section_start validate_feed "Validate the eventfeed against API (ignoring failures)" diff --git a/gitlab/unit-tests.sh b/gitlab/unit-tests.sh index 6713d5d60b..63ae58121e 100755 --- a/gitlab/unit-tests.sh +++ b/gitlab/unit-tests.sh @@ -20,7 +20,7 @@ echo "UPDATE user SET teamid = 1 WHERE userid = 1;" | mysql domjudge_test cp webapp/.env.test /opt/domjudge/domserver/webapp/ # We also need the composer.json for PHPunit to detect the correct directory. -cp composer.json /opt/domjudge/domserver/ +cp webapp/composer.json /opt/domjudge/domserver/webapp/ cd /opt/domjudge/domserver @@ -40,7 +40,7 @@ if [ $CODECOVERAGE -eq 1 ]; then CNT=$(sed -n '/Generating code coverage report/,$p' "$GITLABARTIFACTS"/phpunit.out | grep -v DoctrineTestBundle | grep -cv ^$) FILE=deprecation.txt sed -n '/Generating code coverage report/,$p' "$GITLABARTIFACTS"/phpunit.out > ${CI_PROJECT_DIR}/$FILE - if [ $CNT -le 74 ]; then + if [ $CNT -le 32 ]; then STATE=success else STATE=failure diff --git a/gitlab/webstandard.sh b/gitlab/webstandard.sh deleted file mode 100755 index 19fc925f92..0000000000 --- a/gitlab/webstandard.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash - -. gitlab/ci_settings.sh - -section_start_collap setup "Setup and install" - -export version=8.1 - -show_phpinfo $version - -# Set up -"$( dirname "${BASH_SOURCE[0]}" )"/base.sh - -trap log_on_err ERR - -cd /opt/domjudge/domserver - -section_end setup - -section_start_collap testuser "Setup the test user" -# We're using the admin user in all possible roles -echo "DELETE FROM userrole WHERE userid=1;" | mysql domjudge -ADMINPASS=$(cat etc/initial_admin_password.secret) -export COOKIEJAR -COOKIEJAR=$(mktemp --tmpdir) -export CURLOPTS="--fail -sq -m 30 -b $COOKIEJAR" -if [ $ROLE = "public" ]; then - ADMINPASS="failedlogin" -elif [ $ROLE = "team" ]; then - # Add team to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 3);" | mysql domjudge - echo "UPDATE user SET teamid = 1 WHERE userid = 1;" | mysql domjudge -elif [ $ROLE = "jury" ]; then - # Add jury to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 2);" | mysql domjudge -elif [ $ROLE = "balloon" ]; then - # Add balloon to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 4);" | mysql domjudge -elif [ $ROLE = "admin" ]; then - # Add admin to admin user - echo "INSERT INTO userrole (userid, roleid) VALUES (1, 1);" | mysql domjudge -fi - -# Make an initial request which will get us a session id, and grab the csrf token from it -CSRFTOKEN=$(curl $CURLOPTS -c $COOKIEJAR "http://localhost/domjudge/login" 2>/dev/null | sed -n 's/.*_csrf_token.*value="\(.*\)".*/\1/p') -# Make a second request with our session + csrf token to actually log in -curl $CURLOPTS -c $COOKIEJAR -F "_csrf_token=$CSRFTOKEN" -F "_username=admin" -F "_password=$ADMINPASS" "http://localhost/domjudge/login" - -cd $DIR - -cp $COOKIEJAR cookies.txt -sed -i 's/#HttpOnly_//g' cookies.txt -sed -i 's/\t0\t/\t1999999999\t/g' cookies.txt -section_end testuser - -# Could try different entrypoints -FOUNDERR=0 -URL=public -mkdir $URL -cd $URL -cp $DIR/cookies.txt ./ -section_start_collap scrape "Scrape the site with the rebuild admin user" -set +e -wget \ --reject-regex logout \ --recursive \ --no-clobber \ --page-requisites \ --html-extension \ --convert-links \ --restrict-file-names=windows \ --domains localhost \ --no-parent \ --load-cookies cookies.txt \ http://localhost/domjudge/$URL -RET=$? -set -e -#https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html -# Exit code 4 is network error which we can ignore -if [ $RET -ne 4 ] && [ $RET -ne 0 ]; then - exit $RET -fi -section_end scrape - -if [ "$TEST" = "w3cval" ]; then - section_start_collap upstream_problems "Remove files from upstream with problems" - rm -rf localhost/domjudge/doc - rm -rf localhost/domjudge/css/fontawesome-all.min.css* - rm -rf localhost/domjudge/bundles/nelmioapidoc* - rm -f localhost/domjudge/css/bootstrap.min.css* - rm -f localhost/domjudge/css/select2-bootstrap*.css* - rm -f localhost/domjudge/css/dataTables*.css* - rm -f localhost/domjudge/jury/config/check/phpinfo* - section_end upstream_problems - - section_start_collap test_suite "Install testsuite" - cd $DIR - wget https://github.com/validator/validator/releases/latest/download/vnu.linux.zip - unzip -q vnu.linux.zip - section_end test_suite - FLTR='--filterpattern .*autocomplete.*|.*style.*' - for typ in html css svg - do - $DIR/vnu-runtime-image/bin/vnu --errors-only --exit-zero-always --skip-non-$typ --format json $FLTR $URL 2> result.json - NEWFOUNDERRORS=`$DIR/vnu-runtime-image/bin/vnu --errors-only --exit-zero-always --skip-non-$typ --format gnu $FLTR $URL 2>&1 | wc -l` - FOUNDERR=$((NEWFOUNDERRORS+FOUNDERR)) - python3 -m "json.tool" < result.json > w3c$typ$URL.json - trace_off; python3 gitlab/jsontogitlab.py w3c$typ$URL.json; trace_on - done -else - section_start_collap upstream_problems "Remove files from upstream with problems" - rm -rf localhost/domjudge/{doc,api} - section_end upstream_problems - - if [ $TEST == "axe" ]; then - STAN="-e $TEST" - FLTR="" - else - STAN="-s $TEST" - FLTR="-E '#DataTables_Table_0 > tbody > tr > td > a','#menuDefault > a','#filter-card > div > div > div > span > span:nth-child(1) > span > ul > li > input','.problem-badge'" - fi - cd $DIR - ACCEPTEDERR=5 - # shellcheck disable=SC2044,SC2035 - for file in `find $URL -name *.html` - do - section_start ${file//\//} $file - # T is reasonable amount of errors to allow to not break - su domjudge -c "/node_modules/.bin/pa11y $STAN -T $ACCEPTEDERR $FLTR --reporter json ./$file" | python3 -m json.tool - ERR=`su domjudge -c "/node_modules/.bin/pa11y $STAN -T $ACCEPTEDERR $FLTR --reporter csv ./$file" | wc -l` - FOUNDERR=$((ERR+FOUNDERR-1)) # Remove header row - section_end $file - done -fi -echo "Found: " $FOUNDERR -[ "$FOUNDERR" -eq 0 ] diff --git a/judge/Makefile b/judge/Makefile index f938dc6930..b206de0409 100644 --- a/judge/Makefile +++ b/judge/Makefile @@ -13,14 +13,12 @@ judgehost: $(TARGETS) $(SUBST_FILES) $(SUBST_FILES): %: %.in $(TOPDIR)/paths.mk $(substconfigvars) -runguard: LDFLAGS := $(filter-out -pie,$(LDFLAGS)) - -runguard: -lm $(LIBCGROUP) -runguard$(OBJEXT): $(TOPDIR)/etc/runguard-config.h - evict: evict.c $(LIBHEADERS) $(LIBSOURCES) $(CC) $(CFLAGS) -o $@ $< $(LIBSOURCES) +runguard: runguard.cc $(LIBHEADERS) $(LIBSOURCES) $(TOPDIR)/etc/runguard-config.h + $(CXX) $(CXXFLAGS) -o $@ $< $(LIBSOURCES) $(LIBCGROUP) + runpipe: runpipe.cc $(LIBHEADERS) $(LIBSOURCES) $(CXX) $(CXXFLAGS) -static -o $@ $< $(LIBSOURCES) @@ -32,11 +30,6 @@ install-judgehost: judgedaemon.main.php run-interactive.sh $(INSTALL_PROG) -t $(DESTDIR)$(judgehost_bindir) \ judgedaemon runguard runpipe create_cgroups -ifneq ($(systemd_unitdir),) - $(INSTALL_DIR) $(DESTDIR)$(systemd_unitdir) - $(INSTALL_DATA) -t $(DESTDIR)$(systemd_unitdir) \ - create-cgroups.service domjudge-judgehost.target domjudge-judgedaemon@.service -endif clean-l: -rm -f $(TARGETS) $(TARGETS:%=%$(OBJEXT)) diff --git a/judge/create_cgroups.in b/judge/create_cgroups.in index fbfe48898b..56d1338a31 100755 --- a/judge/create_cgroups.in +++ b/judge/create_cgroups.in @@ -7,7 +7,7 @@ # (hence: not the 'domjudge-run' user!) JUDGEHOSTUSER=@DOMJUDGE_USER@ -CGROUPBASE=@judgehost_cgroupdir@ +CGROUPBASE="/sys/fs/cgroup" cgroup_error_and_usage () { echo "$1" >&2 diff --git a/judge/domjudge-judgedaemon@.service.in b/judge/domjudge-judgedaemon@.service.in index 08acade799..044a321333 100644 --- a/judge/domjudge-judgedaemon@.service.in +++ b/judge/domjudge-judgedaemon@.service.in @@ -20,6 +20,9 @@ Type=simple ExecStart=@judgehost_bindir@/judgedaemon -n %i User=@DOMJUDGE_USER@ +KillSignal=SIGTERM +TimeoutStopSec=180 +FinalKillSignal=SIGKILL Restart=always RestartSec=3 diff --git a/judge/runguard.c b/judge/runguard.cc similarity index 83% rename from judge/runguard.c rename to judge/runguard.cc index 443a28b624..1dda7d9879 100644 --- a/judge/runguard.c +++ b/judge/runguard.cc @@ -32,11 +32,6 @@ /* Some system/site specific config: VALID_USERS, CHROOT_PREFIX */ #include "runguard-config.h" -/* For chroot(), which is not POSIX. */ -#define _DEFAULT_SOURCE -/* For unshare() used by cgroups. */ -#define _GNU_SOURCE - #include #include #include @@ -46,23 +41,23 @@ #include #include #include -#include +#include #include -#include -#include +#include +#include #include -#include -#include -#include +#include +#include +#include #include #include #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include @@ -100,10 +95,6 @@ const struct timespec cg_delete_delay = { 0, 10000000L }; /* 0.01 seconds */ extern int errno; -#ifndef _GNU_SOURCE -extern char **environ; -#endif - const int exit_failure = -1; char *progname; @@ -145,6 +136,9 @@ int be_verbose; int be_quiet; int show_help; int show_version; +int in_error_handling = 0; +pid_t runpipe_pid = -1; + double walltimelimit[2], cputimelimit[2]; /* in seconds, soft and hard limits */ int walllimit_reached, cpulimit_reached; /* 1=soft, 2=hard, 3=both limits reached */ @@ -159,8 +153,6 @@ pid_t child_pid = -1; static volatile sig_atomic_t received_SIGCHLD = 0; static volatile sig_atomic_t received_signal = -1; -FILE *child_stdout; -FILE *child_stderr; int child_pipefd[3][2]; int child_redirfd[3]; @@ -168,28 +160,29 @@ struct timeval progstarttime, starttime, endtime; struct tms startticks, endticks; struct option const long_opts[] = { - {"root", required_argument, NULL, 'r'}, - {"user", required_argument, NULL, 'u'}, - {"group", required_argument, NULL, 'g'}, - {"chdir", required_argument, NULL, 'd'}, - {"walltime", required_argument, NULL, 't'}, - {"cputime", required_argument, NULL, 'C'}, - {"memsize", required_argument, NULL, 'm'}, - {"filesize", required_argument, NULL, 'f'}, - {"nproc", required_argument, NULL, 'p'}, - {"cpuset", required_argument, NULL, 'P'}, - {"no-core", no_argument, NULL, 'c'}, - {"stdout", required_argument, NULL, 'o'}, - {"stderr", required_argument, NULL, 'e'}, - {"streamsize", required_argument, NULL, 's'}, - {"environment",no_argument, NULL, 'E'}, - {"variable", required_argument, NULL, 'V'}, - {"outmeta", required_argument, NULL, 'M'}, - {"verbose", no_argument, NULL, 'v'}, - {"quiet", no_argument, NULL, 'q'}, - {"help", no_argument, &show_help, 1 }, - {"version", no_argument, &show_version, 1 }, - { NULL, 0, NULL, 0 } + {"root", required_argument, nullptr, 'r'}, + {"user", required_argument, nullptr, 'u'}, + {"group", required_argument, nullptr, 'g'}, + {"chdir", required_argument, nullptr, 'd'}, + {"walltime", required_argument, nullptr, 't'}, + {"cputime", required_argument, nullptr, 'C'}, + {"memsize", required_argument, nullptr, 'm'}, + {"filesize", required_argument, nullptr, 'f'}, + {"nproc", required_argument, nullptr, 'p'}, + {"cpuset", required_argument, nullptr, 'P'}, + {"no-core", no_argument, nullptr, 'c'}, + {"stdout", required_argument, nullptr, 'o'}, + {"stderr", required_argument, nullptr, 'e'}, + {"streamsize", required_argument, nullptr, 's'}, + {"environment",no_argument, nullptr, 'E'}, + {"variable", required_argument, nullptr, 'V'}, + {"outmeta", required_argument, nullptr, 'M'}, + {"runpipepid", required_argument, nullptr, 'U'}, + {"verbose", no_argument, nullptr, 'v'}, + {"quiet", no_argument, nullptr, 'q'}, + {"help", no_argument, &show_help, 1 }, + {"version", no_argument, &show_version, 1 }, + { nullptr, 0, nullptr, 0 } }; void warning( const char *, ...) __attribute__((format (printf, 1, 2))); @@ -215,13 +208,12 @@ void verbose(const char *format, ...) { va_list ap; va_start(ap,format); - struct timeval currtime; - double runtime; if ( ! be_quiet && be_verbose ) { - gettimeofday(&currtime,NULL); - runtime = (currtime.tv_sec - progstarttime.tv_sec ) + - (currtime.tv_usec - progstarttime.tv_usec)*1E-6; + struct timeval currtime{}; + gettimeofday(&currtime,nullptr); + double runtime = (currtime.tv_sec - progstarttime.tv_sec ) + + (currtime.tv_usec - progstarttime.tv_usec)*1E-6; fprintf(stderr,"%s [%d @ %10.6lf]: verbose: ",progname,getpid(),runtime); vfprintf(stderr,format,ap); fprintf(stderr,"\n"); @@ -232,31 +224,33 @@ void verbose(const char *format, ...) void error(int errnum, const char *format, ...) { + // Silently ignore errors that happen while handling other errors. + if (in_error_handling) return; + in_error_handling = 1; + va_list ap; va_start(ap,format); - sigset_t sigs; - char *errstr; - int errlen, errpos; /* * Make sure the signal handler for these (terminate()) does not * interfere, we are exiting now anyway. */ + sigset_t sigs; sigaddset(&sigs, SIGALRM); sigaddset(&sigs, SIGTERM); - sigprocmask(SIG_BLOCK, &sigs, NULL); + sigprocmask(SIG_BLOCK, &sigs, nullptr); /* First print to string to be able to reuse the message. */ - errlen = strlen(progname)+255; - if ( format!=NULL ) errlen += strlen(format); + size_t errlen = strlen(progname)+255; + if ( format!=nullptr ) errlen += strlen(format); - errstr = (char *)malloc(errlen); - if ( errstr==NULL ) abort(); + char *errstr = (char *)malloc(errlen); + if ( errstr==nullptr ) abort(); sprintf(errstr,"%s",progname); - errpos = strlen(errstr); + size_t errpos = strlen(errstr); - if ( format!=NULL ) { + if ( format!=nullptr ) { snprintf(errstr+errpos,errlen-errpos,": "); errpos += 2; vsnprintf(errstr+errpos,errlen-errpos,format,ap); @@ -276,7 +270,7 @@ void error(int errnum, const char *format, ...) } errpos += strlen(errstr+errpos); } - if ( format==NULL && errnum==0 ) { + if ( format==nullptr && errnum==0 ) { snprintf(errstr+errpos,errlen-errpos,": unknown error"); } @@ -284,7 +278,7 @@ void error(int errnum, const char *format, ...) va_end(ap); write_meta("internal-error","%s",errstr); - if ( outputmeta && metafile != NULL && fclose(metafile)!=0 ) { + if ( outputmeta && metafile != nullptr && fclose(metafile)!=0 ) { fprintf(stderr,"\nError writing to metafile '%s'.\n",metafilename); } @@ -303,7 +297,7 @@ void error(int errnum, const char *format, ...) } /* Wait a while to make sure the process is killed by now. */ - nanosleep(&killdelay,NULL); + nanosleep(&killdelay,nullptr); } exit(exit_failure); @@ -311,19 +305,21 @@ void error(int errnum, const char *format, ...) void write_meta(const char *key, const char *format, ...) { - va_list ap; - if ( !outputmeta ) return; + va_list ap; va_start(ap,format); if ( fprintf(metafile,"%s: ",key)<=0 ) { + outputmeta = 0; error(0,"cannot write to file `%s'",metafilename); } if ( vfprintf(metafile,format,ap)<0 ) { + outputmeta = 0; error(0,"cannot write to file `%s'(vfprintf)",metafilename); } if ( fprintf(metafile,"\n")<=0 ) { + outputmeta = 0; error(0,"cannot write to file `%s'",metafilename); } @@ -364,8 +360,11 @@ Run COMMAND with restrictions.\n\ -e, --stderr=FILE redirect COMMAND stderr output to FILE\n\ -s, --streamsize=SIZE truncate COMMAND stdout/stderr streams at SIZE kB\n\ -E, --environment preserve environment variables (default only PATH)\n\ - -V, --variable add additional environment variables (in form KEY=VALUE;KEY2=VALUE2)\n\ - -M, --outmeta=FILE write metadata (runtime, exitcode, etc.) to FILE\n"); + -V, --variable add additional environment variables\n\ + (in form KEY=VALUE;KEY2=VALUE2)\n\ + -M, --outmeta=FILE write metadata (runtime, exitcode, etc.) to FILE\n\ + -U, --runpipepid=PID process ID of runpipe to send SIGUSR1 signal when\n\ + timelimit is reached\n"); printf("\ -v, --verbose display some extra warnings and information\n\ -q, --quiet suppress all warnings and verbose output\n\ @@ -386,10 +385,6 @@ real user ID.\n"); void output_exit_time(int exitcode, double cpudiff) { - double walldiff, userdiff, sysdiff; - int timelimit_reached = 0; - unsigned long ticks_per_second = sysconf(_SC_CLK_TCK); - verbose("command exited with exitcode %d",exitcode); write_meta("exitcode","%d",exitcode); @@ -397,11 +392,12 @@ void output_exit_time(int exitcode, double cpudiff) write_meta("signal", "%d", received_signal); } - walldiff = (endtime.tv_sec - starttime.tv_sec ) + - (endtime.tv_usec - starttime.tv_usec)*1E-6; + double walldiff = (endtime.tv_sec - starttime.tv_sec ) + + (endtime.tv_usec - starttime.tv_usec)*1E-6; - userdiff = (double)(endticks.tms_cutime - startticks.tms_cutime) / ticks_per_second; - sysdiff = (double)(endticks.tms_cstime - startticks.tms_cstime) / ticks_per_second; + unsigned long ticks_per_second = sysconf(_SC_CLK_TCK); + double userdiff = (double)(endticks.tms_cutime - startticks.tms_cutime) / ticks_per_second; + double sysdiff = (double)(endticks.tms_cstime - startticks.tms_cstime) / ticks_per_second; write_meta("wall-time","%.3f", walldiff); write_meta("user-time","%.3f", userdiff); @@ -421,6 +417,7 @@ void output_exit_time(int exitcode, double cpudiff) warning("timelimit exceeded (soft cpu time)"); } + int timelimit_reached = 0; switch ( outputtimetype ) { case WALL_TIME_TYPE: write_meta("time-used","wall-time"); @@ -444,31 +441,31 @@ void output_exit_time(int exitcode, double cpudiff) void check_remaining_procs() { - char path[1024]; - - snprintf(path, 1023, "/sys/fs/cgroup/cpuacct%scgroup.procs", cgroupname); - FILE *file = fopen(path, "r"); - if (file == NULL) { - error(errno, "opening cgroups file `%s'", path); - } - - fseek(file, 0L, SEEK_END); - if (ftell(file) > 0) { - error(0, "found left-over processes in cgroup controller, please check!"); - } + char path[1024]; + snprintf(path, 1023, "/sys/fs/cgroup/cpuacct%scgroup.procs", cgroupname); + + FILE *file = fopen(path, "r"); + if (file == nullptr) { + error(errno, "opening cgroups file `%s'", path); + } + + fseek(file, 0L, SEEK_END); + if (ftell(file) > 0) { + error(0, "found left-over processes in cgroup controller, please check!"); + } if (fclose(file) != 0) error(errno, "closing file `%s'", path); } void output_cgroup_stats(double *cputime) { - int ret; - int64_t max_usage, cpu_time_int; struct cgroup *cg; - struct cgroup_controller *cg_controller; + if ( (cg = cgroup_new_cgroup(cgroupname))==nullptr ) error(0,"cgroup_new_cgroup"); - if ( (cg = cgroup_new_cgroup(cgroupname))==NULL ) error(0,"cgroup_new_cgroup"); + int ret; if ((ret = cgroup_get_cgroup(cg)) != 0) error(ret,"get cgroup information"); + int64_t max_usage; + struct cgroup_controller *cg_controller; cg_controller = cgroup_get_controller(cg, "memory"); ret = cgroup_get_value_int64(cg_controller, "memory.memsw.max_usage_in_bytes", &max_usage); if ( ret!=0 ) error(ret,"get cgroup value memory.memsw.max_usage_in_bytes"); @@ -476,6 +473,7 @@ void output_cgroup_stats(double *cputime) verbose("total memory used: %" PRId64 " kB", max_usage/1024); write_meta("memory-bytes","%" PRId64, max_usage); + int64_t cpu_time_int; cg_controller = cgroup_get_controller(cg, "cpuacct"); ret = cgroup_get_value_int64(cg_controller, "cpuacct.usage", &cpu_time_int); if ( ret!=0 ) error(ret,"get cgroup value cpuacct.usage"); @@ -492,27 +490,26 @@ void output_cgroup_stats(double *cputime) void cgroup_create() { - int ret; struct cgroup *cg; - struct cgroup_controller *cg_controller; - cg = cgroup_new_cgroup(cgroupname); if (!cg) error(0,"cgroup_new_cgroup"); /* Set up the memory restrictions; these two options limit ram use and ram+swap use. They are the same so no swapping can occur */ - if ( (cg_controller = cgroup_add_controller(cg, "memory"))==NULL ) { + struct cgroup_controller *cg_controller; + if ( (cg_controller = cgroup_add_controller(cg, "memory"))==nullptr ) { error(0,"cgroup_add_controller memory"); } + int ret; cgroup_add_value(int64, "memory.limit_in_bytes", memsize); cgroup_add_value(int64, "memory.memsw.limit_in_bytes", memsize); /* Set up cpu restrictions; we pin the task to a specific set of cpus. We also give it exclusive access to those cores, and set no limits on memory nodes */ - if ( cpuset!=NULL && strlen(cpuset)>0 ) { - if ( (cg_controller = cgroup_add_controller(cg, "cpuset"))==NULL ) { + if ( cpuset!=nullptr && strlen(cpuset)>0 ) { + if ( (cg_controller = cgroup_add_controller(cg, "cpuset"))==nullptr ) { error(0,"cgroup_add_controller cpuset"); } /* To make a cpuset exclusive, some additional setup outside of domjudge is @@ -524,7 +521,7 @@ void cgroup_create() verbose("cpuset undefined"); } - if ( (cg_controller = cgroup_add_controller(cg, "cpuacct"))==NULL ) { + if ( (cg_controller = cgroup_add_controller(cg, "cpuacct"))==nullptr ) { error(0,"cgroup_add_controller cpuacct"); } @@ -539,12 +536,11 @@ void cgroup_create() void cgroup_attach() { - int ret; struct cgroup *cg; - cg = cgroup_new_cgroup(cgroupname); if (!cg) error(0,"cgroup_new_cgroup"); + int ret; if ( (ret = cgroup_get_cgroup(cg))!=0 ) error(ret,"get cgroup information"); /* Attach task to the cgroup */ @@ -555,13 +551,12 @@ void cgroup_attach() void cgroup_kill() { - int ret; - void *handle = NULL; + void *handle = nullptr; pid_t pid; /* kill any remaining tasks, and wait for them to be gone */ while(1) { - ret = cgroup_get_task_begin(cgroupname, "memory", &handle, &pid); + int ret = cgroup_get_task_begin(cgroupname, "memory", &handle, &pid); cgroup_get_task_end(&handle); if (ret == ECGEOF) break; kill(pid, SIGKILL); @@ -570,21 +565,19 @@ void cgroup_kill() void cgroup_delete() { - int ret; struct cgroup *cg; - cg = cgroup_new_cgroup(cgroupname); if (!cg) error(0,"cgroup_new_cgroup"); - if ( cgroup_add_controller(cg, "cpuacct")==NULL ) error(0,"cgroup_add_controller cpuacct"); - if ( cgroup_add_controller(cg, "memory")==NULL ) error(0,"cgroup_add_controller memory"); + if ( cgroup_add_controller(cg, "cpuacct")==nullptr ) error(0,"cgroup_add_controller cpuacct"); + if ( cgroup_add_controller(cg, "memory")==nullptr ) error(0,"cgroup_add_controller memory"); - if ( cpuset!=NULL && strlen(cpuset)>0 ) { - if ( cgroup_add_controller(cg, "cpuset")==NULL ) error(0,"cgroup_add_controller cpuset"); + if ( cpuset!=nullptr && strlen(cpuset)>0 ) { + if ( cgroup_add_controller(cg, "cpuset")==nullptr ) error(0,"cgroup_add_controller cpuset"); } /* Clean up our cgroup */ - nanosleep(&cg_delete_delay,NULL); - ret = cgroup_delete_cgroup_ext(cg, CGFLAG_DELETE_IGNORE_MIGRATION | CGFLAG_DELETE_RECURSIVE); + nanosleep(&cg_delete_delay,nullptr); + int ret = cgroup_delete_cgroup_ext(cg, CGFLAG_DELETE_IGNORE_MIGRATION | CGFLAG_DELETE_RECURSIVE); if ( ret!=0 ) error(ret,"deleting cgroup"); cgroup_free(&cg); @@ -602,14 +595,19 @@ void terminate(int sig) if ( sigemptyset(&sigact.sa_mask)!=0 ) { warning("could not initialize signal mask"); } - if ( sigaction(SIGTERM,&sigact,NULL)!=0 ) { + if ( sigaction(SIGTERM,&sigact,nullptr)!=0 ) { warning("could not restore signal handler"); } - if ( sigaction(SIGALRM,&sigact,NULL)!=0 ) { + if ( sigaction(SIGALRM,&sigact,nullptr)!=0 ) { warning("could not restore signal handler"); } if ( sig==SIGALRM ) { + if (runpipe_pid > 0) { + warning("sending SIGUSR1 to runpipe with pid %d", runpipe_pid); + kill(runpipe_pid, SIGUSR1); + } + walllimit_reached |= hard_timelimit; warning("timelimit exceeded (hard wall time): aborting command"); } else { @@ -627,7 +625,7 @@ void terminate(int sig) /* Prefer nanosleep over sleep because of higher resolution and it does not interfere with signals. */ - nanosleep(&killdelay,NULL); + nanosleep(&killdelay,nullptr); verbose("sending SIGKILL"); if ( kill(-child_pid,SIGKILL)!=0 && errno!=ESRCH ) { @@ -635,7 +633,7 @@ void terminate(int sig) } /* Wait another while to make sure the process is killed by now. */ - nanosleep(&killdelay,NULL); + nanosleep(&killdelay,nullptr); } static void child_handler(int sig) @@ -645,12 +643,12 @@ static void child_handler(int sig) int userid(char *name) { - struct passwd *pwd; - errno = 0; /* per the linux GETPWNAM(3) man-page */ + + struct passwd *pwd; pwd = getpwnam(name); - if ( pwd==NULL || errno ) return -1; + if ( pwd==nullptr || errno ) return -1; return (int) pwd->pw_uid; } @@ -662,17 +660,17 @@ int groupid(char *name) errno = 0; /* per the linux GETGRNAM(3) man-page */ grp = getgrnam(name); - if ( grp==NULL || errno ) return -1; + if ( grp==nullptr || errno ) return -1; return (int) grp->gr_gid; } long read_optarg_int(const char *desc, long minval, long maxval) { - long arg; char *ptr; - arg = strtol(optarg,&ptr,10); + errno = 0; + long arg = strtol(optarg,&ptr,10); if ( errno || *ptr!='\0' || argmaxval ) { error(errno,"invalid %s specified: `%s'",desc,optarg); } @@ -682,22 +680,25 @@ long read_optarg_int(const char *desc, long minval, long maxval) void read_optarg_time(const char *desc, double *times) { - char *optcopy, *ptr, *sep; - - if ( (optcopy=strdup(optarg))==NULL ) error(0,"strdup() failed"); + char *optcopy; + if ( (optcopy=strdup(optarg))==nullptr ) error(0,"strdup() failed"); /* Check for soft:hard limit separator and cut string. */ - if ( (sep=strchr(optcopy,':'))!=NULL ) *sep = 0; + char *sep; + if ( (sep=strchr(optcopy,':'))!=nullptr ) *sep = 0; + char *ptr; + errno = 0; times[0] = strtod(optcopy,&ptr); if ( errno || *ptr!='\0' || !finite(times[0]) || times[0]<=0 ) { error(errno,"invalid %s specified: `%s'",desc,optarg); } /* And repeat for hard limit if we found the ':' separator. */ - if ( sep!=NULL ) { + if ( sep!=nullptr ) { + errno = 0; times[1] = strtod(sep+1,&ptr); - if ( errno || *ptr!='\0' || !finite(times[1]) || times[1]<=0 ) { + if ( errno || *(sep+1)=='\0' || *ptr!='\0' || !finite(times[1]) || times[1]<=0 ) { error(errno,"invalid %s specified: `%s'",desc,optarg); } if ( times[1]0 ) { + if ( cpuset!=nullptr && strlen(cpuset)>0 ) { int ret = strtol(cpuset, &ptr, 10); /* check if input is only a single integer */ if ( *ptr == '\0' ) { @@ -1181,7 +1179,7 @@ int main(int argc, char **argv) /* Define the cgroup name that we will use and make sure it will * be unique. Note: group names must have slashes! */ - if ( cpuset!=NULL && strlen(cpuset)>0 ) { + if ( cpuset!=nullptr && strlen(cpuset)>0 ) { strncpy(str, cpuset, 16); } else { str[0] = 0; @@ -1200,10 +1198,11 @@ int main(int argc, char **argv) * processes, and at least older versions of sshd seemed to set * it, leading to processes getting a timelimit instead of memory * exceeded, when running via SSH. */ - fp = NULL; + FILE *fp = nullptr; + char *oom_path; if ( !fp && (fp = fopen(OOM_PATH_NEW,"r+")) ) oom_path = strdup(OOM_PATH_NEW); if ( !fp && (fp = fopen(OOM_PATH_OLD,"r+")) ) oom_path = strdup(OOM_PATH_OLD); - if ( fp!=NULL ) { + if ( fp!=nullptr ) { if ( fscanf(fp,"%d",&ret)!=1 ) error(errno,"cannot read from `%s'",oom_path); if ( ret<0 ) { verbose("resetting `%s' from %d to %d",oom_path,ret,OOM_RESET_VALUE); @@ -1226,7 +1225,7 @@ int main(int argc, char **argv) /* Connect pipes to command (stdin/)stdout/stderr and close * unneeded fd's. Do this after setting restrictions to let * any messages not go to command stderr pipe. */ - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { if ( dup2(child_pipefd[i][PIPE_IN],i)<0 ) { error(errno,"redirecting child fd %d",i); } @@ -1258,17 +1257,17 @@ int main(int argc, char **argv) verbose("watchdog using user ID `%d'",getuid()); } - if ( gettimeofday(&starttime,NULL) ) error(errno,"getting time"); + if ( gettimeofday(&starttime,nullptr) ) error(errno,"getting time"); /* Close unused file descriptors */ - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { if ( close(child_pipefd[i][PIPE_IN])!=0 ) { error(errno,"closing pipe for fd %i",i); } } /* Redirect child stdout/stderr to file */ - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { child_redirfd[i] = i; /* Default: no redirects */ data_read[i] = data_passed[i] = 0; /* Reset data counters */ } @@ -1291,7 +1290,7 @@ int main(int argc, char **argv) /* Construct one-time signal handler to terminate() for TERM and ALRM signals. */ - sigmask = emptymask; + sigset_t sigmask = emptymask; if ( sigaddset(&sigmask,SIGALRM)!=0 || sigaddset(&sigmask,SIGTERM)!=0 ) error(errno,"setting signal mask"); @@ -1300,13 +1299,13 @@ int main(int argc, char **argv) sigact.sa_mask = sigmask; /* Kill child command when we receive SIGTERM */ - if ( sigaction(SIGTERM,&sigact,NULL)!=0 ) { + if ( sigaction(SIGTERM,&sigact,nullptr)!=0 ) { error(errno,"installing signal handler"); } if ( use_walltime ) { /* Kill child when we receive SIGALRM */ - if ( sigaction(SIGALRM,&sigact,NULL)!=0 ) { + if ( sigaction(SIGALRM,&sigact,nullptr)!=0 ) { error(errno,"installing signal handler"); } @@ -1316,7 +1315,7 @@ int main(int argc, char **argv) itimer.it_value.tv_sec = (int) walltimelimit[1]; itimer.it_value.tv_usec = (int)(modf(walltimelimit[1],&tmpd) * 1E6); - if ( setitimer(ITIMER_REAL,&itimer,NULL)!=0 ) { + if ( setitimer(ITIMER_REAL,&itimer,nullptr)!=0 ) { error(errno,"setting timer"); } verbose("setting hard wall-time limit to %.3f seconds",walltimelimit[1]); @@ -1329,27 +1328,29 @@ int main(int argc, char **argv) /* Wait for child data or exit. Initialize status here to quelch clang++ warning about uninitialized value; it is set by the wait() call. */ - status = 0; + int status = 0; /* We start using splice() to copy data from child to parent I/O file descriptors. If that fails (not all I/O source - dest combinations support it), then we revert to using read()/write(). */ use_splice = 1; + fd_set readfds; while ( 1 ) { FD_ZERO(&readfds); - nfds = -1; - for(i=1; i<=2; i++) { + int nfds = -1; + for(int i=1; i<=2; i++) { if ( child_pipefd[i][PIPE_OUT]>=0 ) { FD_SET(child_pipefd[i][PIPE_OUT],&readfds); nfds = max(nfds,child_pipefd[i][PIPE_OUT]); } } - r = pselect(nfds+1, &readfds, NULL, NULL, NULL, &emptymask); + int r = pselect(nfds+1, &readfds, nullptr, NULL, NULL, &emptymask); if ( r==-1 && errno!=EINTR ) error(errno,"waiting for child data"); if ( received_SIGCHLD || received_signal == SIGALRM ) { + pid_t pid; if ( (pid = wait(&status))<0 ) error(errno,"waiting on child"); if ( pid==child_pid ) break; } @@ -1359,10 +1360,10 @@ int main(int argc, char **argv) /* Reset pipe filedescriptors to use blocking I/O. */ FD_ZERO(&readfds); - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { if ( child_pipefd[i][PIPE_OUT]>=0 ) { FD_SET(child_pipefd[i][PIPE_OUT],&readfds); - r = fcntl(child_pipefd[i][PIPE_OUT], F_GETFL); + int r = fcntl(child_pipefd[i][PIPE_OUT], F_GETFL); if (r == -1) { error(errno, "fcntl, getting flags"); } @@ -1379,7 +1380,7 @@ int main(int argc, char **argv) } while ( data_passed[1] + data_passed[2] > total_data ); /* Close the output files */ - for(i=1; i<=2; i++) { + for(int i=1; i<=2; i++) { ret = close(child_redirfd[i]); if( ret!=0 ) error(errno,"closing output fd %d", i); } @@ -1388,10 +1389,10 @@ int main(int argc, char **argv) error(errno,"getting end clock ticks"); } - if ( gettimeofday(&endtime,NULL) ) error(errno,"getting time"); + if ( gettimeofday(&endtime,nullptr) ) error(errno,"getting time"); /* Test whether command has finished abnormally */ - exitcode = 0; + int exitcode = 0; if ( ! WIFEXITED(status) ) { if ( WIFSIGNALED(status) ) { if ( WTERMSIG(status)==SIGXCPU ) { @@ -1422,7 +1423,7 @@ int main(int argc, char **argv) itimer.it_value.tv_sec = 0; itimer.it_value.tv_usec = 0; - if ( setitimer(ITIMER_REAL,&itimer,NULL)!=0 ) { + if ( setitimer(ITIMER_REAL,&itimer,nullptr)!=0 ) { error(errno,"disarming timer"); } } diff --git a/judge/runpipe.cc b/judge/runpipe.cc index 17a0b8cb64..928be2d622 100644 --- a/judge/runpipe.cc +++ b/judge/runpipe.cc @@ -39,6 +39,9 @@ // emulate it by sending a message in the SIGCHLD signal handler using an extra // pipe. // +// Additionally, we set up a signal handler for SIGUSR1 which runguard will use +// to indicate when it killed its child process due to a time limit. +// // If the proxy is not enabled (i.e. no traffic capturing), the pipes are setup // like this: // @@ -46,6 +49,7 @@ // stdout -----> stdin // stdin <----- stdout // SIGCHLD -----------------> epoll +// SIGUSR1 -----------------> epoll // // // If the proxy is enabled the pipes are setup like this: @@ -54,6 +58,7 @@ // stdout -----> epoll -----> stdin // stdin <----- epoll <----- stdout // SIGCHLD ----------^ +// SIGUSR1 ----------^ #include "config.h" @@ -234,11 +239,20 @@ struct process_t { void spawn() { fd_t stdio[3] = {stdin_fd, stdout_fd, FDREDIR_NONE}; - vector argv(args.size()); + char pid_buf[12]; + vector argv; for (size_t i = 0; i < args.size(); i++) { - argv[i] = args[i].c_str(); + argv.push_back(args[i].c_str()); + if (i == 1 && cmd == "sudo" && + args[i].find("/runguard") != string::npos) { + // This is a hack, and can be improved significantly after implementing + // https://docs.google.com/document/d/1WZRwdvJUamsczYC7CpP3ZIBU8xG6wNqYqrNJf7osxYs/edit#heading=h.i7kgdnmw8qd7 + argv.push_back("-U"); + sprintf(pid_buf, "%d", getpid()); + argv.push_back(pid_buf); + } } - pid = execute(cmd.c_str(), argv.data(), args.size(), stdio, 0); + pid = execute(cmd.c_str(), argv.data(), argv.size(), stdio, 0); if (pid < 0) { error(errno, "failed to execute command #%ld", index); } @@ -398,9 +412,15 @@ struct state_t { // The PID of the first process that exited. pid_t first_process_exit_id = -1; + // Child indicated TLE. + bool child_indicated_timelimit = false; + // The pipe from which the events about the child exits can be read. The // events are writted by the SIGCHLD handler. fd_t child_exited_pipe = -1; + // The pipe from which the events about the child time limits can be read. The + // events are writted by the SIGUSR1 handler. + fd_t child_timelimit_pipe = -1; // The file descriptor of the epoll. fd_t epoll_fd = -1; @@ -572,7 +592,7 @@ struct state_t { } } - // Install an handler for the SIGCHLD signal. The handler will send a byte to + // Install a handler for the SIGCHLD signal. The handler will send a byte to // a pipe notifying the main loop that a child exited. // This method can be called only once. void install_sigchld_handler() { @@ -597,7 +617,7 @@ struct state_t { signal(SIGCHLD, [](int) { // TODO: Decide whether to keep some logging as the line below. We can't // use logmsg here since that will in turn call syslog which is not safe - // to do in a signal handler (see also `man signl-safety`). + // to do in a signal handler (see also `man signal-safety`). // logmsg(LOG_DEBUG, "caught SIGCHLD signal"); // Notify the main loop that a child exited by sending a message via @@ -611,6 +631,46 @@ struct state_t { child_exited_pipe = read_end; } + // Install a handler for the SIGUSR1 signal. The handler will send a byte to + // a pipe notifying the main loop that the child was terminated due to time limit. + // This method can be called only once. + // TODO: Refactor code to avoid code duplication with install_sigchld_handler. + void install_sigusr1_handler() { + fd_t fds[2]; + if (pipe2(fds, O_CLOEXEC | O_NONBLOCK)) { + error(errno, "creating exit pipes"); + } + + // The lambda below cannot capture anything, otherwise it couldn't be made + // into a function pointer. Therefore the write_end must have a static + // lifetime. + fd_t read_end = fds[0]; + static fd_t write_end = -1; + if (write_end != -1) { + error(0, "install_sigchld_handler can be called only once"); + } + write_end = fds[1]; + + logmsg(LOG_DEBUG, "exit handler will send event using %d -> %d", write_end, + read_end); + + signal(SIGUSR1, [](int) { + // TODO: Decide whether to keep some logging as the line below. We can't + // use logmsg here since that will in turn call syslog which is not safe + // to do in a signal handler (see also `man signal-safety`). + // logmsg(LOG_DEBUG, "caught SIGUSR1 signal"); + + // Notify the main loop that a child was terminated due to time limit by sending a message via + // child_timelimit_pipe. + static char buf[] = {42}; + if (write(write_end, buf, 1) != 1) { + error(errno, "failed to notify child exit"); + } + }); + + child_timelimit_pipe = read_end; + } + // Create the pipes used for the process communication, including the ones for // the proxy, if enabled. void setup_pipes() { @@ -685,6 +745,12 @@ struct state_t { } add_fd(child_exited_pipe); + // Always listen for child timelimit events. + if (child_timelimit_pipe == -1) { + error(0, "SIGUSR1 handler not installed"); + } + add_fd(child_timelimit_pipe); + // Listen for incoming data only when proxy is enabled. if (has_proxy()) { for (auto &proc : processes) { @@ -719,7 +785,8 @@ struct state_t { logmsg(LOG_DEBUG, "child with pid %d exited", pid); - if (first_process_exit_id == -1) { + // Only set the first process if runguard didn't tell us about a TLE. + if (first_process_exit_id == -1 && !child_indicated_timelimit) { first_process_exit_id = pid; } @@ -846,6 +913,19 @@ struct state_t { } continue; } + if (fd == child_timelimit_pipe) { + static char buffer[1]; + if (read(child_timelimit_pipe, buffer, 1) != 1) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + error(errno, "failed to read from tle pipe"); + } + } else if (buffer[0] == 42) { + logmsg(LOG_WARNING, "child indicated TLE"); + child_indicated_timelimit = true; + continue; + } + } + // A process wrote in one of the pipes to the proxy. for (size_t i = 0; i < processes.size(); i++) { @@ -904,6 +984,7 @@ int main(int argc, char **argv) { signal(SIGPIPE, SIG_IGN); state.install_sigterm_handler(); state.install_sigchld_handler(); + state.install_sigusr1_handler(); state.setup_pipes(); for (auto &proc : state.processes) { proc.spawn(); diff --git a/judge/testcase_run.sh b/judge/testcase_run.sh index 0f83e1f7ec..64cdfa5157 100755 --- a/judge/testcase_run.sh +++ b/judge/testcase_run.sh @@ -212,6 +212,12 @@ runcheck "$RUN_SCRIPT" $RUNARGS \ --stderr=program.err --outmeta=program.meta -- \ "$PREFIX/$PROGRAM" 2>runguard.err +if [ "$CREATE_WRITABLE_TEMP_DIR" ]; then + # Revoke access to the TMPDIR as security measure + chown -R "$(id -un):" "$TMPDIR" + chmod -R go= "$TMPDIR" +fi + if [ $COMBINED_RUN_COMPARE -eq 0 ]; then # We first compare the output, so that even if the submission gets a # timelimit exceeded or runtime error verdict later, the jury can diff --git a/lib/.gitignore b/lib/.gitignore index 7963ca6c0d..c585ac7609 100644 --- a/lib/.gitignore +++ b/lib/.gitignore @@ -1,4 +1,3 @@ /judge /submit -/vendor /dj_utils.py diff --git a/lib/Makefile b/lib/Makefile index 5f2e2dea76..238684c01f 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -1,6 +1,7 @@ ifndef TOPDIR TOPDIR=.. endif + include $(TOPDIR)/Makefile.global OBJECTS = $(addsuffix $(OBJEXT),lib.error lib.misc) @@ -12,21 +13,9 @@ $(OBJECTS): %$(OBJEXT): %.c %.h clean-l: rm -f $(OBJECTS) -# Change baseDir in composer autogenerated files -define fix_composer_paths - for file in autoload_psr4.php autoload_classmap.php autoload_files.php autoload_namespaces.php ; do \ - sed -i "s#^\$$baseDir = .*#\$$baseDir = dirname('$(domserver_webappdir)');#" $(1)/composer/$$file ; \ - done - sed -i "s#__DIR__ \. '/\.\./\.\./\.\.' \. '/webapp#'$(domserver_webappdir)#" $(1)/composer/autoload_static.php -endef - install-domserver: $(INSTALL_DATA) -t $(DESTDIR)$(domserver_libdir) *.php $(INSTALL_PROG) -t $(DESTDIR)$(domserver_libdir) alert - for i in vendor/* ; do \ - $(call install_tree,$(DESTDIR)$(domserver_libvendordir),$$i) ; \ - done - $(call fix_composer_paths,$(DESTDIR)$(domserver_libvendordir)) install-judgehost: $(INSTALL_DATA) -t $(DESTDIR)$(judgehost_libdir) *.php *.sh diff --git a/lib/lib.misc.php b/lib/lib.misc.php index c9a68ee943..33ffcb7cb5 100644 --- a/lib/lib.misc.php +++ b/lib/lib.misc.php @@ -75,7 +75,8 @@ function alert(string $msgtype, string $description = '') } /** - * Functions to support graceful shutdown of daemons upon receiving a signal + * Functions to support (graceful) shutdown of daemons upon receiving a + * signal. */ function sig_handler(int $signal, $siginfo = null) { @@ -85,10 +86,11 @@ function sig_handler(int $signal, $siginfo = null) switch ($signal) { case SIGHUP: - $gracefulexitsignalled = true; - // no break case SIGINT: # Ctrl+C case SIGTERM: + $gracefulexitsignalled = true; + // no break + case SIGQUIT: # Ctrl+/ $exitsignalled = true; } } @@ -106,12 +108,13 @@ function initsignals() logmsg(LOG_DEBUG, "Installing signal handlers"); - // Install signal handler for TERMINATE, HANGUP and INTERRUPT - // signals. The sleep() call will automatically return on - // receiving a signal. - pcntl_signal(SIGTERM, "sig_handler"); + // Install signal handler for HANGUP, INTERRUPT, QUIT and TERMINATE + // signals. All but the QUIT signal should trigger a graceful shutdown. + // The sleep() call will automatically return on receiving a signal. pcntl_signal(SIGHUP, "sig_handler"); pcntl_signal(SIGINT, "sig_handler"); + pcntl_signal(SIGQUIT, "sig_handler"); + pcntl_signal(SIGTERM, "sig_handler"); } /** diff --git a/misc-tools/configure-domjudge.in b/misc-tools/configure-domjudge.in index 756453e497..360bb8e163 100755 --- a/misc-tools/configure-domjudge.in +++ b/misc-tools/configure-domjudge.in @@ -31,18 +31,22 @@ def usage(): exit(1) -def compare_configs(expected_config: Set, actual_config: Set, num_spaces=4) -> (List, Set, Set): +def compare_configs(expected_config: Set, actual_config: Set, num_spaces=4, key_mismatch_in_diff=False) -> (List, Set, Set): diffs = [] space_string = ' ' * num_spaces - for k in expected_config.keys(): - if k in actual_config and expected_config[k] != actual_config[k]: - if isinstance(expected_config[k], dict) and isinstance(actual_config[k], dict): - d, n, m = compare_configs(expected_config[k], actual_config[k], num_spaces=num_spaces+2) - if d: - diffs.append(f'{space_string}- {k}:') - diffs.extend(d) - else: - diffs.append(f'{space_string}- {k}:\n {space_string}is: {actual_config[k]}\n {space_string}new: {expected_config[k]}') + all_keys = set(expected_config.keys()) | set(actual_config.keys()) + for k in all_keys: + if k in expected_config and k in actual_config: + if expected_config[k] != actual_config[k]: + if isinstance(expected_config[k], dict) and isinstance(actual_config[k], dict): + d, _, _ = compare_configs(expected_config[k], actual_config[k], num_spaces=num_spaces+2, key_mismatch_in_diff=True) + if d: + diffs.append(f'{space_string}- {k}:') + diffs.extend(d) + else: + diffs.append(f'{space_string}- {k}:\n {space_string}is: {actual_config[k]}\n {space_string}new: {expected_config[k]}') + elif key_mismatch_in_diff: + diffs.append(f'{space_string}- {k}:\n {space_string}is: {actual_config.get(k, "")}\n {space_string}new: {expected_config.get(k, "")}') new_keys = set(expected_config.keys()).difference(set(actual_config.keys())) missing_keys = set(actual_config.keys()).difference(set(expected_config.keys())) diff --git a/misc-tools/dj_make_chroot.in b/misc-tools/dj_make_chroot.in index aa9f80e772..53b0d7c1f5 100755 --- a/misc-tools/dj_make_chroot.in +++ b/misc-tools/dj_make_chroot.in @@ -34,6 +34,11 @@ RELEASE="" DEBIAN_ARCHLIST="amd64,arm64,armel,armhf,i386,mips,mips64el,mipsel,ppc64el,s390x" UBUNTU_ARCHLIST="amd64,arm64,armhf,i386,powerpc,ppc64el,s390x" +if ! command -v lsb_release; then + echo "Please install `lsb_release`." + exit 1 +fi + # If host system is Debian or Ubuntu, use its release and architecture by default if [ "$(lsb_release -i -s || true)" = 'Debian' ] || \ [ "$(lsb_release -i -s || true)" = 'Ubuntu' ]; then diff --git a/misc-tools/import-contest.in b/misc-tools/import-contest.in index edb058852d..fbf080b5f5 100755 --- a/misc-tools/import-contest.in +++ b/misc-tools/import-contest.in @@ -108,7 +108,7 @@ def import_contest_banner(cid: str): break if banner_file: - if dj_utils.confirm(f'Import {banner_file} for contest?', False): + if dj_utils.confirm(f'Import {banner_file} for contest?', True): dj_utils.upload_file(f'contests/{cid}/banner', 'banner', banner_file) print('Contest banner imported.') else: @@ -117,7 +117,7 @@ def import_contest_banner(cid: str): def import_contest_problemset_document(cid: str): """Import the contest problemset document""" - files = ['contest.pdf', 'contest-web.pdf', 'contest.html', 'contest.txt'] + files = ['problemset.pdf', 'contest.pdf', 'contest-web.pdf', 'contest.html', 'contest.txt'] text_file = None for file in files: @@ -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 @@ -197,11 +197,6 @@ if os.path.exists('problems.yaml') or os.path.exists('problems.json') or os.path problems_file = 'problemset.yaml' dj_utils.upload_file(f'contests/{cid}/problems/add-data', 'data', problems_file) - # We might need to translate the problem external ID's into an internal ID (when we are in data source = local mode) - # For this, we get the problems from the API and create a dict with the mapping - problem_mapping = {problem['externalid']: problem['id'] - for problem in dj_utils.do_api_request(f'contests/{cid}/problems')} - if os.path.exists('problems.yaml'): with open('problems.yaml') as problemFile: problemData = yaml.safe_load(problemFile) @@ -226,11 +221,10 @@ if os.path.exists('problems.yaml') or os.path.exists('problems.json') or os.path exit(3) os.system(f'cd {problem} && zip -r \'../{problem}\' -- .timelimit *') - problem_id = problem_mapping[problem] - if ((not confirmIndividually) or dj_utils.confirm(f'Ready to import problem \'{problem}\' to probid={problem_id}. Continue?', True)): + if ((not confirmIndividually) or dj_utils.confirm(f'Ready to import problem \'{problem}\' to problem={problem}. Continue?', True)): print(f'Uploading problem \'{problem}\', please be patient, this may take a while.') response = dj_utils.upload_file( - f'contests/{cid}/problems', 'zip', f'{problem}.zip', {'problem': problem_id}) + f'contests/{cid}/problems', 'zip', f'{problem}.zip', {'problem': problem}) print(json.dumps(response, indent=4)) else: print('Skipping contest import.') diff --git a/paths.mk.in b/paths.mk.in index c0e9d4fead..be51100deb 100644 --- a/paths.mk.in +++ b/paths.mk.in @@ -86,7 +86,6 @@ domserver_etcdir = @domserver_etcdir@ domserver_webappdir = @domserver_webappdir@ domserver_sqldir = @domserver_sqldir@ domserver_libdir = @domserver_libdir@ -domserver_libvendordir = @domserver_libvendordir@ domserver_logdir = @domserver_logdir@ domserver_rundir = @domserver_rundir@ domserver_tmpdir = @domserver_tmpdir@ @@ -103,7 +102,6 @@ judgehost_rundir = @judgehost_rundir@ judgehost_tmpdir = @judgehost_tmpdir@ judgehost_judgedir = @judgehost_judgedir@ judgehost_chrootdir = @judgehost_chrootdir@ -judgehost_cgroupdir = @judgehost_cgroupdir@ domjudge_docdir = @domjudge_docdir@ @@ -112,8 +110,7 @@ systemd_unitdir = @systemd_unitdir@ # The tmpdir's are not in these lists, since they would otherwise get # their permissions overwritten in FHS install mode. domserver_dirs = $(domserver_bindir) $(domserver_etcdir) \ - $(domserver_libdir) $(domserver_libvendordir) \ - $(domserver_logdir) $(domserver_rundir) \ + $(domserver_libdir) $(domserver_logdir) $(domserver_rundir) \ $(addprefix $(domserver_webappdir)/public/images/,affiliations countries teams) \ $(domserver_exampleprobdir) $(domserver_databasedumpdir) @@ -138,7 +135,6 @@ define substconfigvars -e 's,@domserver_webappdir[@],@domserver_webappdir@,g' \ -e 's,@domserver_sqldir[@],@domserver_sqldir@,g' \ -e 's,@domserver_libdir[@],@domserver_libdir@,g' \ - -e 's,@domserver_libvendordir[@],@domserver_libvendordir@,g' \ -e 's,@domserver_logdir[@],@domserver_logdir@,g' \ -e 's,@domserver_rundir[@],@domserver_rundir@,g' \ -e 's,@domserver_tmpdir[@],@domserver_tmpdir@,g' \ @@ -153,7 +149,6 @@ define substconfigvars -e 's,@judgehost_tmpdir[@],@judgehost_tmpdir@,g' \ -e 's,@judgehost_judgedir[@],@judgehost_judgedir@,g' \ -e 's,@judgehost_chrootdir[@],@judgehost_chrootdir@,g' \ - -e 's,@judgehost_cgroupdir[@],@judgehost_cgroupdir@,g' \ -e 's,@domjudge_docdir[@],@domjudge_docdir@,g' \ -e 's,@systemd_unitdir[@],@systemd_unitdir@,g' \ -e 's,@DOMJUDGE_USER[@],@DOMJUDGE_USER@,g' \ diff --git a/sql/files/defaultdata/awk/run b/sql/files/defaultdata/awk/run index 7cdb47508a..1786816dc7 100755 --- a/sql/files/defaultdata/awk/run +++ b/sql/files/defaultdata/awk/run @@ -13,7 +13,12 @@ DEST="$1" ; shift MEMLIMIT="$1" ; shift MAINSOURCE="$1" -# There is no portable way to test the syntax of an awk script. +# Syntax check based on: https://stackoverflow.com/a/7212314 +for j in "$@" ; do + awk -f $j -- 'BEGIN { exit(0) } END { exit(0) }' + EXITCODE=$? + [ "$EXITCODE" -ne 0 ] && exit $EXITCODE +done # We construct here the list of source files to be passed to awk: FILEARGS='' diff --git a/sql/files/defaultdata/hs/run b/sql/files/defaultdata/hs/run index d48e2930eb..736de457a5 100755 --- a/sql/files/defaultdata/hs/run +++ b/sql/files/defaultdata/hs/run @@ -10,6 +10,9 @@ MAINSOURCE="$1" # Set non-existing HOME variable to make GHC program happy, see: # https://ghc.haskell.org/trac/ghc/ticket/11678 export HOME=/does/not/exist +# Allow temporary files during compilation, this directory is not +# available during the submission run. +export TMPDIR="$PWD" # Add -DONLINE_JUDGE or -DDOMJUDGE below if you want it make easier for teams # to do local debugging. diff --git a/sql/files/defaultdata/js/run b/sql/files/defaultdata/js/run index 9ee0065703..aceb0980d7 100755 --- a/sql/files/defaultdata/js/run +++ b/sql/files/defaultdata/js/run @@ -20,6 +20,15 @@ if [ -z "$ENTRY_POINT" ]; then echo "Info: detected entry_point: $MAINSOURCE" fi +# Run syntax check +for file in "$@"; do + case $file in *.js) + nodejs --check "$file" + EXITCODE="$?" + [ "$EXITCODE" -ne 0 ] && exit $EXITCODE ;; + esac +done + # Write executing script: cat > "$DEST" <&2 exit 1 fi +# Check syntax +# +# Store intermediate files in the current dir (/compile) as its only +# available during compilation step. +export TMPDIR=`pwd` +Rscript -e "parse('"$@"')" +EXITCODE=$? +[ "$EXITCODE" -ne 0 ] && exit $EXITCODE # Write executing script: cat > "$DEST" < /dev/null)) + $(error "'composer' command not found in $(PATH), install it via your package manager or https://getcomposer.org/download/") +endif +# We use --no-scripts here because at this point the autoload.php file is +# not generated yet, which is needed to run the post-install scripts. + composer $(subst 1,-q,$(QUIET)) install --prefer-dist -o -a --no-scripts --no-plugins + +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. +# We skip it if autoload_runtime.php already exists, to avoid running composer +# as root during `sudo make install-domserver`. +composer-dump-autoload: vendor/autoload_runtime.php + +vendor/autoload_runtime.php: + composer $(subst 1,-q,$(QUIET)) dump-autoload -o -a + +composer-dump-autoload-dev: + composer $(subst 1,-q,$(QUIET)) dump-autoload + +# Run Symfony in dev mode (for maintainer-mode): +.env.local: + @echo "Creating file '$@'..." + @echo "# This file was automatically created by 'make maintainer-conf' to run" > $@ + @echo "# the DOMjudge Symfony application in developer mode. Adjust as needed." >> $@ + @echo "APP_ENV=dev" >> $@ + copy-bundle-assets: # We can not use bin/console here, as when using a fakeroot, # the include paths are broken. We just copy in the data we need -rm -rf public/bundles/nelmioapidoc mkdir -p public/bundles/nelmioapidoc - cp -R ../lib/vendor/nelmio/api-doc-bundle/Resources/public/* public/bundles/nelmioapidoc/ + cp -R vendor/nelmio/api-doc-bundle/public/* public/bundles/nelmioapidoc/ clean-l: -rm -rf public/bundles/nelmioapidoc + -rm -f vendor/autoload_runtime.php install-domserver: # This must be done first to install with the rest. $(MAKE) copy-bundle-assets $(INSTALL_DIR) $(DESTDIR)$(domserver_webappdir); - for d in bin config migrations public resources src templates tests ; do \ + for d in bin config migrations public resources src templates tests vendor; do \ $(call install_tree,$(DESTDIR)$(domserver_webappdir),$$d) ; \ done # Change webapp/public/doc symlink @@ -32,14 +67,18 @@ install-domserver: # Now change all relative symlinks in webapp/public to their correct paths for link in $$(find $(DESTDIR)$(domserver_webappdir)/public/$$dir -maxdepth 2 -type l); do \ target=$$(readlink $$link) ; \ - if echo $${target} | grep -q '\.\./\.\./lib/vendor' ; then \ + case $${target} in *../vendor*) \ rm $$link ; \ - realtarget=$(domserver_libvendordir)$$(echo $${target} | sed 's!^.*\.\./\.\./lib/vendor!!') ; \ + realtarget=$(domserver_webappdir)/vendor$${target#*../vendor} ; \ ln -s $$realtarget $$link ; \ - fi \ + esac \ done $(INSTALL_DATA) -t $(DESTDIR)$(domserver_webappdir) phpunit.xml.dist .env +maintainer-conf: .env.local + +maintainer-install: composer-dump-autoload-dev + maintainer-clean-l: -for d in cache log ; do \ for t in dev prod ; do \ @@ -47,4 +86,5 @@ maintainer-clean-l: done ; \ done -.PHONY: copy-bundle-assets +.PHONY: composer-dump-autoload composer-dump-autoload-dev \ + copy-bundle-assets diff --git a/webapp/bin/console b/webapp/bin/console index 87a4a429ed..ec90dd3d58 100755 --- a/webapp/bin/console +++ b/webapp/bin/console @@ -4,11 +4,11 @@ use App\Kernel; use Symfony\Bundle\FrameworkBundle\Console\Application; -if (!is_file(dirname(__DIR__, 2) . '/lib/vendor/autoload_runtime.php')) { +if (!is_file(dirname(__DIR__) . '/vendor/autoload_runtime.php')) { throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".'); } -require_once dirname(__DIR__, 2) . '/lib/vendor/autoload_runtime.php'; +require_once dirname(__DIR__) . '/vendor/autoload_runtime.php'; require_once dirname(__DIR__) . '/config/load_db_secrets.php'; set_time_limit(0); diff --git a/webapp/bin/phpunit b/webapp/bin/phpunit index ebac76f857..cefea84bfa 100755 --- a/webapp/bin/phpunit +++ b/webapp/bin/phpunit @@ -5,20 +5,19 @@ if (!ini_get('date.timezone')) { ini_set('date.timezone', 'UTC'); } -if (is_file(dirname(__DIR__, 2) . '/lib/vendor/phpunit/phpunit/phpunit')) { +if (is_file(dirname(__DIR__) . '/vendor/phpunit/phpunit/phpunit')) { if (PHP_VERSION_ID >= 80000) { - require dirname(__DIR__, 2) . '/lib/vendor/phpunit/phpunit/phpunit'; + require dirname(__DIR__) . '/vendor/phpunit/phpunit/phpunit'; } else { - define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__, 2) . '/lib/vendor/autoload.php'); + define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__) . '/vendor/autoload.php'); require PHPUNIT_COMPOSER_INSTALL; PHPUnit\TextUI\Command::main(); } } else { - if (!is_file(dirname(__DIR__, - 2) . '/lib/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { + if (!is_file(dirname(__DIR__) . '/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; exit(1); } - require dirname(__DIR__, 2) . '/lib/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; + require dirname(__DIR__) . '/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; } diff --git a/composer.json b/webapp/composer.json similarity index 93% rename from composer.json rename to webapp/composer.json index d380721557..124580fc0a 100644 --- a/composer.json +++ b/webapp/composer.json @@ -8,9 +8,9 @@ "type": "package", "package": { "name": "fortawesome/font-awesome", - "version": "6.4.2", + "version": "6.5.2", "dist": { - "url": "https://github.com/FortAwesome/Font-Awesome/releases/download/6.4.2/fontawesome-free-6.4.2-web.zip", + "url": "https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.2/fontawesome-free-6.5.2-web.zip", "type": "zip" } } @@ -70,6 +70,7 @@ "league/commonmark": "^2.3", "mbostock/d3": "^3.5", "nelmio/api-doc-bundle": "^4.11", + "nelmio/cors-bundle": "^2.4", "novus/nvd3": "^1.8", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.25", @@ -124,15 +125,15 @@ }, "autoload": { "psr-4": { - "App\\": "webapp/src/" + "App\\": "src/" }, "files": [ - "webapp/resources/functions.php" + "resources/functions.php" ] }, "autoload-dev": { "psr-4": { - "App\\Tests\\": "webapp/tests/" + "App\\Tests\\": "tests/" } }, "config": { @@ -143,8 +144,6 @@ "platform": { "php": "8.1.0" }, - "vendor-dir": "lib/vendor", - "component-dir": "lib/vendor/components", "allow-plugins": { "composer/package-versions-deprecated": true, "symfony/flex": true, @@ -177,12 +176,8 @@ }, "extra": { "symfony": { - "root-dir": "webapp/", "allow-contrib": true, "require": "6.4.*" - }, - "runtime": { - "dotenv_path": "webapp/.env" } } } diff --git a/composer.lock b/webapp/composer.lock similarity index 89% rename from composer.lock rename to webapp/composer.lock index 3cebbec6cc..4e8d6c0e92 100644 --- a/composer.lock +++ b/webapp/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c51071846990b3076d364b3be338becc", + "content-hash": "3ffb9ae6009d6e668f77b90a4c6d5cf5", "packages": [ { "name": "apalfrey/select2-bootstrap-5-theme", @@ -50,25 +50,25 @@ }, { "name": "brick/math", - "version": "0.11.0", + "version": "0.12.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478" + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478", - "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", "shasum": "" }, "require": { - "php": "^8.0" + "php": "^8.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^9.0", - "vimeo/psalm": "5.0.0" + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" }, "type": "library", "autoload": { @@ -88,12 +88,17 @@ "arithmetic", "bigdecimal", "bignum", + "bignumber", "brick", - "math" + "decimal", + "integer", + "math", + "mathematics", + "rational" ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.11.0" + "source": "https://github.com/brick/math/tree/0.12.1" }, "funding": [ { @@ -101,7 +106,7 @@ "type": "github" } ], - "time": "2023-01-15T23:15:59+00:00" + "time": "2023-11-29T23:19:16+00:00" }, { "name": "clue/stream-filter", @@ -265,12 +270,12 @@ "source": { "type": "git", "url": "https://github.com/DataTables/Dist-DataTables.git", - "reference": "8da2d44fb0515635f03a7e4cb8b6441a790d8ef2" + "reference": "5a1078977242eef31741a82bacd516cde9568275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DataTables/Dist-DataTables/zipball/8da2d44fb0515635f03a7e4cb8b6441a790d8ef2", - "reference": "8da2d44fb0515635f03a7e4cb8b6441a790d8ef2", + "url": "https://api.github.com/repos/DataTables/Dist-DataTables/zipball/5a1078977242eef31741a82bacd516cde9568275", + "reference": "5a1078977242eef31741a82bacd516cde9568275", "shasum": "" }, "require": { @@ -299,7 +304,7 @@ "forum": "https://datatables.net/forums", "source": "https://github.com/DataTables/Dist-DataTables" }, - "time": "2024-01-25T11:59:45+00:00" + "time": "2024-06-17T10:20:23+00:00" }, { "name": "datatables.net/datatables.net-bs5", @@ -307,12 +312,12 @@ "source": { "type": "git", "url": "https://github.com/DataTables/Dist-DataTables-Bootstrap5.git", - "reference": "5a819ad3495508797a12d4bdcaa729b7114c65e9" + "reference": "598c1fcced6a8dd64661109f7398975fc23bfb97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DataTables/Dist-DataTables-Bootstrap5/zipball/5a819ad3495508797a12d4bdcaa729b7114c65e9", - "reference": "5a819ad3495508797a12d4bdcaa729b7114c65e9", + "url": "https://api.github.com/repos/DataTables/Dist-DataTables-Bootstrap5/zipball/598c1fcced6a8dd64661109f7398975fc23bfb97", + "reference": "598c1fcced6a8dd64661109f7398975fc23bfb97", "shasum": "" }, "require": { @@ -345,7 +350,7 @@ "issues": "https://github.com/DataTables/Dist-DataTables-Bootstrap5/issues", "source": "https://github.com/DataTables/Dist-DataTables-Bootstrap5" }, - "time": "2024-01-25T11:58:19+00:00" + "time": "2024-06-17T10:20:47+00:00" }, { "name": "dflydev/dot-access-data", @@ -422,82 +427,6 @@ }, "time": "2022-10-27T11:44:00+00:00" }, - { - "name": "doctrine/annotations", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", - "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", - "shasum": "" - }, - "require": { - "doctrine/lexer": "^2 || ^3", - "ext-tokenizer": "*", - "php": "^7.2 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" - }, - "require-dev": { - "doctrine/cache": "^2.0", - "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.8.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^5.4 || ^6", - "vimeo/psalm": "^4.10" - }, - "suggest": { - "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/2.0.1" - }, - "time": "2023-02-02T22:02:53+00:00" - }, { "name": "doctrine/cache", "version": "2.2.0", @@ -593,16 +522,16 @@ }, { "name": "doctrine/collections", - "version": "2.1.4", + "version": "2.2.2", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "72328a11443a0de79967104ad36ba7b30bded134" + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/72328a11443a0de79967104ad36ba7b30bded134", - "reference": "72328a11443a0de79967104ad36ba7b30bded134", + "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59", + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59", "shasum": "" }, "require": { @@ -614,7 +543,7 @@ "ext-json": "*", "phpstan/phpstan": "^1.8", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^10.5", "vimeo/psalm": "^5.11" }, "type": "library", @@ -659,7 +588,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.1.4" + "source": "https://github.com/doctrine/collections/tree/2.2.2" }, "funding": [ { @@ -675,20 +604,20 @@ "type": "tidelift" } ], - "time": "2023-10-03T09:22:33+00:00" + "time": "2024-04-18T06:56:21+00:00" }, { "name": "doctrine/common", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced" + "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/8b5e5650391f851ed58910b3e3d48a71062eeced", - "reference": "8b5e5650391f851ed58910b3e3d48a71062eeced", + "url": "https://api.github.com/repos/doctrine/common/zipball/0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", + "reference": "0aad4b7ab7ce8c6602dfbb1e1a24581275fb9d1a", "shasum": "" }, "require": { @@ -750,7 +679,7 @@ ], "support": { "issues": "https://github.com/doctrine/common/issues", - "source": "https://github.com/doctrine/common/tree/3.4.3" + "source": "https://github.com/doctrine/common/tree/3.4.4" }, "funding": [ { @@ -766,7 +695,7 @@ "type": "tidelift" } ], - "time": "2022-10-09T11:47:59+00:00" + "time": "2024-04-16T13:35:33+00:00" }, { "name": "doctrine/data-fixtures", @@ -854,16 +783,16 @@ }, { "name": "doctrine/dbal", - "version": "3.8.0", + "version": "3.8.6", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9" + "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d244f2e6e6bf32bff5174e6729b57214923ecec9", - "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/b7411825cf7efb7e51f9791dea19d86e43b399a1", + "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1", "shasum": "" }, "require": { @@ -879,12 +808,12 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.56", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.15", + "phpstan/phpstan": "1.11.5", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "9.6.19", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.8.1", + "squizlabs/php_codesniffer": "3.10.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" @@ -947,7 +876,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.0" + "source": "https://github.com/doctrine/dbal/tree/3.8.6" }, "funding": [ { @@ -963,7 +892,7 @@ "type": "tidelift" } ], - "time": "2024-01-25T21:44:02+00:00" + "time": "2024-06-19T10:38:17+00:00" }, { "name": "doctrine/deprecations", @@ -1014,16 +943,16 @@ }, { "name": "doctrine/doctrine-bundle", - "version": "2.11.1", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "4089f1424b724786c062aea50aae5f773449b94b" + "reference": "5418e811a14724068e95e0ba43353b903ada530f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/4089f1424b724786c062aea50aae5f773449b94b", - "reference": "4089f1424b724786c062aea50aae5f773449b94b", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5418e811a14724068e95e0ba43353b903ada530f", + "reference": "5418e811a14724068e95e0ba43353b903ada530f", "shasum": "" }, "require": { @@ -1044,23 +973,24 @@ }, "conflict": { "doctrine/annotations": ">=3.0", - "doctrine/orm": "<2.14 || >=4.0", + "doctrine/orm": "<2.17 || >=4.0", "twig/twig": "<1.34 || >=2.0 <2.4" }, "require-dev": { "doctrine/annotations": "^1 || ^2", "doctrine/coding-standard": "^12", "doctrine/deprecations": "^1.0", - "doctrine/orm": "^2.14 || ^3.0", + "doctrine/orm": "^2.17 || ^3.0", "friendsofphp/proxy-manager-lts": "^1.0", - "phpunit/phpunit": "^9.5.26 || ^10.0", + "phpunit/phpunit": "^9.5.26", "psalm/plugin-phpunit": "^0.18.4", - "psalm/plugin-symfony": "^4", + "psalm/plugin-symfony": "^5", "psr/log": "^1.1.4 || ^2.0 || ^3.0", "symfony/phpunit-bridge": "^6.1 || ^7.0", "symfony/property-info": "^5.4 || ^6.0 || ^7.0", "symfony/proxy-manager-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", "symfony/string": "^5.4 || ^6.0 || ^7.0", "symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/validator": "^5.4 || ^6.0 || ^7.0", @@ -1068,7 +998,7 @@ "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0", "twig/twig": "^1.34 || ^2.12 || ^3.0", - "vimeo/psalm": "^4.30" + "vimeo/psalm": "^5.15" }, "suggest": { "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", @@ -1078,7 +1008,7 @@ "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\DoctrineBundle\\": "" + "Doctrine\\Bundle\\DoctrineBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1113,7 +1043,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.11.1" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.12.0" }, "funding": [ { @@ -1129,20 +1059,20 @@ "type": "tidelift" } ], - "time": "2023-11-15T20:01:50+00:00" + "time": "2024-03-19T07:20:37+00:00" }, { "name": "doctrine/doctrine-fixtures-bundle", - "version": "3.5.1", + "version": "3.6.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", - "reference": "c808a0c85c38c8ee265cc8405b456c1d2b38567d" + "reference": "d13a08ebf244f74c8adb8ff15aa55d01c404e534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/c808a0c85c38c8ee265cc8405b456c1d2b38567d", - "reference": "c808a0c85c38c8ee265cc8405b456c1d2b38567d", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/d13a08ebf244f74c8adb8ff15aa55d01c404e534", + "reference": "d13a08ebf244f74c8adb8ff15aa55d01c404e534", "shasum": "" }, "require": { @@ -1171,7 +1101,7 @@ "type": "symfony-bundle", "autoload": { "psr-4": { - "Doctrine\\Bundle\\FixturesBundle\\": "" + "Doctrine\\Bundle\\FixturesBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1200,7 +1130,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.5.1" + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.6.1" }, "funding": [ { @@ -1216,20 +1146,20 @@ "type": "tidelift" } ], - "time": "2023-11-19T12:48:54+00:00" + "time": "2024-05-07T07:16:35+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "1dd42906a5fb9c5960723e2ebb45c68006493835" + "reference": "715b62c31a5894afcb2b2cdbbc6607d7dd0580c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/1dd42906a5fb9c5960723e2ebb45c68006493835", - "reference": "1dd42906a5fb9c5960723e2ebb45c68006493835", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/715b62c31a5894afcb2b2cdbbc6607d7dd0580c0", + "reference": "715b62c31a5894afcb2b2cdbbc6607d7dd0580c0", "shasum": "" }, "require": { @@ -1240,6 +1170,7 @@ "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { + "composer/semver": "^3.0", "doctrine/coding-standard": "^12", "doctrine/orm": "^2.6 || ^3", "doctrine/persistence": "^2.0 || ^3 ", @@ -1291,7 +1222,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.3.0" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.3.1" }, "funding": [ { @@ -1307,20 +1238,20 @@ "type": "tidelift" } ], - "time": "2023-11-13T19:44:41+00:00" + "time": "2024-05-14T20:32:18+00:00" }, { "name": "doctrine/event-manager", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32" + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/750671534e0241a7c50ea5b43f67e23eb5c96f32", - "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", "shasum": "" }, "require": { @@ -1330,10 +1261,10 @@ "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^10", + "doctrine/coding-standard": "^12", "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^4.28" + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "type": "library", "autoload": { @@ -1382,7 +1313,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.0" + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" }, "funding": [ { @@ -1398,20 +1329,20 @@ "type": "tidelift" } ], - "time": "2022-10-12T20:59:15+00:00" + "time": "2024-05-22T20:47:39+00:00" }, { "name": "doctrine/inflector", - "version": "2.0.9", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65" + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/2930cd5ef353871c821d5c43ed030d39ac8cfe65", - "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", "shasum": "" }, "require": { @@ -1473,7 +1404,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.9" + "source": "https://github.com/doctrine/inflector/tree/2.0.10" }, "funding": [ { @@ -1489,7 +1420,7 @@ "type": "tidelift" } ], - "time": "2024-01-15T18:05:13+00:00" + "time": "2024-02-18T20:23:39+00:00" }, { "name": "doctrine/instantiator", @@ -1563,28 +1494,27 @@ }, { "name": "doctrine/lexer", - "version": "2.1.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^10", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.0" + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { @@ -1621,7 +1551,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.0" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -1637,20 +1567,20 @@ "type": "tidelift" } ], - "time": "2022-12-14T08:49:07+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "doctrine/migrations", - "version": "3.7.2", + "version": "3.7.4", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "47af29eef49f29ebee545947e8b2a4b3be318c8a" + "reference": "954e0a314c2f0eb9fb418210445111747de254a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/47af29eef49f29ebee545947e8b2a4b3be318c8a", - "reference": "47af29eef49f29ebee545947e8b2a4b3be318c8a", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/954e0a314c2f0eb9fb418210445111747de254a6", + "reference": "954e0a314c2f0eb9fb418210445111747de254a6", "shasum": "" }, "require": { @@ -1723,7 +1653,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.7.2" + "source": "https://github.com/doctrine/migrations/tree/3.7.4" }, "funding": [ { @@ -1739,20 +1669,20 @@ "type": "tidelift" } ], - "time": "2023-12-05T11:35:05+00:00" + "time": "2024-03-06T13:41:11+00:00" }, { "name": "doctrine/orm", - "version": "2.17.4", + "version": "2.19.5", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "ccfc97c32f63aaa0988ac6aa42e71c5590bb794d" + "reference": "94986af28452da42a46a4489d1c958a2e5d710e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/ccfc97c32f63aaa0988ac6aa42e71c5590bb794d", - "reference": "ccfc97c32f63aaa0988ac6aa42e71c5590bb794d", + "url": "https://api.github.com/repos/doctrine/orm/zipball/94986af28452da42a46a4489d1c958a2e5d710e5", + "reference": "94986af28452da42a46a4489d1c958a2e5d710e5", "shasum": "" }, "require": { @@ -1765,7 +1695,7 @@ "doctrine/event-manager": "^1.2 || ^2", "doctrine/inflector": "^1.4 || ^2.0", "doctrine/instantiator": "^1.3 || ^2", - "doctrine/lexer": "^2", + "doctrine/lexer": "^2 || ^3", "doctrine/persistence": "^2.4 || ^3", "ext-ctype": "*", "php": "^7.1 || ^8.0", @@ -1781,14 +1711,14 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.10.35", + "phpstan/phpstan": "~1.4.10 || 1.10.59", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "vimeo/psalm": "4.30.0 || 5.16.0" + "vimeo/psalm": "4.30.0 || 5.22.2" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1801,7 +1731,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\ORM\\": "lib/Doctrine/ORM" + "Doctrine\\ORM\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1838,22 +1768,22 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.17.4" + "source": "https://github.com/doctrine/orm/tree/2.19.5" }, - "time": "2024-01-26T19:41:16+00:00" + "time": "2024-04-30T06:49:54+00:00" }, { "name": "doctrine/persistence", - "version": "3.2.0", + "version": "3.3.3", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "63fee8c33bef740db6730eb2a750cd3da6495603" + "reference": "b337726451f5d530df338fc7f68dee8781b49779" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/63fee8c33bef740db6730eb2a750cd3da6495603", - "reference": "63fee8c33bef740db6730eb2a750cd3da6495603", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/b337726451f5d530df338fc7f68dee8781b49779", + "reference": "b337726451f5d530df338fc7f68dee8781b49779", "shasum": "" }, "require": { @@ -1865,15 +1795,14 @@ "doctrine/common": "<2.10" }, "require-dev": { - "composer/package-versions-deprecated": "^1.11", - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^12", "doctrine/common": "^3.0", - "phpstan/phpstan": "1.9.4", + "phpstan/phpstan": "1.11.1", "phpstan/phpstan-phpunit": "^1", "phpstan/phpstan-strict-rules": "^1.1", "phpunit/phpunit": "^8.5 || ^9.5", "symfony/cache": "^4.4 || ^5.4 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.3.0" + "vimeo/psalm": "4.30.0 || 5.24.0" }, "type": "library", "autoload": { @@ -1922,7 +1851,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/3.2.0" + "source": "https://github.com/doctrine/persistence/tree/3.3.3" }, "funding": [ { @@ -1938,27 +1867,30 @@ "type": "tidelift" } ], - "time": "2023-05-17T18:32:04+00:00" + "time": "2024-06-20T10:14:30+00:00" }, { "name": "doctrine/sql-formatter", - "version": "1.1.3", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5" + "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/25a06c7bf4c6b8218f47928654252863ffc890a5", - "reference": "25a06c7bf4c6b8218f47928654252863ffc890a5", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d1ac84aef745c69ea034929eb6d65a6908b675cc", + "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" }, "bin": [ "bin/sql-formatter" @@ -1988,9 +1920,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.1.3" + "source": "https://github.com/doctrine/sql-formatter/tree/1.4.0" }, - "time": "2022-05-23T21:33:49+00:00" + "time": "2024-05-08T08:12:09+00:00" }, { "name": "eligrey/filesaver", @@ -2003,37 +1935,38 @@ }, { "name": "fortawesome/font-awesome", - "version": "6.4.2", + "version": "6.5.2", "dist": { "type": "zip", - "url": "https://github.com/FortAwesome/Font-Awesome/releases/download/6.4.2/fontawesome-free-6.4.2-web.zip" + "url": "https://github.com/FortAwesome/Font-Awesome/releases/download/6.5.2/fontawesome-free-6.5.2-web.zip" }, "type": "library" }, { "name": "friendsofsymfony/rest-bundle", - "version": "3.6.0", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/FriendsOfSymfony/FOSRestBundle.git", - "reference": "e01be8113d4451adb3cbb29d7d2cc96bbc698179" + "reference": "db7d9a17da2bcae1bb8e2d7ff320ef3915903373" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/e01be8113d4451adb3cbb29d7d2cc96bbc698179", - "reference": "e01be8113d4451adb3cbb29d7d2cc96bbc698179", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSRestBundle/zipball/db7d9a17da2bcae1bb8e2d7ff320ef3915903373", + "reference": "db7d9a17da2bcae1bb8e2d7ff320ef3915903373", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "symfony/config": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/framework-bundle": "^4.4.1|^5.0|^6.0", - "symfony/http-foundation": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/routing": "^5.4|^6.0", - "symfony/security-core": "^5.4|^6.0", + "php": "^7.4|^8.0", + "symfony/config": "^5.4|^6.4|^7.0", + "symfony/dependency-injection": "^5.4|^6.4|^7.0", + "symfony/deprecation-contracts": "^2.1|^3.0", + "symfony/event-dispatcher": "^5.4|^6.4|^7.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/http-foundation": "^5.4|^6.4|^7.0", + "symfony/http-kernel": "^5.4|^6.4|^7.0", + "symfony/routing": "^5.4|^6.4|^7.0", + "symfony/security-core": "^5.4|^6.4|^7.0", "willdurand/jsonp-callback-validator": "^1.0|^2.0", "willdurand/negotiation": "^2.0|^3.0" }, @@ -2044,32 +1977,32 @@ "sensio/framework-extra-bundle": "<6.1" }, "require-dev": { - "doctrine/annotations": "^1.13.2|^2.0 ", - "friendsofphp/php-cs-fixer": "^3.0", + "doctrine/annotations": "^1.13.2|^2.0", + "friendsofphp/php-cs-fixer": "^3.43", "jms/serializer": "^1.13|^2.0|^3.0", "jms/serializer-bundle": "^2.4.3|^3.0.1|^4.0|^5.0", "psr/http-message": "^1.0", "psr/log": "^1.0|^2.0|^3.0", "sensio/framework-extra-bundle": "^6.1", - "symfony/asset": "^5.4|^6.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/css-selector": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/form": "^5.4|^6.0", - "symfony/mime": "^5.4|^6.0", - "symfony/phpunit-bridge": "^5.4|^6.0", - "symfony/security-bundle": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0", - "symfony/twig-bundle": "^5.4|^6.0", - "symfony/validator": "^5.4|^6.0", - "symfony/web-profiler-bundle": "^5.4|^6.0", - "symfony/yaml": "^5.4|^6.0" + "symfony/asset": "^5.4|^6.4|^7.0", + "symfony/browser-kit": "^5.4|^6.4|^7.0", + "symfony/css-selector": "^5.4|^6.4|^7.0", + "symfony/expression-language": "^5.4|^6.4|^7.0", + "symfony/form": "^5.4|^6.4|^7.0", + "symfony/mime": "^5.4|^6.4|^7.0", + "symfony/phpunit-bridge": "^7.0.1", + "symfony/security-bundle": "^5.4|^6.4|^7.0", + "symfony/serializer": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", + "symfony/validator": "^5.4|^6.4|^7.0", + "symfony/web-profiler-bundle": "^5.4|^6.4|^7.0", + "symfony/yaml": "^5.4|^6.4|^7.0" }, "suggest": { - "jms/serializer-bundle": "Add support for advanced serialization capabilities, recommended, requires ^2.0|^3.0", - "sensio/framework-extra-bundle": "Add support for the request body converter and the view response listener, requires ^3.0", - "symfony/serializer": "Add support for basic serialization capabilities and xml decoding, requires ^2.7|^3.0", - "symfony/validator": "Add support for validation capabilities in the ParamFetcher, requires ^2.7|^3.0" + "jms/serializer-bundle": "Add support for advanced serialization capabilities, recommended", + "sensio/framework-extra-bundle": "Add support for the request body converter and the view response listener, not supported with Symfony >=7.0", + "symfony/serializer": "Add support for basic serialization capabilities and xml decoding", + "symfony/validator": "Add support for validation capabilities in the ParamFetcher" }, "type": "symfony-bundle", "extra": { @@ -2111,9 +2044,9 @@ ], "support": { "issues": "https://github.com/FriendsOfSymfony/FOSRestBundle/issues", - "source": "https://github.com/FriendsOfSymfony/FOSRestBundle/tree/3.6.0" + "source": "https://github.com/FriendsOfSymfony/FOSRestBundle/tree/3.7.1" }, - "time": "2023-09-27T11:41:02+00:00" + "time": "2024-04-12T22:57:10+00:00" }, { "name": "guzzlehttp/promises", @@ -2420,16 +2353,16 @@ }, { "name": "jean85/pretty-package-versions", - "version": "2.0.5", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", "shasum": "" }, "require": { @@ -2437,9 +2370,9 @@ "php": "^7.1|^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", + "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", + "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^7.5|^8.5|^9.4", "vimeo/psalm": "^4.3" }, @@ -2473,9 +2406,9 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" }, - "time": "2021-10-08T21:21:46+00:00" + "time": "2024-03-08T09:58:59+00:00" }, { "name": "jms/metadata", @@ -2543,27 +2476,27 @@ }, { "name": "jms/serializer", - "version": "3.29.1", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "111451f43abb448ce297361a8ab96a9591e848cd" + "reference": "bf1105358123d7c02ee6cad08ea33ab535a09d5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/111451f43abb448ce297361a8ab96a9591e848cd", - "reference": "111451f43abb448ce297361a8ab96a9591e848cd", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/bf1105358123d7c02ee6cad08ea33ab535a09d5e", + "reference": "bf1105358123d7c02ee6cad08ea33ab535a09d5e", "shasum": "" }, "require": { - "doctrine/annotations": "^1.14 || ^2.0", "doctrine/instantiator": "^1.3.1 || ^2.0", "doctrine/lexer": "^2.0 || ^3.0", "jms/metadata": "^2.6", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpstan/phpdoc-parser": "^1.20" }, "require-dev": { + "doctrine/annotations": "^1.14 || ^2.0", "doctrine/coding-standard": "^12.0", "doctrine/orm": "^2.14 || ^3.0", "doctrine/persistence": "^2.5.2 || ^3.0", @@ -2573,16 +2506,17 @@ "ocramius/proxy-manager": "^1.0 || ^2.0", "phpbench/phpbench": "^1.0", "phpstan/phpstan": "^1.0.2", - "phpunit/phpunit": "^8.5.21 || ^9.0 || ^10.0", + "phpunit/phpunit": "^9.0 || ^10.0", "psr/container": "^1.0 || ^2.0", - "symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/expression-language": "^3.2 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/filesystem": "^4.2 || ^5.0 || ^6.0 || ^7.0", - "symfony/form": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/translation": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/uid": "^5.1 || ^6.0 || ^7.0", - "symfony/validator": "^3.1.9 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "rector/rector": "^0.19.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/form": "^5.4 || ^6.0 || ^7.0", + "symfony/translation": "^5.4 || ^6.0 || ^7.0", + "symfony/uid": "^5.4 || ^6.0 || ^7.0", + "symfony/validator": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0", "twig/twig": "^1.34 || ^2.4 || ^3.0" }, "suggest": { @@ -2627,7 +2561,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/serializer/issues", - "source": "https://github.com/schmittjoh/serializer/tree/3.29.1" + "source": "https://github.com/schmittjoh/serializer/tree/3.30.0" }, "funding": [ { @@ -2635,7 +2569,7 @@ "type": "github" } ], - "time": "2023-12-14T15:25:09+00:00" + "time": "2024-02-24T14:12:14+00:00" }, { "name": "jms/serializer-bundle", @@ -2727,16 +2661,16 @@ }, { "name": "league/commonmark", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5" + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5", - "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", "shasum": "" }, "require": { @@ -2749,7 +2683,7 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.0", + "commonmark/cmark": "0.30.3", "commonmark/commonmark.js": "0.30.0", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", @@ -2759,10 +2693,10 @@ "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", + "symfony/finder": "^5.3 | ^6.0 || ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -2829,7 +2763,7 @@ "type": "tidelift" } ], - "time": "2023-08-30T16:55:00+00:00" + "time": "2024-02-02T11:59:32+00:00" }, { "name": "league/config", @@ -2915,16 +2849,16 @@ }, { "name": "league/uri", - "version": "7.4.0", + "version": "7.4.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "bf414ba956d902f5d98bf9385fcf63954f09dce5" + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/bf414ba956d902f5d98bf9385fcf63954f09dce5", - "reference": "bf414ba956d902f5d98bf9385fcf63954f09dce5", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/bedb6e55eff0c933668addaa7efa1e1f2c417cc4", + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4", "shasum": "" }, "require": { @@ -2993,7 +2927,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.4.0" + "source": "https://github.com/thephpleague/uri/tree/7.4.1" }, "funding": [ { @@ -3001,20 +2935,20 @@ "type": "github" } ], - "time": "2023-12-01T06:24:25+00:00" + "time": "2024-03-23T07:42:40+00:00" }, { "name": "league/uri-interfaces", - "version": "7.4.0", + "version": "7.4.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "bd8c487ec236930f7bbc42b8d374fa882fbba0f3" + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/bd8c487ec236930f7bbc42b8d374fa882fbba0f3", - "reference": "bd8c487ec236930f7bbc42b8d374fa882fbba0f3", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/8d43ef5c841032c87e2de015972c06f3865ef718", + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718", "shasum": "" }, "require": { @@ -3077,7 +3011,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.1" }, "funding": [ { @@ -3085,20 +3019,20 @@ "type": "github" } ], - "time": "2023-11-24T15:40:42+00:00" + "time": "2024-03-23T07:42:40+00:00" }, { "name": "masterminds/html5", - "version": "2.8.1", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf" + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f47dcf3c70c584de14f21143c55d9939631bc6cf", - "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", "shasum": "" }, "require": { @@ -3106,7 +3040,7 @@ "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8" + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" }, "type": "library", "extra": { @@ -3150,9 +3084,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.8.1" + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" }, - "time": "2023-05-10T11:58:31+00:00" + "time": "2024-03-31T07:05:07+00:00" }, { "name": "mbostock/d3", @@ -3199,16 +3133,16 @@ }, { "name": "monolog/monolog", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448" + "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448", - "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", "shasum": "" }, "require": { @@ -3231,7 +3165,7 @@ "phpstan/phpstan": "^1.9", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "^10.1", + "phpunit/phpunit": "^10.5.17", "predis/predis": "^1.1 || ^2", "ruflin/elastica": "^7", "symfony/mailer": "^5.4 || ^6", @@ -3284,7 +3218,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.5.0" + "source": "https://github.com/Seldaek/monolog/tree/3.6.0" }, "funding": [ { @@ -3296,65 +3230,74 @@ "type": "tidelift" } ], - "time": "2023-10-27T15:32:31+00:00" + "time": "2024-04-12T21:02:21+00:00" }, { "name": "nelmio/api-doc-bundle", - "version": "v4.19.2", + "version": "v4.28.0", "source": { "type": "git", "url": "https://github.com/nelmio/NelmioApiDocBundle.git", - "reference": "5a5049dd00ce69b7b04c1485f3f41ec58ff7ec3b" + "reference": "684391a5fab4bdfac752560d3483d0f7109448a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/5a5049dd00ce69b7b04c1485f3f41ec58ff7ec3b", - "reference": "5a5049dd00ce69b7b04c1485f3f41ec58ff7ec3b", + "url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/684391a5fab4bdfac752560d3483d0f7109448a5", + "reference": "684391a5fab4bdfac752560d3483d0f7109448a5", "shasum": "" }, "require": { "ext-json": "*", - "php": ">=7.2", - "phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0", - "psr/cache": "^1.0|^2.0|^3.0", - "psr/container": "^1.0|^2.0", - "psr/log": "^1.0|^2.0|^3.0", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/framework-bundle": "^5.4.24|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/options-resolver": "^5.4|^6.0|^7.0", - "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0", - "zircote/swagger-php": "^4.2.15" + "php": ">=7.4", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0", + "phpdocumentor/type-resolver": "^1.8.2", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/config": "^5.4 || ^6.4 || ^7.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/framework-bundle": "^5.4.24 || ^6.4 || ^7.0", + "symfony/http-foundation": "^5.4 || ^6.4 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/property-info": "^5.4.10 || ^6.4 || ^7.0", + "symfony/routing": "^5.4 || ^6.4 || ^7.0", + "zircote/swagger-php": "^4.6.1" + }, + "conflict": { + "zircote/swagger-php": "4.8.7" }, "require-dev": { - "api-platform/core": "^2.7.0|^3", + "api-platform/core": "^2.7.0 || ^3", "composer/package-versions-deprecated": "1.11.99.1", "doctrine/annotations": "^2.0", - "friendsofsymfony/rest-bundle": "^2.8|^3.0", - "jms/serializer": "^1.14|^3.0", - "jms/serializer-bundle": "^2.3|^3.0|^4.0|^5.0", - "phpunit/phpunit": "^8.5|^9.6", - "sensio/framework-extra-bundle": "^5.4|^6.0", - "symfony/asset": "^5.4|^6.0|^7.0", - "symfony/browser-kit": "^5.4|^6.0|^7.0", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/dom-crawler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/form": "^5.4|^6.0|^7.0", + "friendsofphp/php-cs-fixer": "^3.52", + "friendsofsymfony/rest-bundle": "^2.8 || ^3.0", + "jms/serializer": "^1.14 || ^3.0", + "jms/serializer-bundle": "^2.3 || ^3.0 || ^4.0 || ^5.0", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpstan/phpstan-symfony": "^1.3", + "phpunit/phpunit": "^9.6 || ^10.5", + "symfony/asset": "^5.4 || ^6.4 || ^7.0", + "symfony/browser-kit": "^5.4 || ^6.4 || ^7.0", + "symfony/cache": "^5.4 || ^6.4 || ^7.0", + "symfony/dom-crawler": "^5.4 || ^6.4 || ^7.0", + "symfony/expression-language": "^5.4 || ^6.4 || ^7.0", + "symfony/form": "^5.4 || ^6.4 || ^7.0", "symfony/phpunit-bridge": "^6.4", - "symfony/property-access": "^5.4|^6.0|^7.0", - "symfony/security-csrf": "^5.4|^6.0|^7.0", - "symfony/serializer": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/templating": "^5.4|^6.0|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", - "symfony/validator": "^5.4|^6.0|^7.0", - "willdurand/hateoas-bundle": "^1.0|^2.0" + "symfony/property-access": "^5.4 || ^6.4 || ^7.0", + "symfony/security-csrf": "^5.4 || ^6.4 || ^7.0", + "symfony/serializer": "^5.4 || ^6.4 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0", + "symfony/templating": "^5.4 || ^6.4 || ^7.0", + "symfony/twig-bundle": "^5.4 || ^6.4 || ^7.0", + "symfony/uid": "^5.4 || ^6.4 || ^7.0", + "symfony/validator": "^5.4 || ^6.4 || ^7.0", + "willdurand/hateoas-bundle": "^1.0 || ^2.0" }, "suggest": { "api-platform/core": "For using an API oriented framework.", @@ -3379,11 +3322,8 @@ }, "autoload": { "psr-4": { - "Nelmio\\ApiDocBundle\\": "" - }, - "exclude-from-classmap": [ - "Tests/" - ] + "Nelmio\\ApiDocBundle\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3395,7 +3335,7 @@ "homepage": "https://github.com/nelmio/NelmioApiDocBundle/contributors" } ], - "description": "Generates documentation for your REST API from annotations", + "description": "Generates documentation for your REST API from annotations and attributes", "keywords": [ "api", "doc", @@ -3404,9 +3344,71 @@ ], "support": { "issues": "https://github.com/nelmio/NelmioApiDocBundle/issues", - "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.19.2" + "source": "https://github.com/nelmio/NelmioApiDocBundle/tree/v4.28.0" + }, + "time": "2024-06-19T14:37:45+00:00" + }, + { + "name": "nelmio/cors-bundle", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "78fcdb91f76b080a1008133def9c7f613833933d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/78fcdb91f76b080a1008133def9c7f613833933d", + "reference": "78fcdb91f76b080a1008133def9c7f613833933d", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.6", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, - "time": "2024-01-30T09:05:50+00:00" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.4.0" + }, + "time": "2023-11-30T16:41:19+00:00" }, { "name": "nette/schema", @@ -3680,16 +3682,16 @@ }, { "name": "php-http/discovery", - "version": "1.19.2", + "version": "1.19.4", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb" + "reference": "0700efda8d7526335132360167315fdab3aeb599" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb", - "reference": "61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb", + "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", + "reference": "0700efda8d7526335132360167315fdab3aeb599", "shasum": "" }, "require": { @@ -3713,7 +3715,8 @@ "php-http/httplug": "^1.0 || ^2.0", "php-http/message-factory": "^1.0", "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", - "symfony/phpunit-bridge": "^6.2" + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" }, "type": "composer-plugin", "extra": { @@ -3752,9 +3755,9 @@ ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.19.2" + "source": "https://github.com/php-http/discovery/tree/1.19.4" }, - "time": "2023-11-30T16:49:05+00:00" + "time": "2024-03-29T13:00:05+00:00" }, { "name": "php-http/httplug", @@ -3815,16 +3818,16 @@ }, { "name": "php-http/message", - "version": "1.16.0", + "version": "1.16.1", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd" + "reference": "5997f3289332c699fa2545c427826272498a2088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/47a14338bf4ebd67d317bf1144253d7db4ab55fd", - "reference": "47a14338bf4ebd67d317bf1144253d7db4ab55fd", + "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", + "reference": "5997f3289332c699fa2545c427826272498a2088", "shasum": "" }, "require": { @@ -3878,9 +3881,9 @@ ], "support": { "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.16.0" + "source": "https://github.com/php-http/message/tree/1.16.1" }, - "time": "2023-05-17T06:43:38+00:00" + "time": "2024-03-07T13:22:09+00:00" }, { "name": "php-http/message-factory", @@ -3939,16 +3942,16 @@ }, { "name": "php-http/promise", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/php-http/promise.git", - "reference": "2916a606d3b390f4e9e8e2b8dd68581508be0f07" + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/2916a606d3b390f4e9e8e2b8dd68581508be0f07", - "reference": "2916a606d3b390f4e9e8e2b8dd68581508be0f07", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", "shasum": "" }, "require": { @@ -3985,9 +3988,9 @@ ], "support": { "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.3.0" + "source": "https://github.com/php-http/promise/tree/1.3.1" }, - "time": "2024-01-04T18:49:48+00:00" + "time": "2024-03-15T13:55:21+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4044,28 +4047,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", + "version": "5.4.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", + "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.1", "ext-filter": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" + "mockery/mockery": "~1.3.5", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^5.13" }, "type": "library", "extra": { @@ -4089,33 +4099,33 @@ }, { "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1" }, - "time": "2021-10-19T17:43:47+00:00" + "time": "2024-05-21T05:55:05+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc" + "reference": "153ae662783729388a584b4361f2545e4d841e3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fad452781b3d774e3337b0c0b245dd8e5a4455fc", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.4 || ^8.0", + "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", "phpstan/phpdoc-parser": "^1.13" }, @@ -4153,22 +4163,22 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" }, - "time": "2024-01-11T11:49:22+00:00" + "time": "2024-02-23T11:10:43+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.25.0", + "version": "1.29.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", - "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", "shasum": "" }, "require": { @@ -4200,22 +4210,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" }, - "time": "2024-01-04T17:06:16+00:00" + "time": "2024-05-31T08:52:43+00:00" }, { "name": "promphp/prometheus_client_php", - "version": "v2.9.0", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/PromPHP/prometheus_client_php.git", - "reference": "ad2a85fa52a7b9f6e6d84095cd34d1cb6f0a06bc" + "reference": "a09ea80ec1ec26dd1d4853e9af2a811e898dbfeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PromPHP/prometheus_client_php/zipball/ad2a85fa52a7b9f6e6d84095cd34d1cb6f0a06bc", - "reference": "ad2a85fa52a7b9f6e6d84095cd34d1cb6f0a06bc", + "url": "https://api.github.com/repos/PromPHP/prometheus_client_php/zipball/a09ea80ec1ec26dd1d4853e9af2a811e898dbfeb", + "reference": "a09ea80ec1ec26dd1d4853e9af2a811e898dbfeb", "shasum": "" }, "require": { @@ -4267,9 +4277,9 @@ "description": "Prometheus instrumentation library for PHP applications.", "support": { "issues": "https://github.com/PromPHP/prometheus_client_php/issues", - "source": "https://github.com/PromPHP/prometheus_client_php/tree/v2.9.0" + "source": "https://github.com/PromPHP/prometheus_client_php/tree/v2.10.0" }, - "time": "2023-12-20T06:22:58+00:00" + "time": "2024-02-01T13:28:34+00:00" }, { "name": "psr/cache", @@ -4525,20 +4535,20 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -4562,7 +4572,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -4574,9 +4584,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -4816,20 +4826,20 @@ }, { "name": "ramsey/uuid", - "version": "4.7.5", + "version": "4.7.6", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e" + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e", - "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", "ext-json": "*", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" @@ -4892,7 +4902,7 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.7.5" + "source": "https://github.com/ramsey/uuid/tree/4.7.6" }, "funding": [ { @@ -4904,7 +4914,7 @@ "type": "tidelift" } ], - "time": "2023-11-08T05:53:05+00:00" + "time": "2024-04-27T21:32:50+00:00" }, { "name": "riverline/multipart-parser", @@ -5163,16 +5173,16 @@ }, { "name": "sentry/sentry-symfony", - "version": "4.13.2", + "version": "4.14.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-symfony.git", - "reference": "bf049e69863465f2e0ba2555dbb5224641a37d67" + "reference": "001c4cfd8fe93cbb00edaca903ffbfac28259170" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/bf049e69863465f2e0ba2555dbb5224641a37d67", - "reference": "bf049e69863465f2e0ba2555dbb5224641a37d67", + "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/001c4cfd8fe93cbb00edaca903ffbfac28259170", + "reference": "001c4cfd8fe93cbb00edaca903ffbfac28259170", "shasum": "" }, "require": { @@ -5193,8 +5203,8 @@ "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0" }, "require-dev": { - "doctrine/dbal": "^2.13||^3.0", - "doctrine/doctrine-bundle": "^1.12||^2.5", + "doctrine/dbal": "^2.13||^3.3||^4.0", + "doctrine/doctrine-bundle": "^2.6", "friendsofphp/php-cs-fixer": "^2.19||^3.40", "masterminds/html5": "^2.8", "phpstan/extension-installer": "^1.0", @@ -5261,7 +5271,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-symfony/issues", - "source": "https://github.com/getsentry/sentry-symfony/tree/4.13.2" + "source": "https://github.com/getsentry/sentry-symfony/tree/4.14.0" }, "funding": [ { @@ -5273,20 +5283,20 @@ "type": "custom" } ], - "time": "2024-01-11T14:55:45+00:00" + "time": "2024-02-26T09:27:19+00:00" }, { "name": "symfony/asset", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/asset.git", - "reference": "14b1c0fddb64af6ea626af51bb3c47af9fa19cb7" + "reference": "c668aa320e26b7379540368832b9d1dd43d32603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/asset/zipball/14b1c0fddb64af6ea626af51bb3c47af9fa19cb7", - "reference": "14b1c0fddb64af6ea626af51bb3c47af9fa19cb7", + "url": "https://api.github.com/repos/symfony/asset/zipball/c668aa320e26b7379540368832b9d1dd43d32603", + "reference": "c668aa320e26b7379540368832b9d1dd43d32603", "shasum": "" }, "require": { @@ -5326,7 +5336,7 @@ "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/asset/tree/v6.4.3" + "source": "https://github.com/symfony/asset/tree/v6.4.8" }, "funding": [ { @@ -5342,20 +5352,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/browser-kit", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "495ffa2e6d17e199213f93768efa01af32bbf70e" + "reference": "62ab90b92066ef6cce5e79365625b4b1432464c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/495ffa2e6d17e199213f93768efa01af32bbf70e", - "reference": "495ffa2e6d17e199213f93768efa01af32bbf70e", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/62ab90b92066ef6cce5e79365625b4b1432464c8", + "reference": "62ab90b92066ef6cce5e79365625b4b1432464c8", "shasum": "" }, "require": { @@ -5394,7 +5404,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v6.4.3" + "source": "https://github.com/symfony/browser-kit/tree/v6.4.8" }, "funding": [ { @@ -5410,20 +5420,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/cache", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "49f8cdee544a621a621cd21b6cda32a38926d310" + "reference": "287142df5579ce223c485b3872df3efae8390984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/49f8cdee544a621a621cd21b6cda32a38926d310", - "reference": "49f8cdee544a621a621cd21b6cda32a38926d310", + "url": "https://api.github.com/repos/symfony/cache/zipball/287142df5579ce223c485b3872df3efae8390984", + "reference": "287142df5579ce223c485b3872df3efae8390984", "shasum": "" }, "require": { @@ -5490,7 +5500,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.3" + "source": "https://github.com/symfony/cache/tree/v6.4.8" }, "funding": [ { @@ -5506,20 +5516,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "1d74b127da04ffa87aa940abe15446fa89653778" + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1d74b127da04ffa87aa940abe15446fa89653778", - "reference": "1d74b127da04ffa87aa940abe15446fa89653778", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/df6a1a44c890faded49a5fca33c2d5c5fd3c2197", + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197", "shasum": "" }, "require": { @@ -5529,7 +5539,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -5566,7 +5576,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.0" }, "funding": [ { @@ -5582,20 +5592,20 @@ "type": "tidelift" } ], - "time": "2023-09-25T12:52:38+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/clock", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "f48770105c544001da00b8d745873a628e0de198" + "reference": "7a4840efd17135cbd547e41ec49fb910ed4f8b98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/f48770105c544001da00b8d745873a628e0de198", - "reference": "f48770105c544001da00b8d745873a628e0de198", + "url": "https://api.github.com/repos/symfony/clock/zipball/7a4840efd17135cbd547e41ec49fb910ed4f8b98", + "reference": "7a4840efd17135cbd547e41ec49fb910ed4f8b98", "shasum": "" }, "require": { @@ -5640,7 +5650,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v6.4.3" + "source": "https://github.com/symfony/clock/tree/v6.4.8" }, "funding": [ { @@ -5656,20 +5666,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:51:39+00:00" }, { "name": "symfony/config", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "206482ff3ed450495b1d5b7bad1bc3a852def96f" + "reference": "12e7e52515ce37191b193cf3365903c4f3951e35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/206482ff3ed450495b1d5b7bad1bc3a852def96f", - "reference": "206482ff3ed450495b1d5b7bad1bc3a852def96f", + "url": "https://api.github.com/repos/symfony/config/zipball/12e7e52515ce37191b193cf3365903c4f3951e35", + "reference": "12e7e52515ce37191b193cf3365903c4f3951e35", "shasum": "" }, "require": { @@ -5715,7 +5725,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.3" + "source": "https://github.com/symfony/config/tree/v6.4.8" }, "funding": [ { @@ -5731,20 +5741,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T13:26:27+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/console", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e" + "reference": "be5854cee0e8c7b110f00d695d11debdfa1a2a91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", - "reference": "2aaf83b4de5b9d43b93e4aec6f2f8b676f7c567e", + "url": "https://api.github.com/repos/symfony/console/zipball/be5854cee0e8c7b110f00d695d11debdfa1a2a91", + "reference": "be5854cee0e8c7b110f00d695d11debdfa1a2a91", "shasum": "" }, "require": { @@ -5809,7 +5819,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.3" + "source": "https://github.com/symfony/console/tree/v6.4.8" }, "funding": [ { @@ -5825,20 +5835,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/css-selector", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229" + "reference": "4b61b02fe15db48e3687ce1c45ea385d1780fe08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/ee0f7ed5cf298cc019431bb3b3977ebc52b86229", - "reference": "ee0f7ed5cf298cc019431bb3b3977ebc52b86229", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/4b61b02fe15db48e3687ce1c45ea385d1780fe08", + "reference": "4b61b02fe15db48e3687ce1c45ea385d1780fe08", "shasum": "" }, "require": { @@ -5874,7 +5884,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.4.3" + "source": "https://github.com/symfony/css-selector/tree/v6.4.8" }, "funding": [ { @@ -5890,20 +5900,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "6871811c5a5c5e180244ddb689746446db02c05b" + "reference": "d3b618176e8c3a9e5772151c51eba0c52a0c771c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6871811c5a5c5e180244ddb689746446db02c05b", - "reference": "6871811c5a5c5e180244ddb689746446db02c05b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/d3b618176e8c3a9e5772151c51eba0c52a0c771c", + "reference": "d3b618176e8c3a9e5772151c51eba0c52a0c771c", "shasum": "" }, "require": { @@ -5955,7 +5965,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.3" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.8" }, "funding": [ { @@ -5971,20 +5981,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -5993,7 +6003,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -6022,7 +6032,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -6038,20 +6048,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "9c9a44bb06337dadeb9db1a8b202f15cca804353" + "reference": "afbf291ccaf595c8ff6f4ed3943aa0ea479e4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/9c9a44bb06337dadeb9db1a8b202f15cca804353", - "reference": "9c9a44bb06337dadeb9db1a8b202f15cca804353", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/afbf291ccaf595c8ff6f4ed3943aa0ea479e4d04", + "reference": "afbf291ccaf595c8ff6f4ed3943aa0ea479e4d04", "shasum": "" }, "require": { @@ -6069,7 +6079,7 @@ "doctrine/orm": "<2.15", "symfony/cache": "<5.4", "symfony/dependency-injection": "<6.2", - "symfony/form": "<5.4.21|>=6,<6.2.7", + "symfony/form": "<5.4.38|>=6,<6.4.6|>=7,<7.0.6", "symfony/http-foundation": "<6.3", "symfony/http-kernel": "<6.2", "symfony/lock": "<6.3", @@ -6090,7 +6100,7 @@ "symfony/dependency-injection": "^6.2|^7.0", "symfony/doctrine-messenger": "^5.4|^6.0|^7.0", "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/form": "^5.4.21|^6.2.7|^7.0", + "symfony/form": "^5.4.38|^6.4.6|^7.0.6", "symfony/http-kernel": "^6.3|^7.0", "symfony/lock": "^6.3|^7.0", "symfony/messenger": "^5.4|^6.0|^7.0", @@ -6130,7 +6140,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.8" }, "funding": [ { @@ -6146,20 +6156,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T11:24:52+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/dom-crawler", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "6db31849011fefe091e94d0bb10cba26f7919894" + "reference": "105b56a0305d219349edeb60a800082eca864e4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/6db31849011fefe091e94d0bb10cba26f7919894", - "reference": "6db31849011fefe091e94d0bb10cba26f7919894", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/105b56a0305d219349edeb60a800082eca864e4b", + "reference": "105b56a0305d219349edeb60a800082eca864e4b", "shasum": "" }, "require": { @@ -6197,7 +6207,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.4.3" + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.8" }, "funding": [ { @@ -6213,20 +6223,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/dotenv", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "3cb7ca997124760ed1389d5341806247670f4ef8" + "reference": "55aefa0029adff89ecffdb560820e945c7983f06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/3cb7ca997124760ed1389d5341806247670f4ef8", - "reference": "3cb7ca997124760ed1389d5341806247670f4ef8", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/55aefa0029adff89ecffdb560820e945c7983f06", + "reference": "55aefa0029adff89ecffdb560820e945c7983f06", "shasum": "" }, "require": { @@ -6271,7 +6281,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.3" + "source": "https://github.com/symfony/dotenv/tree/v6.4.8" }, "funding": [ { @@ -6287,20 +6297,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/error-handler", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "6dc3c76a278b77f01d864a6005d640822c6f26a6" + "reference": "ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/6dc3c76a278b77f01d864a6005d640822c6f26a6", - "reference": "6dc3c76a278b77f01d864a6005d640822c6f26a6", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc", + "reference": "ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc", "shasum": "" }, "require": { @@ -6346,7 +6356,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.3" + "source": "https://github.com/symfony/error-handler/tree/v6.4.8" }, "funding": [ { @@ -6362,20 +6372,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:40:36+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef" + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae9d3a6f3003a6caf56acd7466d8d52378d44fef", - "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", "shasum": "" }, "require": { @@ -6426,7 +6436,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" }, "funding": [ { @@ -6442,20 +6452,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", - "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { @@ -6465,7 +6475,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -6502,7 +6512,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -6518,20 +6528,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/expression-language", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "b4a4ae33fbb33a99d23c5698faaecadb76ad0fe4" + "reference": "0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/b4a4ae33fbb33a99d23c5698faaecadb76ad0fe4", - "reference": "b4a4ae33fbb33a99d23c5698faaecadb76ad0fe4", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a", + "reference": "0b63cb437741a42104d3ccc9bf60bbd8e1acbd2a", "shasum": "" }, "require": { @@ -6566,7 +6576,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v6.4.3" + "source": "https://github.com/symfony/expression-language/tree/v6.4.8" }, "funding": [ { @@ -6582,20 +6592,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" + "reference": "4d37529150e7081c51b3c5d5718c55a04a9503f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/4d37529150e7081c51b3c5d5718c55a04a9503f3", + "reference": "4d37529150e7081c51b3c5d5718c55a04a9503f3", "shasum": "" }, "require": { @@ -6603,6 +6613,9 @@ "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" + }, "type": "library", "autoload": { "psr-4": { @@ -6629,7 +6642,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.3" + "source": "https://github.com/symfony/filesystem/tree/v6.4.8" }, "funding": [ { @@ -6645,20 +6658,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/finder", - "version": "v6.4.0", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + "reference": "3ef977a43883215d560a2cecb82ec8e62131471c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", - "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "url": "https://api.github.com/repos/symfony/finder/zipball/3ef977a43883215d560a2cecb82ec8e62131471c", + "reference": "3ef977a43883215d560a2cecb82ec8e62131471c", "shasum": "" }, "require": { @@ -6693,7 +6706,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.0" + "source": "https://github.com/symfony/finder/tree/v6.4.8" }, "funding": [ { @@ -6709,20 +6722,20 @@ "type": "tidelift" } ], - "time": "2023-10-31T17:30:12+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/flex", - "version": "v2.4.3", + "version": "v2.4.5", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1" + "reference": "b0a405f40614c9f584b489d54f91091817b0e26e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1", - "reference": "6b44ac75c7f07f48159ec36c2d21ef8cf48a21b1", + "url": "https://api.github.com/repos/symfony/flex/zipball/b0a405f40614c9f584b489d54f91091817b0e26e", + "reference": "b0a405f40614c9f584b489d54f91091817b0e26e", "shasum": "" }, "require": { @@ -6758,7 +6771,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.4.3" + "source": "https://github.com/symfony/flex/tree/v2.4.5" }, "funding": [ { @@ -6774,20 +6787,20 @@ "type": "tidelift" } ], - "time": "2024-01-02T11:08:32+00:00" + "time": "2024-03-02T08:16:47+00:00" }, { "name": "symfony/form", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "dabf7e9375550aada8916ced1736d01c2e3debff" + "reference": "196ebc738e59bec2bbf1f49c24cc221b47f77f5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/dabf7e9375550aada8916ced1736d01c2e3debff", - "reference": "dabf7e9375550aada8916ced1736d01c2e3debff", + "url": "https://api.github.com/repos/symfony/form/zipball/196ebc738e59bec2bbf1f49c24cc221b47f77f5d", + "reference": "196ebc738e59bec2bbf1f49c24cc221b47f77f5d", "shasum": "" }, "require": { @@ -6855,7 +6868,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v6.4.3" + "source": "https://github.com/symfony/form/tree/v6.4.8" }, "funding": [ { @@ -6871,20 +6884,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "fb413ac4483803954411966a39f3a9204835848e" + "reference": "7c7739f87f1a8be1c2f5e7d28addfe763a917acb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/fb413ac4483803954411966a39f3a9204835848e", - "reference": "fb413ac4483803954411966a39f3a9204835848e", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/7c7739f87f1a8be1c2f5e7d28addfe763a917acb", + "reference": "7c7739f87f1a8be1c2f5e7d28addfe763a917acb", "shasum": "" }, "require": { @@ -6923,7 +6936,7 @@ "symfony/mime": "<6.4", "symfony/property-access": "<5.4", "symfony/property-info": "<5.4", - "symfony/scheduler": "<6.4.3|>=7.0.0,<7.0.3", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", "symfony/security-core": "<5.4", "symfony/security-csrf": "<5.4", "symfony/serializer": "<6.4", @@ -6962,7 +6975,7 @@ "symfony/process": "^5.4|^6.0|^7.0", "symfony/property-info": "^5.4|^6.0|^7.0", "symfony/rate-limiter": "^5.4|^6.0|^7.0", - "symfony/scheduler": "^6.4.3|^7.0.3", + "symfony/scheduler": "^6.4.4|^7.0.4", "symfony/security-bundle": "^5.4|^6.0|^7.0", "symfony/semaphore": "^5.4|^6.0|^7.0", "symfony/serializer": "^6.4|^7.0", @@ -6975,7 +6988,7 @@ "symfony/web-link": "^5.4|^6.0|^7.0", "symfony/workflow": "^6.4|^7.0", "symfony/yaml": "^5.4|^6.0|^7.0", - "twig/twig": "^2.10|^3.0" + "twig/twig": "^2.10|^3.0.4" }, "type": "symfony-bundle", "autoload": { @@ -7003,7 +7016,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.8" }, "funding": [ { @@ -7019,20 +7032,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:02:55+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "116335ab09e10b05405f01d8afd31ccc3832b08b" + "reference": "9de29a710320ee802374e479169c5a82f9ee7854" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/116335ab09e10b05405f01d8afd31ccc3832b08b", - "reference": "116335ab09e10b05405f01d8afd31ccc3832b08b", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/9de29a710320ee802374e479169c5a82f9ee7854", + "reference": "9de29a710320ee802374e479169c5a82f9ee7854", "shasum": "" }, "require": { @@ -7072,7 +7085,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v6.4.3" + "source": "https://github.com/symfony/html-sanitizer/tree/v6.4.8" }, "funding": [ { @@ -7088,27 +7101,27 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:51:39+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "a9034bc119fab8238f76cf49c770f3135f3ead86" + "reference": "61faba993e620fc22d4f0ab3b6bcf8fbb0d44b05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/a9034bc119fab8238f76cf49c770f3135f3ead86", - "reference": "a9034bc119fab8238f76cf49c770f3135f3ead86", + "url": "https://api.github.com/repos/symfony/http-client/zipball/61faba993e620fc22d4f0ab3b6bcf8fbb0d44b05", + "reference": "61faba993e620fc22d4f0ab3b6bcf8fbb0d44b05", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3", + "symfony/http-client-contracts": "^3.4.1", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -7126,7 +7139,7 @@ "amphp/http-client": "^4.2.1", "amphp/http-tunnel": "^1.0", "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4", + "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", @@ -7165,7 +7178,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.3" + "source": "https://github.com/symfony/http-client/tree/v6.4.8" }, "funding": [ { @@ -7181,20 +7194,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:01:07+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "1ee70e699b41909c209a0c930f11034b93578654" + "reference": "20414d96f391677bf80078aa55baece78b82647d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/1ee70e699b41909c209a0c930f11034b93578654", - "reference": "1ee70e699b41909c209a0c930f11034b93578654", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", "shasum": "" }, "require": { @@ -7203,7 +7216,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -7243,7 +7256,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" }, "funding": [ { @@ -7259,20 +7272,20 @@ "type": "tidelift" } ], - "time": "2023-07-30T20:28:31+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "5677bdf7cade4619cb17fc9e1e7b31ec392244a9" + "reference": "27de8cc95e11db7a50b027e71caaab9024545947" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5677bdf7cade4619cb17fc9e1e7b31ec392244a9", - "reference": "5677bdf7cade4619cb17fc9e1e7b31ec392244a9", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/27de8cc95e11db7a50b027e71caaab9024545947", + "reference": "27de8cc95e11db7a50b027e71caaab9024545947", "shasum": "" }, "require": { @@ -7320,7 +7333,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.3" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.8" }, "funding": [ { @@ -7336,20 +7349,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "9c6ec4e543044f7568a53a76ab1484ecd30637a2" + "reference": "6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9c6ec4e543044f7568a53a76ab1484ecd30637a2", - "reference": "9c6ec4e543044f7568a53a76ab1484ecd30637a2", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1", + "reference": "6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1", "shasum": "" }, "require": { @@ -7398,12 +7411,13 @@ "symfony/process": "^5.4|^6.0|^7.0", "symfony/property-access": "^5.4.5|^6.0.5|^7.0", "symfony/routing": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.3|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", "symfony/stopwatch": "^5.4|^6.0|^7.0", "symfony/translation": "^5.4|^6.0|^7.0", "symfony/translation-contracts": "^2.5|^3", "symfony/uid": "^5.4|^6.0|^7.0", "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", "symfony/var-exporter": "^6.2|^7.0", "twig/twig": "^2.13|^3.0.4" }, @@ -7433,7 +7447,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.3" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.8" }, "funding": [ { @@ -7449,20 +7463,20 @@ "type": "tidelift" } ], - "time": "2024-01-31T07:21:29+00:00" + "time": "2024-06-02T16:06:25+00:00" }, { "name": "symfony/intl", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "2628ded562ca132ed7cdea72f5ec6aaf65d94414" + "reference": "50265cdcf5a44bec3fcf487b5d0015aece91d1eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/2628ded562ca132ed7cdea72f5ec6aaf65d94414", - "reference": "2628ded562ca132ed7cdea72f5ec6aaf65d94414", + "url": "https://api.github.com/repos/symfony/intl/zipball/50265cdcf5a44bec3fcf487b5d0015aece91d1eb", + "reference": "50265cdcf5a44bec3fcf487b5d0015aece91d1eb", "shasum": "" }, "require": { @@ -7479,7 +7493,8 @@ "Symfony\\Component\\Intl\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/Resources/data/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7515,7 +7530,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.4.3" + "source": "https://github.com/symfony/intl/tree/v6.4.8" }, "funding": [ { @@ -7531,20 +7546,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/mime", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "5017e0a9398c77090b7694be46f20eb796262a34" + "reference": "618597ab8b78ac86d1c75a9d0b35540cda074f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/5017e0a9398c77090b7694be46f20eb796262a34", - "reference": "5017e0a9398c77090b7694be46f20eb796262a34", + "url": "https://api.github.com/repos/symfony/mime/zipball/618597ab8b78ac86d1c75a9d0b35540cda074f33", + "reference": "618597ab8b78ac86d1c75a9d0b35540cda074f33", "shasum": "" }, "require": { @@ -7565,6 +7580,7 @@ "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", "symfony/property-access": "^5.4|^6.0|^7.0", "symfony/property-info": "^5.4|^6.0|^7.0", "symfony/serializer": "^6.3.2|^7.0" @@ -7599,7 +7615,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.3" + "source": "https://github.com/symfony/mime/tree/v6.4.8" }, "funding": [ { @@ -7615,20 +7631,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-06-01T07:50:16+00:00" }, { "name": "symfony/monolog-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "1e1ec293f15dcc815146637ee9ee8a7f43642fa1" + "reference": "0fbee64913b1c595e7650a1919ba3edba8d49ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/1e1ec293f15dcc815146637ee9ee8a7f43642fa1", - "reference": "1e1ec293f15dcc815146637ee9ee8a7f43642fa1", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/0fbee64913b1c595e7650a1919ba3edba8d49ea7", + "reference": "0fbee64913b1c595e7650a1919ba3edba8d49ea7", "shasum": "" }, "require": { @@ -7641,7 +7657,7 @@ "conflict": { "symfony/console": "<5.4", "symfony/http-foundation": "<5.4", - "symfony/security-core": "<6.0" + "symfony/security-core": "<5.4" }, "require-dev": { "symfony/console": "^5.4|^6.0|^7.0", @@ -7649,7 +7665,7 @@ "symfony/mailer": "^5.4|^6.0|^7.0", "symfony/messenger": "^5.4|^6.0|^7.0", "symfony/mime": "^5.4|^6.0|^7.0", - "symfony/security-core": "^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "symfony-bridge", @@ -7678,7 +7694,7 @@ "description": "Provides integration for Monolog with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.8" }, "funding": [ { @@ -7694,7 +7710,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:01:07+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/monolog-bundle", @@ -7779,16 +7795,16 @@ }, { "name": "symfony/options-resolver", - "version": "v6.4.0", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e" + "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b", + "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b", "shasum": "" }, "require": { @@ -7826,7 +7842,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.0" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.8" }, "funding": [ { @@ -7842,20 +7858,20 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:16:24+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/password-hasher", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/password-hasher.git", - "reference": "5189cdfe89a9acb56cc6d6d7a5233bfb180c7268" + "reference": "90ebbe946e5d64a5fad9ac9427e335045cf2bd31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/password-hasher/zipball/5189cdfe89a9acb56cc6d6d7a5233bfb180c7268", - "reference": "5189cdfe89a9acb56cc6d6d7a5233bfb180c7268", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/90ebbe946e5d64a5fad9ac9427e335045cf2bd31", + "reference": "90ebbe946e5d64a5fad9ac9427e335045cf2bd31", "shasum": "" }, "require": { @@ -7898,7 +7914,7 @@ "password" ], "support": { - "source": "https://github.com/symfony/password-hasher/tree/v6.4.3" + "source": "https://github.com/symfony/password-hasher/tree/v6.4.8" }, "funding": [ { @@ -7914,20 +7930,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "875e90aeea2777b6f135677f618529449334a612" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", - "reference": "875e90aeea2777b6f135677f618529449334a612", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -7938,9 +7954,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -7979,7 +7992,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -7995,20 +8008,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "e46b4da57951a16053cd751f63f4a24292788157" + "reference": "e76343c631b453088e2260ac41dfebe21954de81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e46b4da57951a16053cd751f63f4a24292788157", - "reference": "e46b4da57951a16053cd751f63f4a24292788157", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/e76343c631b453088e2260ac41dfebe21954de81", + "reference": "e76343c631b453088e2260ac41dfebe21954de81", "shasum": "" }, "require": { @@ -8019,9 +8032,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8066,7 +8076,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.30.0" }, "funding": [ { @@ -8082,20 +8092,20 @@ "type": "tidelift" } ], - "time": "2023-03-21T17:27:24+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d" + "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", + "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", "shasum": "" }, "require": { @@ -8108,9 +8118,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8153,7 +8160,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0" }, "funding": [ { @@ -8169,20 +8176,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:30:37+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -8193,9 +8200,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8237,7 +8241,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -8253,20 +8257,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -8280,9 +8284,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8320,7 +8321,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -8336,20 +8337,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + "reference": "10112722600777e02d2745716b70c5db4ca70442" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442", + "reference": "10112722600777e02d2745716b70c5db4ca70442", "shasum": "" }, "require": { @@ -8357,9 +8358,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8396,7 +8394,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0" }, "funding": [ { @@ -8412,20 +8410,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -8433,9 +8431,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8479,7 +8474,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -8495,31 +8490,27 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.28.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", - "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-php80": "^1.14" + "php": ">=7.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -8559,7 +8550,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0" }, "funding": [ { @@ -8575,20 +8566,20 @@ "type": "tidelift" } ], - "time": "2023-08-16T06:22:46+00:00" + "time": "2024-06-19T12:35:24+00:00" }, { "name": "symfony/property-access", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "dd22c9247a16c712bfa128b3c90ebdd505102778" + "reference": "e4d9b00983612f9c0013ca37c61affdba2dd975a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/dd22c9247a16c712bfa128b3c90ebdd505102778", - "reference": "dd22c9247a16c712bfa128b3c90ebdd505102778", + "url": "https://api.github.com/repos/symfony/property-access/zipball/e4d9b00983612f9c0013ca37c61affdba2dd975a", + "reference": "e4d9b00983612f9c0013ca37c61affdba2dd975a", "shasum": "" }, "require": { @@ -8636,7 +8627,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.4.3" + "source": "https://github.com/symfony/property-access/tree/v6.4.8" }, "funding": [ { @@ -8652,20 +8643,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/property-info", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "e96d740ab5ac39aa530c8eaa0720ea8169118e26" + "reference": "7f544bc6ceb1a6a2283c7af8e8621262c43b7ede" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/e96d740ab5ac39aa530c8eaa0720ea8169118e26", - "reference": "e96d740ab5ac39aa530c8eaa0720ea8169118e26", + "url": "https://api.github.com/repos/symfony/property-info/zipball/7f544bc6ceb1a6a2283c7af8e8621262c43b7ede", + "reference": "7f544bc6ceb1a6a2283c7af8e8621262c43b7ede", "shasum": "" }, "require": { @@ -8719,7 +8710,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v6.4.3" + "source": "https://github.com/symfony/property-info/tree/v6.4.8" }, "funding": [ { @@ -8735,20 +8726,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "49cfb0223ec64379f7154214dcc1f7c46f3c7a47" + "reference": "23a162bd446b93948a2c2f6909d80ad06195be10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/49cfb0223ec64379f7154214dcc1f7c46f3c7a47", - "reference": "49cfb0223ec64379f7154214dcc1f7c46f3c7a47", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/23a162bd446b93948a2c2f6909d80ad06195be10", + "reference": "23a162bd446b93948a2c2f6909d80ad06195be10", "shasum": "" }, "require": { @@ -8802,7 +8793,7 @@ "psr-7" ], "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.8" }, "funding": [ { @@ -8818,20 +8809,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:51:39+00:00" }, { "name": "symfony/routing", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "3b2957ad54902f0f544df83e3d58b38d7e8e5842" + "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/3b2957ad54902f0f544df83e3d58b38d7e8e5842", - "reference": "3b2957ad54902f0f544df83e3d58b38d7e8e5842", + "url": "https://api.github.com/repos/symfony/routing/zipball/8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", + "reference": "8a40d0f9b01f0fbb80885d3ce0ad6714fb603a58", "shasum": "" }, "require": { @@ -8885,7 +8876,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.3" + "source": "https://github.com/symfony/routing/tree/v6.4.8" }, "funding": [ { @@ -8901,20 +8892,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T13:55:02+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/runtime", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "5682281d26366cd3bf0648cec69de0e62cca7fa0" + "reference": "b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/5682281d26366cd3bf0648cec69de0e62cca7fa0", - "reference": "5682281d26366cd3bf0648cec69de0e62cca7fa0", + "url": "https://api.github.com/repos/symfony/runtime/zipball/b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0", + "reference": "b4bfa2fd4cad1fee62f80b3dfe4eb674cc3302a0", "shasum": "" }, "require": { @@ -8964,7 +8955,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.4.3" + "source": "https://github.com/symfony/runtime/tree/v6.4.8" }, "funding": [ { @@ -8980,20 +8971,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/security-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "a53a9e1f6695447ce613aa8c9c698cfd012bd2aa" + "reference": "dfb286069b0332e1f1c21962133d17c0fbc1e5e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/a53a9e1f6695447ce613aa8c9c698cfd012bd2aa", - "reference": "a53a9e1f6695447ce613aa8c9c698cfd012bd2aa", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/dfb286069b0332e1f1c21962133d17c0fbc1e5e7", + "reference": "dfb286069b0332e1f1c21962133d17c0fbc1e5e7", "shasum": "" }, "require": { @@ -9076,7 +9067,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.8" }, "funding": [ { @@ -9092,20 +9083,20 @@ "type": "tidelift" } ], - "time": "2024-01-28T15:49:46+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/security-core", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "bb10f630cf5b1819ff80aa3ad57a09c61268fc48" + "reference": "5fc7850ada5e8e03d78c1739c82c64d5e2f7d495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/bb10f630cf5b1819ff80aa3ad57a09c61268fc48", - "reference": "bb10f630cf5b1819ff80aa3ad57a09c61268fc48", + "url": "https://api.github.com/repos/symfony/security-core/zipball/5fc7850ada5e8e03d78c1739c82c64d5e2f7d495", + "reference": "5fc7850ada5e8e03d78c1739c82c64d5e2f7d495", "shasum": "" }, "require": { @@ -9162,7 +9153,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.4.3" + "source": "https://github.com/symfony/security-core/tree/v6.4.8" }, "funding": [ { @@ -9178,20 +9169,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/security-csrf", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-csrf.git", - "reference": "e10257dd26f965d75e96bbfc27e46efd943f3010" + "reference": "f46ab02b76311087873257071559edcaf6d7ab99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-csrf/zipball/e10257dd26f965d75e96bbfc27e46efd943f3010", - "reference": "e10257dd26f965d75e96bbfc27e46efd943f3010", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/f46ab02b76311087873257071559edcaf6d7ab99", + "reference": "f46ab02b76311087873257071559edcaf6d7ab99", "shasum": "" }, "require": { @@ -9230,7 +9221,7 @@ "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-csrf/tree/v6.4.3" + "source": "https://github.com/symfony/security-csrf/tree/v6.4.8" }, "funding": [ { @@ -9246,20 +9237,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/security-http", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "d1962d08e02d620dccbaa28192498642500b5043" + "reference": "fb82ddec887dc67f3bcf4d6df3cb8efd529be104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/d1962d08e02d620dccbaa28192498642500b5043", - "reference": "d1962d08e02d620dccbaa28192498642500b5043", + "url": "https://api.github.com/repos/symfony/security-http/zipball/fb82ddec887dc67f3bcf4d6df3cb8efd529be104", + "reference": "fb82ddec887dc67f3bcf4d6df3cb8efd529be104", "shasum": "" }, "require": { @@ -9318,7 +9309,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.4.3" + "source": "https://github.com/symfony/security-http/tree/v6.4.8" }, "funding": [ { @@ -9334,20 +9325,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "51a06ee93c4d5ab5b9edaa0635d8b83953e3c14d" + "reference": "d6eda9966a3e5d1823c1cedf41bf98f8ed969d7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/51a06ee93c4d5ab5b9edaa0635d8b83953e3c14d", - "reference": "51a06ee93c4d5ab5b9edaa0635d8b83953e3c14d", + "url": "https://api.github.com/repos/symfony/serializer/zipball/d6eda9966a3e5d1823c1cedf41bf98f8ed969d7c", + "reference": "d6eda9966a3e5d1823c1cedf41bf98f8ed969d7c", "shasum": "" }, "require": { @@ -9416,7 +9407,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.3" + "source": "https://github.com/symfony/serializer/tree/v6.4.8" }, "funding": [ { @@ -9432,25 +9423,26 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.4.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -9458,7 +9450,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -9498,7 +9490,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -9514,20 +9506,20 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "416596166641f1f728b0a64f5b9dd07cceb410c1" + "reference": "63e069eb616049632cde9674c46957819454b8aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/416596166641f1f728b0a64f5b9dd07cceb410c1", - "reference": "416596166641f1f728b0a64f5b9dd07cceb410c1", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/63e069eb616049632cde9674c46957819454b8aa", + "reference": "63e069eb616049632cde9674c46957819454b8aa", "shasum": "" }, "require": { @@ -9560,7 +9552,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.3" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.8" }, "funding": [ { @@ -9576,20 +9568,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:35:58+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/string", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "7a14736fb179876575464e4658fce0c304e8c15b" + "reference": "a147c0f826c4a1f3afb763ab8e009e37c877a44d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/7a14736fb179876575464e4658fce0c304e8c15b", - "reference": "7a14736fb179876575464e4658fce0c304e8c15b", + "url": "https://api.github.com/repos/symfony/string/zipball/a147c0f826c4a1f3afb763ab8e009e37c877a44d", + "reference": "a147c0f826c4a1f3afb763ab8e009e37c877a44d", "shasum": "" }, "require": { @@ -9646,7 +9638,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.3" + "source": "https://github.com/symfony/string/tree/v6.4.8" }, "funding": [ { @@ -9662,20 +9654,20 @@ "type": "tidelift" } ], - "time": "2024-01-25T09:26:29+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.4.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "06450585bf65e978026bda220cdebca3f867fde7" + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/06450585bf65e978026bda220cdebca3f867fde7", - "reference": "06450585bf65e978026bda220cdebca3f867fde7", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", "shasum": "" }, "require": { @@ -9684,7 +9676,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -9724,7 +9716,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0" }, "funding": [ { @@ -9740,20 +9732,20 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/twig-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "bf6b411a5d9a0ce6ea43cca0fcf5f05f5196a957" + "reference": "57de1b7d7499053a2c5beb9344751e8bfd332649" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/bf6b411a5d9a0ce6ea43cca0fcf5f05f5196a957", - "reference": "bf6b411a5d9a0ce6ea43cca0fcf5f05f5196a957", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/57de1b7d7499053a2c5beb9344751e8bfd332649", + "reference": "57de1b7d7499053a2c5beb9344751e8bfd332649", "shasum": "" }, "require": { @@ -9833,7 +9825,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/twig-bridge/tree/v6.4.8" }, "funding": [ { @@ -9849,20 +9841,20 @@ "type": "tidelift" } ], - "time": "2024-01-30T08:32:12+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/twig-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/twig-bundle.git", - "reference": "2e63e50de2ade430191af0b5d21bfd6526fe3709" + "reference": "ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/2e63e50de2ade430191af0b5d21bfd6526fe3709", - "reference": "2e63e50de2ade430191af0b5d21bfd6526fe3709", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65", + "reference": "ef17bc8fc2cb2376b235cd1b98f0275a78c5ba65", "shasum": "" }, "require": { @@ -9917,7 +9909,7 @@ "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/twig-bundle/tree/v6.4.8" }, "funding": [ { @@ -9933,20 +9925,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/validator", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "9c1d8bb4edce5304fcefca7923741085f1ca5b60" + "reference": "dab2781371d54c86f6b25623ab16abb2dde2870c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/9c1d8bb4edce5304fcefca7923741085f1ca5b60", - "reference": "9c1d8bb4edce5304fcefca7923741085f1ca5b60", + "url": "https://api.github.com/repos/symfony/validator/zipball/dab2781371d54c86f6b25623ab16abb2dde2870c", + "reference": "dab2781371d54c86f6b25623ab16abb2dde2870c", "shasum": "" }, "require": { @@ -9993,7 +9985,8 @@ "Symfony\\Component\\Validator\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/Resources/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -10013,7 +10006,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.3" + "source": "https://github.com/symfony/validator/tree/v6.4.8" }, "funding": [ { @@ -10029,20 +10022,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T15:01:07+00:00" + "time": "2024-06-02T15:48:50+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "0435a08f69125535336177c29d56af3abc1f69da" + "reference": "ad23ca4312395f0a8a8633c831ef4c4ee542ed25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0435a08f69125535336177c29d56af3abc1f69da", - "reference": "0435a08f69125535336177c29d56af3abc1f69da", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ad23ca4312395f0a8a8633c831ef4c4ee542ed25", + "reference": "ad23ca4312395f0a8a8633c831ef4c4ee542ed25", "shasum": "" }, "require": { @@ -10098,7 +10091,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.3" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.8" }, "funding": [ { @@ -10114,20 +10107,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:53:30+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "a8c12b5448a5ac685347f5eeb2abf6a571ec16b8" + "reference": "792ca836f99b340f2e9ca9497c7953948c49a504" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/a8c12b5448a5ac685347f5eeb2abf6a571ec16b8", - "reference": "a8c12b5448a5ac685347f5eeb2abf6a571ec16b8", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/792ca836f99b340f2e9ca9497c7953948c49a504", + "reference": "792ca836f99b340f2e9ca9497c7953948c49a504", "shasum": "" }, "require": { @@ -10135,6 +10128,8 @@ "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", @@ -10173,7 +10168,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.3" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.8" }, "funding": [ { @@ -10189,20 +10184,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/web-profiler-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "e78f98da7b4f842bab89368d53c962f5b2f9e49c" + "reference": "bcc806d1360991de3bf78ac5ca0202db85de9bfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/e78f98da7b4f842bab89368d53c962f5b2f9e49c", - "reference": "e78f98da7b4f842bab89368d53c962f5b2f9e49c", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/bcc806d1360991de3bf78ac5ca0202db85de9bfc", + "reference": "bcc806d1360991de3bf78ac5ca0202db85de9bfc", "shasum": "" }, "require": { @@ -10255,7 +10250,7 @@ "dev" ], "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.8" }, "funding": [ { @@ -10271,20 +10266,20 @@ "type": "tidelift" } ], - "time": "2024-01-28T15:49:46+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d75715985f0f94f978e3a8fa42533e10db921b90" + "reference": "52903de178d542850f6f341ba92995d3d63e60c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d75715985f0f94f978e3a8fa42533e10db921b90", - "reference": "d75715985f0f94f978e3a8fa42533e10db921b90", + "url": "https://api.github.com/repos/symfony/yaml/zipball/52903de178d542850f6f341ba92995d3d63e60c9", + "reference": "52903de178d542850f6f341ba92995d3d63e60c9", "shasum": "" }, "require": { @@ -10327,7 +10322,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.3" + "source": "https://github.com/symfony/yaml/tree/v6.4.8" }, "funding": [ { @@ -10343,20 +10338,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "twbs/bootstrap", - "version": "v5.3.2", + "version": "v5.3.3", "source": { "type": "git", "url": "https://github.com/twbs/bootstrap.git", - "reference": "344e912d04b5b6a04482113eff20ab416ff01048" + "reference": "6e1f75f420f68e1d52733b8e407fc7c3766c9dba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/344e912d04b5b6a04482113eff20ab416ff01048", - "reference": "344e912d04b5b6a04482113eff20ab416ff01048", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/6e1f75f420f68e1d52733b8e407fc7c3766c9dba", + "reference": "6e1f75f420f68e1d52733b8e407fc7c3766c9dba", "shasum": "" }, "replace": { @@ -10391,40 +10386,40 @@ ], "support": { "issues": "https://github.com/twbs/bootstrap/issues", - "source": "https://github.com/twbs/bootstrap/tree/v5.3.2" + "source": "https://github.com/twbs/bootstrap/tree/v5.3.3" }, - "time": "2023-09-14T14:19:27+00:00" + "time": "2024-02-20T15:14:29+00:00" }, { "name": "twig/extra-bundle", - "version": "v3.8.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "32807183753de0388c8e59f7ac2d13bb47311140" + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/32807183753de0388c8e59f7ac2d13bb47311140", - "reference": "32807183753de0388c8e59f7ac2d13bb47311140", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/cdc6e23aeb7f4953c1039568c3439aab60c56454", + "reference": "cdc6e23aeb7f4953c1039568c3439aab60c56454", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/framework-bundle": "^5.4|^6.0|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.4|^7.0", "twig/twig": "^3.0" }, "require-dev": { "league/commonmark": "^1.0|^2.0", "symfony/phpunit-bridge": "^6.4|^7.0", "twig/cache-extra": "^3.0", - "twig/cssinliner-extra": "^2.12|^3.0", - "twig/html-extra": "^2.12|^3.0", - "twig/inky-extra": "^2.12|^3.0", - "twig/intl-extra": "^2.12|^3.0", - "twig/markdown-extra": "^2.12|^3.0", - "twig/string-extra": "^2.12|^3.0" + "twig/cssinliner-extra": "^3.0", + "twig/html-extra": "^3.0", + "twig/inky-extra": "^3.0", + "twig/intl-extra": "^3.0", + "twig/markdown-extra": "^3.0", + "twig/string-extra": "^3.0" }, "type": "symfony-bundle", "autoload": { @@ -10455,7 +10450,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.8.0" + "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.10.0" }, "funding": [ { @@ -10467,24 +10462,25 @@ "type": "tidelift" } ], - "time": "2023-11-21T14:02:01+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/markdown-extra", - "version": "v3.8.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/markdown-extra.git", - "reference": "b6e4954ab60030233df5d293886b5404558daac8" + "reference": "e4bf2419df819dcf9dc7a0b25dd8cd1092cbd86d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/b6e4954ab60030233df5d293886b5404558daac8", - "reference": "b6e4954ab60030233df5d293886b5404558daac8", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/e4bf2419df819dcf9dc7a0b25dd8cd1092cbd86d", + "reference": "e4bf2419df819dcf9dc7a0b25dd8cd1092cbd86d", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "twig/twig": "^3.0" }, "require-dev": { @@ -10496,6 +10492,9 @@ }, "type": "library", "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Twig\\Extra\\Markdown\\": "" }, @@ -10523,7 +10522,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/markdown-extra/tree/v3.8.0" + "source": "https://github.com/twigphp/markdown-extra/tree/v3.10.0" }, "funding": [ { @@ -10535,25 +10534,25 @@ "type": "tidelift" } ], - "time": "2023-11-21T14:02:01+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/string-extra", - "version": "v3.8.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/twigphp/string-extra.git", - "reference": "b0c9037d96baff79abe368dc092a59b726517548" + "reference": "cd76ed8ae081bcd4fddf549e92e20c5df76c358a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/string-extra/zipball/b0c9037d96baff79abe368dc092a59b726517548", - "reference": "b0c9037d96baff79abe368dc092a59b726517548", + "url": "https://api.github.com/repos/twigphp/string-extra/zipball/cd76ed8ae081bcd4fddf549e92e20c5df76c358a", + "reference": "cd76ed8ae081bcd4fddf549e92e20c5df76c358a", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/string": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.4|^7.0", "symfony/translation-contracts": "^1.1|^2|^3", "twig/twig": "^3.0" }, @@ -10590,7 +10589,7 @@ "unicode" ], "support": { - "source": "https://github.com/twigphp/string-extra/tree/v3.8.0" + "source": "https://github.com/twigphp/string-extra/tree/v3.10.0" }, "funding": [ { @@ -10602,34 +10601,41 @@ "type": "tidelift" } ], - "time": "2023-11-21T14:02:01+00:00" + "time": "2024-05-11T07:35:57+00:00" }, { "name": "twig/twig", - "version": "v3.8.0", + "version": "v3.10.3", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" + "reference": "67f29781ffafa520b0bbfbd8384674b42db04572" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", - "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572", + "reference": "67f29781ffafa520b0bbfbd8384674b42db04572", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3", "symfony/polyfill-php80": "^1.22" }, "require-dev": { "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0" + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -10662,7 +10668,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.8.0" + "source": "https://github.com/twigphp/Twig/tree/v3.10.3" }, "funding": [ { @@ -10674,7 +10680,7 @@ "type": "tidelift" } ], - "time": "2023-11-21T18:54:41+00:00" + "time": "2024-05-16T10:04:27+00:00" }, { "name": "webmozart/assert", @@ -10835,16 +10841,16 @@ }, { "name": "zircote/swagger-php", - "version": "4.8.3", + "version": "4.10.0", "source": { "type": "git", "url": "https://github.com/zircote/swagger-php.git", - "reference": "598958d8a83cfbd44ba36388b2f9ed69e8b86ed4" + "reference": "2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/598958d8a83cfbd44ba36388b2f9ed69e8b86ed4", - "reference": "598958d8a83cfbd44ba36388b2f9ed69e8b86ed4", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56", + "reference": "2d983ce67b9eb7e18403ae7bc5e765f8ce7b8d56", "shasum": "" }, "require": { @@ -10858,7 +10864,7 @@ "require-dev": { "composer/package-versions-deprecated": "^1.11", "doctrine/annotations": "^1.7 || ^2.0", - "friendsofphp/php-cs-fixer": "^2.17 || ^3.0", + "friendsofphp/php-cs-fixer": "^2.17 || ^3.47.1", "phpstan/phpstan": "^1.6", "phpunit/phpunit": ">=8", "vimeo/psalm": "^4.23" @@ -10910,31 +10916,30 @@ ], "support": { "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.8.3" + "source": "https://github.com/zircote/swagger-php/tree/4.10.0" }, - "time": "2024-01-07T22:33:09+00:00" + "time": "2024-06-06T22:42:02+00:00" } ], "packages-dev": [ { "name": "dama/doctrine-test-bundle", - "version": "v8.0.1", + "version": "v8.2.0", "source": { "type": "git", "url": "https://github.com/dmaicher/doctrine-test-bundle.git", - "reference": "e382d27bc03ee04e0fd0ef95391047042792e7cc" + "reference": "1f81a280ea63f049d24e9c8ce00e557b18e0ff2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/e382d27bc03ee04e0fd0ef95391047042792e7cc", - "reference": "e382d27bc03ee04e0fd0ef95391047042792e7cc", + "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/1f81a280ea63f049d24e9c8ce00e557b18e0ff2f", + "reference": "1f81a280ea63f049d24e9c8ce00e557b18e0ff2f", "shasum": "" }, "require": { "doctrine/dbal": "^3.3 || ^4.0", - "doctrine/doctrine-bundle": "^2.2.2", - "ext-json": "*", - "php": "^7.3 || ^8.0", + "doctrine/doctrine-bundle": "^2.11.0", + "php": "^7.4 || ^8.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "symfony/cache": "^5.4 || ^6.3 || ^7.0", "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0" @@ -10943,7 +10948,7 @@ "behat/behat": "^3.0", "friendsofphp/php-cs-fixer": "^3.27", "phpstan/phpstan": "^1.2", - "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0 || ^11.0", "symfony/phpunit-bridge": "^6.3", "symfony/process": "^5.4 || ^6.3 || ^7.0", "symfony/yaml": "^5.4 || ^6.3 || ^7.0" @@ -10980,22 +10985,22 @@ ], "support": { "issues": "https://github.com/dmaicher/doctrine-test-bundle/issues", - "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.0.1" + "source": "https://github.com/dmaicher/doctrine-test-bundle/tree/v8.2.0" }, - "time": "2023-12-05T16:11:29+00:00" + "time": "2024-05-28T15:41:06+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -11003,11 +11008,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -11033,7 +11039,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -11041,29 +11047,31 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v5.0.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -11071,7 +11079,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -11095,26 +11103,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" }, - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-03-05T20:51:40+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -11155,9 +11164,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -11212,16 +11227,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.57", + "version": "1.11.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e" + "reference": "490f0ae1c92b082f154681d7849aee776a7c1443" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1627b1d03446904aaa77593f370c5201d2ecc34e", - "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/490f0ae1c92b082f154681d7849aee776a7c1443", + "reference": "490f0ae1c92b082f154681d7849aee776a7c1443", "shasum": "" }, "require": { @@ -11264,31 +11279,27 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2024-01-24T11:51:34+00:00" + "time": "2024-06-17T15:10:54+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.3.59", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "9534fcd0b6906c62594146b506acadeabd3a99b3" + "reference": "dd27a3e83777ba0d9e9cedfaf4ebf95ff67b271f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/9534fcd0b6906c62594146b506acadeabd3a99b3", - "reference": "9534fcd0b6906c62594146b506acadeabd3a99b3", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/dd27a3e83777ba0d9e9cedfaf4ebf95ff67b271f", + "reference": "dd27a3e83777ba0d9e9cedfaf4ebf95ff67b271f", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.48" + "phpstan/phpstan": "^1.11" }, "conflict": { "doctrine/collections": "<1.0", @@ -11298,24 +11309,26 @@ "doctrine/persistence": "<1.3" }, "require-dev": { + "cache/array-adapter": "^1.1", "composer/semver": "^3.3.2", - "doctrine/annotations": "^1.11.0", - "doctrine/collections": "^1.6", + "cweagans/composer-patches": "^1.7.3", + "doctrine/annotations": "^1.11 || ^2.0", + "doctrine/collections": "^1.6 || ^2.1", "doctrine/common": "^2.7 || ^3.0", "doctrine/dbal": "^2.13.8 || ^3.3.3", - "doctrine/lexer": "^1.2.1", - "doctrine/mongodb-odm": "^1.3 || ^2.1", - "doctrine/orm": "^2.14.0", - "doctrine/persistence": "^1.3.8 || ^2.2.1", + "doctrine/lexer": "^2.0 || ^3.0", + "doctrine/mongodb-odm": "^1.3 || ^2.4.3", + "doctrine/orm": "^2.16.0", + "doctrine/persistence": "^2.2.1 || ^3.2", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-phpunit": "^1.3.13", "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.5.10", - "ramsey/uuid-doctrine": "^1.5.0", - "symfony/cache": "^4.4.35" + "phpunit/phpunit": "^9.6.16", + "ramsey/uuid": "^4.2", + "symfony/cache": "^5.4" }, "type": "phpstan-extension", "extra": { @@ -11338,22 +11351,22 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.59" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.4.3" }, - "time": "2024-01-18T09:41:35+00:00" + "time": "2024-06-08T05:48:50+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.30", + "version": "9.2.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", - "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", "shasum": "" }, "require": { @@ -11410,7 +11423,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" }, "funding": [ { @@ -11418,7 +11431,7 @@ "type": "github" } ], - "time": "2023-12-22T06:47:57+00:00" + "time": "2024-03-02T06:37:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -11663,16 +11676,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.16", + "version": "9.6.19", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f" + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3767b2c56ce02d01e3491046f33466a1ae60a37f", - "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", "shasum": "" }, "require": { @@ -11746,7 +11759,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.16" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" }, "funding": [ { @@ -11762,20 +11775,20 @@ "type": "tidelift" } ], - "time": "2024-01-19T07:03:14+00:00" + "time": "2024-04-05T04:35:58+00:00" }, { "name": "sebastian/cli-parser", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { @@ -11810,7 +11823,7 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -11818,7 +11831,7 @@ "type": "github" } ], - "time": "2020-09-28T06:08:49+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { "name": "sebastian/code-unit", @@ -12064,16 +12077,16 @@ }, { "name": "sebastian/diff", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", - "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { @@ -12118,7 +12131,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -12126,7 +12139,7 @@ "type": "github" } ], - "time": "2023-05-07T05:35:17+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { "name": "sebastian/environment", @@ -12193,16 +12206,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", "shasum": "" }, "require": { @@ -12258,7 +12271,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" }, "funding": [ { @@ -12266,20 +12279,20 @@ "type": "github" } ], - "time": "2022-09-14T06:03:37+00:00" + "time": "2024-03-02T06:33:00+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.6", + "version": "5.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bde739e7565280bda77be70044ac1047bc007e34" + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", - "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", "shasum": "" }, "require": { @@ -12322,7 +12335,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" }, "funding": [ { @@ -12330,7 +12343,7 @@ "type": "github" } ], - "time": "2023-08-02T09:26:13+00:00" + "time": "2024-03-02T06:35:11+00:00" }, { "name": "sebastian/lines-of-code", @@ -12566,16 +12579,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -12587,7 +12600,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -12608,8 +12621,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -12617,7 +12629,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -12730,16 +12742,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.8.1", + "version": "3.10.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7" + "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877", + "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877", "shasum": "" }, "require": { @@ -12806,20 +12818,20 @@ "type": "open_collective" } ], - "time": "2024-01-11T20:47:48+00:00" + "time": "2024-05-22T21:24:41+00:00" }, { "name": "symfony/debug-bundle", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/debug-bundle.git", - "reference": "425c7760a4e6fdc6cb643c791d32277037c971df" + "reference": "689f1bcb0bd3b945e3c671cbd06274b127c64dc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/425c7760a4e6fdc6cb643c791d32277037c971df", - "reference": "425c7760a4e6fdc6cb643c791d32277037c971df", + "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/689f1bcb0bd3b945e3c671cbd06274b127c64dc9", + "reference": "689f1bcb0bd3b945e3c671cbd06274b127c64dc9", "shasum": "" }, "require": { @@ -12864,7 +12876,7 @@ "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/debug-bundle/tree/v6.4.3" + "source": "https://github.com/symfony/debug-bundle/tree/v6.4.8" }, "funding": [ { @@ -12880,49 +12892,49 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "symfony/maker-bundle", - "version": "v1.52.0", + "version": "v1.60.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "112f9466c94a46ca33dc441eee59a12cd1790757" + "reference": "c305a02a22974670f359d4274c9431e1a191f559" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/112f9466c94a46ca33dc441eee59a12cd1790757", - "reference": "112f9466c94a46ca33dc441eee59a12cd1790757", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/c305a02a22974670f359d4274c9431e1a191f559", + "reference": "c305a02a22974670f359d4274c9431e1a191f559", "shasum": "" }, "require": { "doctrine/inflector": "^2.0", - "nikic/php-parser": "^4.11", + "nikic/php-parser": "^4.18|^5.0", "php": ">=8.1", - "symfony/config": "^6.3|^7.0", - "symfony/console": "^6.3|^7.0", - "symfony/dependency-injection": "^6.3|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.3|^7.0", - "symfony/finder": "^6.3|^7.0", - "symfony/framework-bundle": "^6.3|^7.0", - "symfony/http-kernel": "^6.3|^7.0", - "symfony/process": "^6.3|^7.0" + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" }, "conflict": { - "doctrine/doctrine-bundle": "<2.4", - "doctrine/orm": "<2.10" + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" }, "require-dev": { "composer/semver": "^3.0", "doctrine/doctrine-bundle": "^2.5.0", - "doctrine/orm": "^2.10.0", - "symfony/http-client": "^6.3|^7.0", - "symfony/phpunit-bridge": "^6.3|^7.0", - "symfony/security-core": "^6.3|^7.0", - "symfony/yaml": "^6.3|^7.0", - "twig/twig": "^2.0|^3.0" + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" }, "type": "symfony-bundle", "extra": { @@ -12956,7 +12968,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.52.0" + "source": "https://github.com/symfony/maker-bundle/tree/v1.60.0" }, "funding": [ { @@ -12972,20 +12984,20 @@ "type": "tidelift" } ], - "time": "2023-10-31T18:23:49+00:00" + "time": "2024-06-10T06:03:18+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "d49b4f6dc4690cf2c194311bb498abf0cf4f7485" + "reference": "937f47cc64922f283bb0c474f33415bba0a9fc0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/d49b4f6dc4690cf2c194311bb498abf0cf4f7485", - "reference": "d49b4f6dc4690cf2c194311bb498abf0cf4f7485", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/937f47cc64922f283bb0c474f33415bba0a9fc0d", + "reference": "937f47cc64922f283bb0c474f33415bba0a9fc0d", "shasum": "" }, "require": { @@ -13017,7 +13029,8 @@ "Symfony\\Bridge\\PhpUnit\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -13037,7 +13050,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v6.4.3" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.4.8" }, "funding": [ { @@ -13053,20 +13066,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-06-02T15:48:50+00:00" }, { "name": "symfony/process", - "version": "v6.4.3", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "31642b0818bfcff85930344ef93193f8c607e0a3" + "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/31642b0818bfcff85930344ef93193f8c607e0a3", - "reference": "31642b0818bfcff85930344ef93193f8c607e0a3", + "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5", "shasum": "" }, "require": { @@ -13098,7 +13111,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.3" + "source": "https://github.com/symfony/process/tree/v6.4.8" }, "funding": [ { @@ -13114,20 +13127,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -13156,7 +13169,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -13164,7 +13177,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], diff --git a/webapp/config/autoload.php.in b/webapp/config/autoload.php.in index 5e3601f839..30aaf65bc6 100644 --- a/webapp/config/autoload.php.in +++ b/webapp/config/autoload.php.in @@ -12,8 +12,8 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Composer\Autoload\ClassLoader; // Load the static domserver file if we don't have the constants from it yet -if (!defined('LIBVENDORDIR')) { +if (!defined('VENDORDIR')) { require('@domserver_etcdir@/domserver-static.php'); } -return require LIBVENDORDIR.'/autoload.php'; +return require VENDORDIR.'/autoload.php'; diff --git a/webapp/config/bundles.php b/webapp/config/bundles.php index f759fd8010..2d4ec46fc9 100644 --- a/webapp/config/bundles.php +++ b/webapp/config/bundles.php @@ -1,4 +1,4 @@ - ['all' => true], @@ -17,4 +17,5 @@ DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Sentry\SentryBundle\SentryBundle::class => ['prod' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], ]; diff --git a/webapp/config/packages/doctrine.yaml b/webapp/config/packages/doctrine.yaml index 5b95b7a30b..d04de47c52 100644 --- a/webapp/config/packages/doctrine.yaml +++ b/webapp/config/packages/doctrine.yaml @@ -24,11 +24,12 @@ doctrine: #server_version: '15' orm: auto_generate_proxy_classes: true - enable_lazy_ghost_objects: false report_fields_where_declared: true validate_xml_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true + controller_resolver: + auto_mapping: false mappings: App: type: attribute diff --git a/webapp/config/packages/framework.yaml b/webapp/config/packages/framework.yaml index ed7df0ab36..21a3cec7bc 100644 --- a/webapp/config/packages/framework.yaml +++ b/webapp/config/packages/framework.yaml @@ -4,7 +4,7 @@ framework: esi: false fragments: false http_method_override: true - annotations: true + annotations: false handle_all_throwables: true serializer: enabled: true diff --git a/webapp/config/packages/nelmio_api_doc.yaml b/webapp/config/packages/nelmio_api_doc.yaml index 2453372b42..0038e04abf 100644 --- a/webapp/config/packages/nelmio_api_doc.yaml +++ b/webapp/config/packages/nelmio_api_doc.yaml @@ -1,9 +1,11 @@ nelmio_api_doc: + html_config: + assets_mode: bundle 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 @@ -32,15 +34,12 @@ nelmio_api_doc: schema: type: string examples: - int0: - value: "2" - summary: The Demo contest (datasource=0) - int02: - value: "1" - summary: The Demo practice contest (datasource=0) - string: + demo: value: "demo" - summary: The Demo contest (datasource=1) + summary: The Demo contest + demoprac: + value: "demoprac" + summary: The Demo practice contest balloonId: name: balloonId in: path diff --git a/webapp/config/packages/nelmio_cors.yaml b/webapp/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000000..0e5ea4bee8 --- /dev/null +++ b/webapp/config/packages/nelmio_cors.yaml @@ -0,0 +1,7 @@ +nelmio_cors: + paths: + '^/api': + allow_origin: [ '*' ] + allow_credentials: true + allow_methods: [ 'POST', 'PUT', 'GET', 'DELETE' ] + max_age: 3600 diff --git a/webapp/config/packages/security.yaml b/webapp/config/packages/security.yaml index 90be4c137e..33361a36db 100644 --- a/webapp/config/packages/security.yaml +++ b/webapp/config/packages/security.yaml @@ -33,8 +33,8 @@ security: # API does Basic Auth and IP address auth api: pattern: ^/api + context: domjudge provider: domjudge_db_provider - stateless: true user_checker: App\Security\UserChecker entry_point: App\Security\DOMJudgeIPAuthenticator # SEE NOTE ABOVE IF CHANGING ANYTHING HERE @@ -45,6 +45,7 @@ security: # Provides prometheus metrics metrics: pattern: ^/prometheus/metrics + context: domjudge provider: domjudge_db_provider stateless: true user_checker: App\Security\UserChecker @@ -57,6 +58,7 @@ security: # rest of app does form_login main: pattern: ^/ + context: domjudge provider: domjudge_db_provider user_checker: App\Security\UserChecker entry_point: App\Security\DOMJudgeXHeadersAuthenticator diff --git a/webapp/config/static.yaml.in b/webapp/config/static.yaml.in index 26983977e6..0a5057aa37 100644 --- a/webapp/config/static.yaml.in +++ b/webapp/config/static.yaml.in @@ -7,7 +7,7 @@ parameters: domjudge.webappdir: @domserver_webappdir@ domjudge.libdir: @domserver_libdir@ domjudge.sqldir: @domserver_sqldir@ - domjudge.libvendordir: @domserver_libvendordir@ + domjudge.vendordir: @domserver_webappdir@/vendor domjudge.logdir: @domserver_logdir@ domjudge.rundir: @domserver_rundir@ domjudge.tmpdir: @domserver_tmpdir@ diff --git a/webapp/migrations/Version20210407120356.php b/webapp/migrations/Version20210407120356.php index 4e7ecffcfa..14c263fae3 100644 --- a/webapp/migrations/Version20210407120356.php +++ b/webapp/migrations/Version20210407120356.php @@ -42,7 +42,6 @@ public function up(Schema $schema) : void for ($idx = 0; $idx < $zip->numFiles; $idx++) { $filename = basename($zip->getNameIndex($idx)); $content = $zip->getFromIndex($idx); - $encodedContent = ($content === '' ? '' : ('0x' . strtoupper(bin2hex($content)))); // In doubt make files executable, but try to read it from the zip file. $executableBit = '1'; @@ -52,21 +51,19 @@ public function up(Schema $schema) : void $executableBit = '0'; } $this->connection->executeStatement( - 'INSERT INTO executable_file ' - . '(`immutable_execid`, `filename`, `ranknumber`, `file_content`, `is_executable`) ' - . 'VALUES (' . $immutable_execid . ', "' . $filename . '", ' - . $idx . ', ' . $encodedContent . ', ' - . $executableBit . ')' + 'INSERT INTO executable_file (`immutable_execid`, `filename`, `ranknumber`, `file_content`, `hash`, `is_executable`)' + . ' VALUES (?, ?, ?, ?, ?, ?)', + [$immutable_execid, $filename, $idx, $content, md5($content), $executableBit] ); } $this->connection->executeStatement( - 'UPDATE executable SET immutable_execid = ' - . $immutable_execid . ' WHERE execid = "' . $oldRow['execid'] . '"' + 'UPDATE executable SET immutable_execid = :immutable_execid WHERE execid = :execid', + ['immutable_execid' => $immutable_execid, 'execid' => $oldRow['execid']] ); } - $this->addSql('ALTER TABLE `executable` DROP COLUMN `zipfile`'); + $this->connection->executeStatement('ALTER TABLE `executable` DROP COLUMN `zipfile`'); } } diff --git a/webapp/migrations/Version20230508120033.php b/webapp/migrations/Version20230508120033.php index b3fe9b2e35..d4d7754433 100644 --- a/webapp/migrations/Version20230508120033.php +++ b/webapp/migrations/Version20230508120033.php @@ -19,6 +19,9 @@ public function getDescription(): string public function up(Schema $schema): void { + // Cleanup judgetasks for removed submissions + // This can happen when a full contest has been removed and the full delete cascade has failed. + $this->addSql('DELETE from judgetask WHERE submitid not in (SELECT submitid FROM submission)'); // this up() migration is auto-generated, please modify it to your needs $this->addSql('ALTER TABLE judgetask ADD CONSTRAINT FK_83142B703605A691 FOREIGN KEY (submitid) REFERENCES submission (submitid) ON DELETE CASCADE'); } diff --git a/webapp/migrations/Version20230508163415.php b/webapp/migrations/Version20230508163415.php index 9cef0b66ba..0e59742533 100644 --- a/webapp/migrations/Version20230508163415.php +++ b/webapp/migrations/Version20230508163415.php @@ -4,48 +4,28 @@ namespace DoctrineMigrations; -use App\Entity\ExecutableFile; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20230508163415 extends AbstractMigration { + // The code that was in this file was moved to Version20230508180000.php so + // that it will re-run after Version20230508170000.php. This file has been + // kept as a no-op to prevent warnings about previously executed migrations + // that are not registered. + public function getDescription(): string { - return 'Update hashes of immutable executables'; + return '[Deleted]'; } public function up(Schema $schema): void { - $immutableExecutables = $this->connection->fetchAllAssociative('SELECT immutable_execid FROM immutable_executable'); - foreach ($immutableExecutables as $immutableExecutable) { - $files = $this->connection->fetchAllAssociative('SELECT hash, filename, is_executable FROM executable_file WHERE immutable_execid = :id', ['id' => $immutableExecutable['immutable_execid']]); - uasort($files, fn(array $a, array $b) => strcmp($a['filename'], $b['filename'])); - $newHash = md5( - join( - array_map( - fn(array $file) => $file['hash'] . $file['filename'] . (bool)$file['is_executable'], - $files - ) - ) - ); - $this->connection->executeQuery('UPDATE immutable_executable SET hash = :hash WHERE immutable_execid = :id', [ - 'hash' => $newHash, - 'id' => $immutableExecutable['immutable_execid'], - ]); - } + $this->addSql('-- no-op'); // suppress warning "Migration was executed but did not result in any SQL statements." } public function down(Schema $schema): void { - // We don't handle this case - } - - public function isTransactional(): bool - { - return false; + $this->addSql('-- no-op'); } } diff --git a/webapp/migrations/Version20230508170000.php b/webapp/migrations/Version20230508170000.php new file mode 100644 index 0000000000..beb8955941 --- /dev/null +++ b/webapp/migrations/Version20230508170000.php @@ -0,0 +1,41 @@ +connection->fetchAllAssociative('SELECT execfileid, file_content FROM executable_file WHERE hash IS NULL'); + foreach ($executableFiles as $file) { + $this->addSql('UPDATE executable_file SET hash = :hash WHERE execfileid = :id', [ + 'hash' => md5($file['file_content']), + 'id' => $file['execfileid'] + ]); + } + } + + public function down(Schema $schema): void + { + // We don't handle this case + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20230508180000.php b/webapp/migrations/Version20230508180000.php new file mode 100644 index 0000000000..0d11330f79 --- /dev/null +++ b/webapp/migrations/Version20230508180000.php @@ -0,0 +1,50 @@ +connection->fetchAllAssociative('SELECT immutable_execid FROM immutable_executable'); + foreach ($immutableExecutables as $immutableExecutable) { + $files = $this->connection->fetchAllAssociative('SELECT hash, filename, is_executable FROM executable_file WHERE immutable_execid = :id', ['id' => $immutableExecutable['immutable_execid']]); + uasort($files, fn(array $a, array $b) => strcmp($a['filename'], $b['filename'])); + $newHash = md5( + join( + array_map( + fn(array $file) => $file['hash'] . $file['filename'] . (bool)$file['is_executable'], + $files + ) + ) + ); + $this->addSql('UPDATE immutable_executable SET hash = :hash WHERE immutable_execid = :id', [ + 'hash' => $newHash, + 'id' => $immutableExecutable['immutable_execid'], + ]); + } + } + + public function down(Schema $schema): void + { + // We don't handle this case + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20231120225210.php b/webapp/migrations/Version20231120225210.php index 4f2bedd3cc..7a4d901b0b 100644 --- a/webapp/migrations/Version20231120225210.php +++ b/webapp/migrations/Version20231120225210.php @@ -34,7 +34,7 @@ public function up(Schema $schema): void if ($updated) { $newExtensionsJson = json_encode($extensions); - $this->connection->executeQuery('UPDATE language SET extensions = :extensions WHERE langid = :langid', [ + $this->addSql('UPDATE language SET extensions = :extensions WHERE langid = :langid', [ 'extensions' => $newExtensionsJson, 'langid' => $language['langid'], ]); diff --git a/webapp/migrations/Version20240511091916.php b/webapp/migrations/Version20240511091916.php new file mode 100644 index 0000000000..2418526d17 --- /dev/null +++ b/webapp/migrations/Version20240511091916.php @@ -0,0 +1,94 @@ + 'gnatmake --version', + 'awk'=> 'awk --version', + 'bash'=> 'bash --version', + 'c' => 'gcc --version', + 'cpp' => 'g++ --version', + 'csharp' => 'mcs --version', + 'f95' => 'gfortran --version', + 'hs' => 'ghc --version', + 'java' => 'javac --version', + 'js' => 'nodejs --version', + 'kt' => 'kotlinc --version', + 'lua' => 'luac -v', + 'pas' => 'fpc -iW', + 'pl' => 'perl -v', + 'plg' => 'swipl --version', + 'py3' => 'pypy3 --version', + 'ocaml' => 'ocamlopt --version', + 'r' => 'Rscript --version', + 'rb' => 'ruby --version', + 'rs' => 'rustc --version', + 'scala' => 'scalac --version', + 'sh' => 'md5sum /bin/sh', + 'swift' => 'swiftc --version']; + + private const RUNNER_VERSION_COMMAND = ['awk'=> 'awk --version', + 'bash'=> 'bash --version', + 'csharp' => 'mono --version', + 'java' => 'java --version', + 'js' => 'nodejs --version', + 'kt' => 'kotlin --version', + 'lua' => 'lua -v', + 'pl' => 'perl -v', + 'py3' => 'pypy3 --version', + 'r' => 'Rscript --version', + 'rb' => 'ruby --version', + 'scala' => 'scala --version', + 'sh' => 'md5sum /bin/sh']; + + public function getDescription(): string + { + return 'Fill default version command for compiler/runner.'; + } + + public function up(Schema $schema): void + { + foreach (self::COMPILER_VERSION_COMMAND as $langid => $versionCommand) { + $this->addSql( + "UPDATE language SET compiler_version_command = :compiler_version_command WHERE langid = :langid AND compiler_version_command IS NULL", + ['compiler_version_command' => $versionCommand, 'langid' => $langid] + ); + } + foreach (self::RUNNER_VERSION_COMMAND as $langid => $versionCommand) { + $this->addSql( + "UPDATE language SET runner_version_command = :compiler_version_command WHERE langid = :langid AND runner_version_command IS NULL", + ['compiler_version_command' => $versionCommand, 'langid' => $langid] + ); + } + } + + public function down(Schema $schema): void + { + foreach (self::COMPILER_VERSION_COMMAND as $langid => $versionCommand) { + $this->addSql( + "UPDATE language SET compiler_version_command = NULL WHERE langid = :langid AND compiler_version_command = :compiler_version_command", + ['compiler_version_command' => $versionCommand, 'langid' => $langid] + ); + } + foreach (self::RUNNER_VERSION_COMMAND as $langid => $versionCommand) { + $this->addSql( + "UPDATE language SET runner_version_command = NULL WHERE langid = :langid AND runner_version_command = :compiler_version_command", + ['compiler_version_command' => $versionCommand, 'langid' => $langid] + ); + } + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20240601180624.php b/webapp/migrations/Version20240601180624.php new file mode 100644 index 0000000000..446247dcf1 --- /dev/null +++ b/webapp/migrations/Version20240601180624.php @@ -0,0 +1,38 @@ +addSql('INSERT INTO configuration (name, value) SELECT \'shadow_mode\', 0 FROM configuration WHERE name = \'data_source\' AND value != \'2\''); + $this->addSql('INSERT INTO configuration (name, value) SELECT \'shadow_mode\', 1 FROM configuration WHERE name = \'data_source\' AND value = \'2\''); + $this->addSql('DELETE FROM configuration WHERE name = \'data_source\''); + } + + public function down(Schema $schema): void + { + $this->addSql('INSERT INTO configuration (name, value) SELECT \'data_source\', 1 FROM configuration WHERE name = \'shadow_mode\' AND value = 0'); + $this->addSql('INSERT INTO configuration (name, value) SELECT \'data_source\', 2 FROM configuration WHERE name = \'shadow_mode\' AND value = 1'); + $this->addSql('DELETE FROM configuration WHERE name = \'shadow_mode\''); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20240604202419.php b/webapp/migrations/Version20240604202419.php new file mode 100644 index 0000000000..aaed521051 --- /dev/null +++ b/webapp/migrations/Version20240604202419.php @@ -0,0 +1,70 @@ + 'cid', + 'language' => 'langid', + 'problem' => 'probid', + 'team' => 'teamid', + 'team_affiliation' => 'affilid', + 'team_category' => 'categoryid', + ]; + $notPrefixed = [ + 'clarification' => 'clarid', + 'submission' => 'submitid', + 'user' => 'username', + ]; + + foreach ($djPrefixed as $table => $column) { + $this->setExternalIds($table, $column, 'dj-'); + } + + foreach ($notPrefixed as $table => $column) { + $this->setExternalIds($table, $column); + } + } + + protected function setExternalIds(string $table, string $column, string $prefix = '') + { + $entries = $this->connection->fetchAllAssociative("SELECT $column FROM $table WHERE externalid IS NULL"); + foreach ($entries as $entry) { + $newExternalId = $prefix . $entry[$column]; + // Check if any entity already has this external ID + $existingEntity = $this->connection->fetchAssociative("SELECT externalid FROM $table WHERE externalid = :externalid", ['externalid' => $newExternalId]); + $humanReadableTable = ucfirst(str_replace('_', ' ', $table)); + $this->abortIf((bool)$existingEntity, "$humanReadableTable entity with external ID $newExternalId already exists, manually set a different external ID"); + $this->addSql("UPDATE $table SET externalid = :externalid WHERE $column = :$column", [ + 'externalid' => $newExternalId, + $column => $entry[$column], + ]); + } + } + + public function down(Schema $schema): void + { + // No down migration needed + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/webapp/migrations/Version20240825115643.php b/webapp/migrations/Version20240825115643.php new file mode 100644 index 0000000000..15a68824f0 --- /dev/null +++ b/webapp/migrations/Version20240825115643.php @@ -0,0 +1,66 @@ +dropKeys(); + $this->addKeys(true); + } + + public function down(Schema $schema): void + { + $this->dropKeys(); + $this->addKeys(false); + } + + public function dropKeys(): void + { + $this->addSql('ALTER TABLE debug_package DROP CONSTRAINT FK_9E17399BE0E4FC3E'); + $this->addSql('ALTER TABLE version DROP CONSTRAINT FK_BF1CD3C32271845'); + $this->addSql('ALTER TABLE version DROP CONSTRAINT FK_BF1CD3C3E0E4FC3E'); + $this->addSql('ALTER TABLE judging_run DROP CONSTRAINT FK_29A6E6E13CBA64F2'); + $this->addSql('ALTER TABLE judging_run DROP CONSTRAINT judging_run_ibfk_1'); + $this->addSql('ALTER TABLE judgetask DROP CONSTRAINT judgetask_ibfk_1'); + $this->addSql('ALTER TABLE submission DROP FOREIGN KEY FK_DB055AF3F132696E'); + } + + public function addKeys(bool $isUp): void + { + // foreign-keys that are related to judgehosts are set to null so that no data is lost. + $cascadeClause = $isUp ? 'ON DELETE CASCADE' : ''; + $nullClause = $isUp ? 'ON DELETE SET NULL' : ''; + + $this->addSql('ALTER TABLE version ADD CONSTRAINT `FK_BF1CD3C32271845` FOREIGN KEY (`langid`) REFERENCES `language` (`langid`) ' . $cascadeClause); + $this->addSql('ALTER TABLE version ADD CONSTRAINT `FK_BF1CD3C3E0E4FC3E` FOREIGN KEY (`judgehostid`) REFERENCES `judgehost` (`judgehostid`) ' . $nullClause); + $this->addSql('ALTER TABLE judging_run ADD CONSTRAINT `FK_29A6E6E13CBA64F2` FOREIGN KEY (`judgetaskid`) REFERENCES `judgetask` (`judgetaskid`) ' . $cascadeClause); + $this->addSql('ALTER TABLE judging_run ADD CONSTRAINT `judging_run_ibfk_1` FOREIGN KEY (`testcaseid`) REFERENCES `testcase` (`testcaseid`) ' . $cascadeClause); + $this->addSql('ALTER TABLE judgetask ADD CONSTRAINT `judgetask_ibfk_1` FOREIGN KEY (`judgehostid`) REFERENCES `judgehost` (`judgehostid`) ' . $nullClause); + $this->addSql('ALTER TABLE debug_package ADD CONSTRAINT `FK_9E17399BE0E4FC3E` FOREIGN KEY (`judgehostid`) REFERENCES `judgehost` (`judgehostid`) ' . $nullClause); + + $clause = $isUp ? 'ON DELETE SET NULL' : 'ON DELETE CASCADE'; + $this->addSql('ALTER TABLE submission ADD CONSTRAINT FK_DB055AF3F132696E FOREIGN KEY (userid) REFERENCES user (userid) ' . $clause); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/phpstan.dist.neon b/webapp/phpstan.dist.neon similarity index 67% rename from phpstan.dist.neon rename to webapp/phpstan.dist.neon index 531ebea2c1..52c70905c5 100644 --- a/phpstan.dist.neon +++ b/webapp/phpstan.dist.neon @@ -1,25 +1,25 @@ parameters: level: 6 paths: - - webapp/src - - webapp/tests + - src + - tests excludePaths: - - webapp/src/Utils/Adminer.php + - src/Utils/Adminer.php ignoreErrors: - message: '#Method .* return type has no value type specified in iterable type array#' - path: webapp/tests + path: tests - message: '#Method .* has parameter .* with no value type specified in iterable type array#' - path: webapp/tests + path: tests - message: '#Property .* type has no value type specified in iterable type array#' - path: webapp/tests + path: tests - message: '#PHPDoc tag @var for variable .* has no value type specified in iterable type array#' - path: webapp/tests + path: tests - message: "#Method .* return type has no value type specified in iterable type array#" - path: webapp/src/DataFixtures/Test + path: src/DataFixtures/Test includes: - - lib/vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-doctrine/extension.neon diff --git a/webapp/phpunit.xml.dist b/webapp/phpunit.xml.dist index 516d82a59d..21b6388923 100644 --- a/webapp/phpunit.xml.dist +++ b/webapp/phpunit.xml.dist @@ -17,6 +17,10 @@ + + + + diff --git a/webapp/public/css/bootstrap.min.css b/webapp/public/css/bootstrap.min.css index d380a3793c..8be6e0f2c4 120000 --- a/webapp/public/css/bootstrap.min.css +++ b/webapp/public/css/bootstrap.min.css @@ -1 +1 @@ -../../../lib/vendor/twbs/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file +../../vendor/twbs/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file diff --git a/webapp/public/css/bootstrap.min.css.map b/webapp/public/css/bootstrap.min.css.map index cef2026e83..80d05e88d0 120000 --- a/webapp/public/css/bootstrap.min.css.map +++ b/webapp/public/css/bootstrap.min.css.map @@ -1 +1 @@ -../../../lib/vendor/twbs/bootstrap/dist/css/bootstrap.min.css.map \ No newline at end of file +../../vendor/twbs/bootstrap/dist/css/bootstrap.min.css.map \ No newline at end of file diff --git a/webapp/public/css/dataTables.bootstrap5.min.css b/webapp/public/css/dataTables.bootstrap5.min.css index ed9929d74d..c2bb4544bf 120000 --- a/webapp/public/css/dataTables.bootstrap5.min.css +++ b/webapp/public/css/dataTables.bootstrap5.min.css @@ -1 +1 @@ -../../../lib/vendor/datatables.net/datatables.net-bs5/css/dataTables.bootstrap5.min.css \ No newline at end of file +../../vendor/datatables.net/datatables.net-bs5/css/dataTables.bootstrap5.min.css \ No newline at end of file diff --git a/webapp/public/css/fontawesome-all.min.css b/webapp/public/css/fontawesome-all.min.css index d07e91bcd3..a95a7f46b0 120000 --- a/webapp/public/css/fontawesome-all.min.css +++ b/webapp/public/css/fontawesome-all.min.css @@ -1 +1 @@ -../../../lib/vendor/fortawesome/font-awesome/css/all.min.css \ No newline at end of file +../../vendor/fortawesome/font-awesome/css/all.min.css \ No newline at end of file diff --git a/webapp/public/css/nv.d3.min.css b/webapp/public/css/nv.d3.min.css index 51a5208376..9a26ecc6cd 120000 --- a/webapp/public/css/nv.d3.min.css +++ b/webapp/public/css/nv.d3.min.css @@ -1 +1 @@ -../../../lib/vendor/novus/nvd3/build/nv.d3.min.css \ No newline at end of file +../../vendor/novus/nvd3/build/nv.d3.min.css \ No newline at end of file diff --git a/webapp/public/css/nv.d3.min.css.map b/webapp/public/css/nv.d3.min.css.map index 3a322fb470..220b5722a5 120000 --- a/webapp/public/css/nv.d3.min.css.map +++ b/webapp/public/css/nv.d3.min.css.map @@ -1 +1 @@ -../../../lib/vendor/novus/nvd3/build/nv.d3.min.css.map \ No newline at end of file +../../vendor/novus/nvd3/build/nv.d3.min.css.map \ No newline at end of file diff --git a/webapp/public/css/select2-bootstrap-5-theme.min.css b/webapp/public/css/select2-bootstrap-5-theme.min.css index 407fb9f5f9..cd8fd07b88 120000 --- a/webapp/public/css/select2-bootstrap-5-theme.min.css +++ b/webapp/public/css/select2-bootstrap-5-theme.min.css @@ -1 +1 @@ -../../../lib/vendor/apalfrey/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.min.css \ No newline at end of file +../../vendor/apalfrey/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.min.css \ No newline at end of file diff --git a/webapp/public/css/select2.min.css b/webapp/public/css/select2.min.css index 2eda641b12..adb8a9e6fc 120000 --- a/webapp/public/css/select2.min.css +++ b/webapp/public/css/select2.min.css @@ -1 +1 @@ -../../../lib/vendor/select2/select2/dist/css/select2.min.css \ No newline at end of file +../../vendor/select2/select2/dist/css/select2.min.css \ No newline at end of file diff --git a/webapp/public/flags b/webapp/public/flags index 1dde7d80c9..d54be4395b 120000 --- a/webapp/public/flags +++ b/webapp/public/flags @@ -1 +1 @@ -../../lib/vendor/components/flag-icon-css/flags \ No newline at end of file +../vendor/components/flag-icon-css/flags \ No newline at end of file diff --git a/webapp/public/index.php b/webapp/public/index.php index dccc8f0ae8..1fd07b5fb5 100644 --- a/webapp/public/index.php +++ b/webapp/public/index.php @@ -2,7 +2,7 @@ use App\Kernel; -require_once dirname(__DIR__, 2) . '/lib/vendor/autoload_runtime.php'; +require_once dirname(__DIR__) . '/vendor/autoload_runtime.php'; require_once dirname(__DIR__) . '/config/load_db_secrets.php'; return function (array $context) { diff --git a/webapp/public/js/FileSaver.min.js b/webapp/public/js/FileSaver.min.js index db9159a068..2d4de7b278 120000 --- a/webapp/public/js/FileSaver.min.js +++ b/webapp/public/js/FileSaver.min.js @@ -1 +1 @@ -../../../lib/vendor/eligrey/filesaver/dist/FileSaver.min.js \ No newline at end of file +../../vendor/eligrey/filesaver/dist/FileSaver.min.js \ No newline at end of file diff --git a/webapp/public/js/FileSaver.min.js.map b/webapp/public/js/FileSaver.min.js.map index 587c5cbaa6..8c92a5a1b1 120000 --- a/webapp/public/js/FileSaver.min.js.map +++ b/webapp/public/js/FileSaver.min.js.map @@ -1 +1 @@ -../../../lib/vendor/eligrey/filesaver/dist/FileSaver.min.js.map \ No newline at end of file +../../vendor/eligrey/filesaver/dist/FileSaver.min.js.map \ No newline at end of file diff --git a/webapp/public/js/bootstrap.bundle.min.js b/webapp/public/js/bootstrap.bundle.min.js index a0994828bc..0889d27957 120000 --- a/webapp/public/js/bootstrap.bundle.min.js +++ b/webapp/public/js/bootstrap.bundle.min.js @@ -1 +1 @@ -../../../lib/vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js \ No newline at end of file +../../vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js \ No newline at end of file diff --git a/webapp/public/js/bootstrap.bundle.min.js.map b/webapp/public/js/bootstrap.bundle.min.js.map index 5f0e462a2e..61f03fce66 120000 --- a/webapp/public/js/bootstrap.bundle.min.js.map +++ b/webapp/public/js/bootstrap.bundle.min.js.map @@ -1 +1 @@ -../../../lib/vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js.map \ No newline at end of file +../../vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/webapp/public/js/d3.min.js b/webapp/public/js/d3.min.js index 9fb1b29cbc..27b19c4f7a 120000 --- a/webapp/public/js/d3.min.js +++ b/webapp/public/js/d3.min.js @@ -1 +1 @@ -../../../lib/vendor/mbostock/d3/d3.min.js \ No newline at end of file +../../vendor/mbostock/d3/d3.min.js \ No newline at end of file diff --git a/webapp/public/js/dataTables.bootstrap5.min.js b/webapp/public/js/dataTables.bootstrap5.min.js index 9f856d06ea..3b9c9206a6 120000 --- a/webapp/public/js/dataTables.bootstrap5.min.js +++ b/webapp/public/js/dataTables.bootstrap5.min.js @@ -1 +1 @@ -../../../lib/vendor/datatables.net/datatables.net-bs5/js/dataTables.bootstrap5.min.js \ No newline at end of file +../../vendor/datatables.net/datatables.net-bs5/js/dataTables.bootstrap5.min.js \ No newline at end of file diff --git a/webapp/public/js/dataTables.min.js b/webapp/public/js/dataTables.min.js index 4f945df306..179e4bf625 120000 --- a/webapp/public/js/dataTables.min.js +++ b/webapp/public/js/dataTables.min.js @@ -1 +1 @@ -../../../lib/vendor/datatables.net/datatables.net/js/dataTables.min.js \ No newline at end of file +../../vendor/datatables.net/datatables.net/js/dataTables.min.js \ No newline at end of file diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js index 1cba40b466..9cb4bc1df1 100644 --- a/webapp/public/js/domjudge.js +++ b/webapp/public/js/domjudge.js @@ -55,6 +55,22 @@ function disableNotifications() return true; } +function enableKeys() +{ + setCookie('domjudge_keys', 1); + $("#keys_disable").removeClass('d-none'); + $("#keys_disable").show(); + $("#keys_enable").hide(); +} + +function disableKeys() +{ + setCookie('domjudge_keys', 0); + $("#keys_enable").removeClass('d-none'); + $("#keys_enable").show(); + $("#keys_disable").hide(); +} + // Send a notification if notifications have been enabled. // The options argument is passed to the Notification constructor, // except that the following tags (if found) are interpreted and @@ -117,21 +133,6 @@ function collapse(x) $(x).toggleClass('d-none'); } -function togglelastruns() -{ - var names = {'lastruntime':0, 'lastresult':1, 'lasttcruns':2}; - for (var name in names) { - var cells = document.getElementsByClassName(name); - for (var i = 0; i < cells.length; i++) { - var style = 'inline'; - if (name === 'lasttcruns') { - style = 'table-row'; - } - cells[i].style.display = (cells[i].style.display === 'none') ? style : 'none'; - } - } -} - // TODO: We should probably reload the page if the clock hits contest // start (and end?). function updateClock() @@ -822,5 +823,169 @@ function setupPreviewClarification($input, $previewDiv, previewInitial) { } $(function () { - $('[data-toggle="tooltip"]').tooltip(); + $('[data-bs-toggle="tooltip"]').tooltip(); +}); + +function initializeKeyboardShortcuts() { + var $body = $('body'); + var ignore = false; + $body.on('keydown', function(e) { + var keysCookie = getCookie('domjudge_keys'); + if (keysCookie != 1 && keysCookie != "") { + return; + } + // Check if the user is not typing in an input field. + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + return; + } + var key = e.key.toLowerCase(); + if (key === '?') { + var $keyhelp = $('#keyhelp'); + if ($keyhelp.length) { + $keyhelp.toggleClass('d-none'); + } + return; + } + if (key === 'escape') { + var $keyhelp = $('#keyhelp'); + if ($keyhelp.length && !$keyhelp.hasClass('d-none')) { + $keyhelp.addClass('d-none'); + } + } + + if (!ignore && !e.shiftKey && (key === 'j' || key === 'k')) { + var parts = window.location.href.split('/'); + var lastPart = parts[parts.length - 1]; + var params = lastPart.split('?'); + var currentNumber = parseInt(params[0]); + if (isNaN(currentNumber)) { + return; + } + if (key === 'j') { + parts[parts.length - 1] = currentNumber + 1; + } else if (key === 'k') { + parts[parts.length - 1] = currentNumber - 1; + } + if (params.length > 1) { + parts[parts.length - 1] += '?' + params[1]; + } + window.location = parts.join('/'); + } else if (!ignore && (key === 's' || key === 't' || key === 'p' || key === 'j' || key === 'c')) { + if (e.shiftKey && key === 's') { + window.location = domjudge_base_url + '/jury/scoreboard'; + return; + } + var type = key; + ignore = true; + var oldFunc = null; + var events = $._data($body[0], 'events'); + if (events && events.keydown) { + oldFunc = events.keydown[0].handler; + } + var sequence = ''; + var box = null; + var $sequenceBox = $('
'); + box = $sequenceBox; + $sequenceBox.text(type + sequence); + $sequenceBox.appendTo($body); + $body.on('keydown', function(e) { + // Check if the user is not typing in an input field. + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { + ignore = false; + if (box) { + box.remove(); + } + sequence = ''; + return; + } + if (e.key >= '0' && e.key <= '9') { + sequence += e.key; + box.text(type + sequence); + } else if (e.key === 'Enter') { + ignore = false; + switch (type) { + case 's': + type = 'submissions'; + break; + case 't': + type = 'teams'; + break; + case 'p': + type = 'problems'; + break; + case 'c': + type = 'clarifications'; + break; + case 'j': + window.location = domjudge_base_url + '/jury/submissions/by-judging-id/' + sequence; + return; + } + var redirect_to = domjudge_base_url + '/jury/' + type; + if (sequence) { + redirect_to += '/' + sequence; + } + window.location = redirect_to; + } else { + ignore = false; + if (box) { + box.remove(); + } + sequence = ''; + $body.off('keydown'); + $body.on('keydown', oldFunc); + } + }); + } + }); +} + +// Make sure the items in the desktop scoreboard fit +document.querySelectorAll(".desktop-scoreboard .forceWidth:not(.toolong)").forEach(el => { + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + } +}); + +/** + * Helper method to resize mobile team names and problem badges + */ +function resizeMobileTeamNamesAndProblemBadges() { + // Make team names fit on the screen, but only when the mobile + // scoreboard is visible + const mobileScoreboard = document.querySelector('.mobile-scoreboard'); + if (mobileScoreboard.offsetWidth === 0) { + return; + } + const windowWidth = document.body.offsetWidth; + const teamNameMaxWidth = Math.max(10, windowWidth - 150); + const problemBadgesMaxWidth = Math.max(10, windowWidth - 78); + document.querySelectorAll(".mobile-scoreboard .forceWidth:not(.toolong)").forEach(el => { + el.classList.remove("toolong"); + el.style.maxWidth = teamNameMaxWidth + 'px'; + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + } else { + el.classList.remove("toolong"); + } + }); + document.querySelectorAll(".mobile-scoreboard .mobile-problem-badges:not(.toolong)").forEach(el => { + el.classList.remove("toolong"); + el.style.maxWidth = problemBadgesMaxWidth + 'px'; + if (el instanceof Element && el.scrollWidth > el.offsetWidth) { + el.classList.add("toolong"); + const scale = el.offsetWidth / el.scrollWidth; + const offset = -1 * (el.scrollWidth - el.offsetWidth) / 2; + el.style.transform = `scale(${scale}) translateX(${offset}px)`; + } else { + el.classList.remove("toolong"); + el.style.transform = null; + } + }); +} + +$(function() { + if (document.querySelector('.mobile-scoreboard')) { + window.addEventListener('resize', resizeMobileTeamNamesAndProblemBadges); + resizeMobileTeamNamesAndProblemBadges(); + } }); diff --git a/webapp/public/js/jquery.min.js b/webapp/public/js/jquery.min.js index 6afb7f5993..2380e19217 120000 --- a/webapp/public/js/jquery.min.js +++ b/webapp/public/js/jquery.min.js @@ -1 +1 @@ -../../../lib/vendor/components/jquery/jquery.min.js \ No newline at end of file +../../vendor/components/jquery/jquery.min.js \ No newline at end of file diff --git a/webapp/public/js/nv.d3.min.js b/webapp/public/js/nv.d3.min.js index ab7aa7313e..319ecf7354 120000 --- a/webapp/public/js/nv.d3.min.js +++ b/webapp/public/js/nv.d3.min.js @@ -1 +1 @@ -../../../lib/vendor/novus/nvd3/build/nv.d3.min.js \ No newline at end of file +../../vendor/novus/nvd3/build/nv.d3.min.js \ No newline at end of file diff --git a/webapp/public/js/nv.d3.min.js.map b/webapp/public/js/nv.d3.min.js.map index 6c57c82185..b4dd186d12 120000 --- a/webapp/public/js/nv.d3.min.js.map +++ b/webapp/public/js/nv.d3.min.js.map @@ -1 +1 @@ -../../../lib/vendor/novus/nvd3/build/nv.d3.min.js.map \ No newline at end of file +../../vendor/novus/nvd3/build/nv.d3.min.js.map \ No newline at end of file diff --git a/webapp/public/js/select2.min.js b/webapp/public/js/select2.min.js index f71b2857a3..491cfe1ed4 120000 --- a/webapp/public/js/select2.min.js +++ b/webapp/public/js/select2.min.js @@ -1 +1 @@ -../../../lib/vendor/select2/select2/dist/js/select2.min.js \ No newline at end of file +../../vendor/select2/select2/dist/js/select2.min.js \ No newline at end of file diff --git a/webapp/public/style_domjudge.css b/webapp/public/style_domjudge.css index 629e6b28bc..2f06361835 100644 --- a/webapp/public/style_domjudge.css +++ b/webapp/public/style_domjudge.css @@ -84,9 +84,9 @@ a { .prevsubmit { color: #696969; } .output_text { - border-top: 1px dotted #C0C0C0; - border-bottom: 1px dotted #C0C0C0; - background-color: #FAFAFA; + border-top: 1px dotted #c0c0c0; + border-bottom: 1px dotted #c0c0c0; + background-color: #fafafa; margin: 0; padding: 5px; font-family: monospace; @@ -94,9 +94,9 @@ a { } .clarificationform pre { - border-top: 1px dotted #C0C0C0; - border-bottom: 1px dotted #C0C0C0; - background-color: #FAFAFA; + border-top: 1px dotted #c0c0c0; + border-bottom: 1px dotted #c0c0c0; + background-color: #fafafa; margin: 0; padding: 5px; font-family: monospace; @@ -104,7 +104,7 @@ a { } kbd { - background-color: #FAFAFA; + background-color: #fafafa; color: black; } @@ -219,6 +219,13 @@ del { display: block; overflow: hidden; } + +.mobile-problem-badges { + position: relative; + display: block; + white-space: nowrap; +} + .toolong:after { content: ""; width: 30%; @@ -227,7 +234,7 @@ del { right: 0; bottom: 0; } -.scoreboard .scoreaf { white-space: nowrap; border: 0; text-align: center; } +.scoreboard .scoreaf { white-space: nowrap; border: 0; padding-left: 2px; text-align: center; } .scoreboard .scoreaf img { vertical-align: middle; } .univ { font-size: 80%; @@ -265,14 +272,14 @@ img.affiliation-logo { padding-right: 3pt; } -.score_correct { background: #60e760; } -.score_first { background: #1daa1d !important; } -.score_pending { background: #6666FF; } -.score_incorrect { background: #e87272; } +.score_correct { background: #60e760; } +.score_correct.score_first { background: #1daa1d; } +.score_incorrect { background: #e87272; } +.score_pending { background: #6666ff; } -.gold-medal { background-color: #EEC710 } -.silver-medal { background-color: #AAA } -.bronze-medal { 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; } @@ -337,10 +344,11 @@ td.scorenc { border-color: silver; border-right: 0; } } .countryflag { - height: 30px; - width: 40px; - border-radius: 8px; - padding: 2px; + height: 1.5rem; + width: 2rem; + border-radius: .375rem; + margin: .125rem; + box-shadow: 0 0 0 1px rgba(0, 0, 0, .2); } .select2 img.countryflag { @@ -400,7 +408,7 @@ tr.ignore td, td.ignore, span.ignore { margin-right: auto; } -#teampicture { +.teampicture { width: 100%; border: 1px solid black; } @@ -618,7 +626,9 @@ tr.ignore td, td.ignore, span.ignore { } .problem-badge { - font-size: 100%; + --badge-padding-x: 0.5em; + font-size: 1em; + min-width: 2em; } .tooltip .tooltip-inner { @@ -652,6 +662,68 @@ blockquote { } #contesttimer { - color: DimGray; - margin-left: auto; + color: DimGray; + margin-left: auto; +} + +.lasttcruns, .lastresult { + opacity: 0.5; +} + +.keybox { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #295d8a; + font-size: 200%; + font-weight: bold; + color: white; + padding: 20px; + border-radius: 5px; + z-index: 1000; +} + +#keyhelp { + position: fixed; + height: 90%; + width: 90%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(41, 93, 138, 0.9); + font-size: 150%; + font-weight: bold; + color: white; + padding: 20px; + border-radius: 5px; + z-index: 1001; +} + +#keyhelp code { + color: white; + background-color: black; + padding: 3px; + border-radius: 5px; +} + +.strike-diagonal { + position: relative; + text-align: center; +} + +.strike-diagonal:before { + position: absolute; + content: ""; + left: 0; + top: 50%; + right: 0; + border-top: 2px solid; + border-color: firebrick; + + -webkit-transform:rotate(-35deg); + -moz-transform:rotate(-35deg); + -ms-transform:rotate(-35deg); + -o-transform:rotate(-35deg); + transform:rotate(-35deg); } diff --git a/webapp/public/style_jury.css b/webapp/public/style_jury.css index 9c3ad91837..9cc0bcfc21 100644 --- a/webapp/public/style_jury.css +++ b/webapp/public/style_jury.css @@ -48,6 +48,11 @@ tr.summary td { border-top: 1px solid black; } clickable in the jury scoreboard and for all team cells */ .scoreboard_jury td, .scoreboard_jury th { padding: 0; } +/* show pending submissions using a blue corner */ +.score_pending.score_correct { background: linear-gradient(45deg, #60e760 85%, #6666ff 85%); } +.score_pending.score_correct.score_first { background: linear-gradient(45deg, #1daa1d 85%, #6666ff 85%); } +.score_pending.score_incorrect { background: linear-gradient(45deg, #e87272 85%, #6666ff 85%); } + #submission_layout { width: 100%; } #djlogo { @@ -191,7 +196,7 @@ table.submissions-table { } .devmode { - background-color: #295D8A !important; + background-color: #295d8a !important; } .devmode-icon { @@ -239,3 +244,7 @@ table.table-full-clickable-cell tr .table-button-head-right-right{ .execid { font-family: monospace; } + +.timebutton { + min-width: 9em; +} diff --git a/webapp/public/webfonts b/webapp/public/webfonts index 3478e1aff4..a7aa14cc23 120000 --- a/webapp/public/webfonts +++ b/webapp/public/webfonts @@ -1 +1 @@ -../../lib/vendor/fortawesome/font-awesome/webfonts \ No newline at end of file +../vendor/fortawesome/font-awesome/webfonts \ No newline at end of file diff --git a/webapp/src/Command/AbstractCompareCommand.php b/webapp/src/Command/AbstractCompareCommand.php new file mode 100644 index 0000000000..99c0d2cfc4 --- /dev/null +++ b/webapp/src/Command/AbstractCompareCommand.php @@ -0,0 +1,96 @@ + $compareService + */ + public function __construct( + protected readonly SerializerInterface $serializer, + protected AbstractCompareService $compareService + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('file1', InputArgument::REQUIRED, 'First file to compare') + ->addArgument('file2', InputArgument::REQUIRED, 'Second file to compare'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + $messages = $this->compareService->compareFiles($input->getArgument('file1'), $input->getArgument('file2')); + + return $this->displayMessages($style, $messages) ?? Command::SUCCESS; + } + + /** + * @param Message[] $messages + */ + protected function displayMessages(SymfonyStyle $style, array $messages): ?int + { + if (empty($messages)) { + $style->success('Files match fully'); + return null; + } + + $headers = ['Level', 'Message', 'Source', 'Target']; + $rows = []; + $counts = []; + foreach ($messages as $message) { + if (!isset($counts[$message->type->value])) { + $counts[$message->type->value] = 0; + } + $counts[$message->type->value]++; + $rows[] = [ + $this->formatMessage($message->type, $message->type->value), + $this->formatMessage($message->type, $message->message), + $this->formatMessage($message->type, $message->source ?? ''), + $this->formatMessage($message->type, $message->target ?? ''), + ]; + } + $style->table($headers, $rows); + + $style->newLine(); + foreach ($counts as $type => $count) { + $style->writeln($this->formatMessage(MessageType::from($type), sprintf('Found %d %s(s)', $count, $type))); + } + + if (isset($counts['error'])) { + $style->error('Files have potential critical differences'); + return Command::FAILURE; + } + + $style->success('Files have differences but probably non critical'); + + return null; + } + + protected function formatMessage(MessageType $level, string $message): string + { + $colors = [ + MessageType::ERROR->value => 'red', + MessageType::WARNING->value => 'yellow', + MessageType::INFO->value => 'green', + ]; + return sprintf('%s', $colors[$level->value], $message); + } +} diff --git a/webapp/src/Command/CompareAwardsCommand.php b/webapp/src/Command/CompareAwardsCommand.php new file mode 100644 index 0000000000..757c73f29c --- /dev/null +++ b/webapp/src/Command/CompareAwardsCommand.php @@ -0,0 +1,25 @@ + + */ +#[AsCommand( + name: 'compare:awards', + description: 'Compare awards between two files' +)] +class CompareAwardsCommand extends AbstractCompareCommand +{ + public function __construct( + SerializerInterface $serializer, + AwardCompareService $compareService + ) { + parent::__construct($serializer, $compareService); + } +} diff --git a/webapp/src/Command/CompareResultsCommand.php b/webapp/src/Command/CompareResultsCommand.php new file mode 100644 index 0000000000..f12d0ced8e --- /dev/null +++ b/webapp/src/Command/CompareResultsCommand.php @@ -0,0 +1,25 @@ + + */ +#[AsCommand( + name: 'compare:results', + description: 'Compare results between two files' +)] +class CompareResultsCommand extends AbstractCompareCommand +{ + public function __construct( + SerializerInterface $serializer, + ResultsCompareService $compareService + ) { + parent::__construct($serializer, $compareService); + } +} diff --git a/webapp/src/Command/CompareScoreboardCommand.php b/webapp/src/Command/CompareScoreboardCommand.php new file mode 100644 index 0000000000..c4eb4b8c03 --- /dev/null +++ b/webapp/src/Command/CompareScoreboardCommand.php @@ -0,0 +1,25 @@ + + */ +#[AsCommand( + name: 'compare:scoreboard', + description: 'Compare scoreboard between two files' +)] +class CompareScoreboardCommand extends AbstractCompareCommand +{ + public function __construct( + SerializerInterface $serializer, + ScoreboardCompareService $compareService + ) { + parent::__construct($serializer, $compareService); + } +} diff --git a/webapp/src/Command/ImportEventFeedCommand.php b/webapp/src/Command/ImportEventFeedCommand.php index 1b44a3f27a..4b1960059b 100644 --- a/webapp/src/Command/ImportEventFeedCommand.php +++ b/webapp/src/Command/ImportEventFeedCommand.php @@ -39,6 +39,7 @@ class ImportEventFeedCommand extends Command public function __construct( protected readonly EntityManagerInterface $em, protected readonly ConfigurationService $config, + protected readonly DOMJudgeService $dj, protected readonly TokenStorageInterface $tokenStorage, protected readonly ?Profiler $profiler, protected readonly ExternalContestSourceService $sourceService, @@ -102,15 +103,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $dataSource = (int)$this->config->get('data_source'); - $importDataSource = DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL; - if ($dataSource !== $importDataSource) { - $dataSourceOptions = $this->config->getConfigSpecification()['data_source']->options; - $this->style->error(sprintf( - "data_source configuration setting is set to '%s' but should be '%s'.", - $dataSourceOptions[$dataSource], - $dataSourceOptions[$importDataSource] - )); + if (!$this->dj->shadowMode()) { + $this->style->error("shadow_mode configuration setting is set to 'false' but should be 'true'."); return Command::FAILURE; } diff --git a/webapp/src/Controller/API/AbstractApiController.php b/webapp/src/Controller/API/AbstractApiController.php index 163f000bba..f35de779d9 100644 --- a/webapp/src/Controller/API/AbstractApiController.php +++ b/webapp/src/Controller/API/AbstractApiController.php @@ -79,7 +79,7 @@ protected function getContestId(Request $request): int $qb = $this->getContestQueryBuilder($request->query->getBoolean('onlyActive', false)); $qb - ->andWhere(sprintf('c.%s = :cid', $this->getContestIdField())) + ->andWhere('c.externalid = :cid') ->setParameter('cid', $request->attributes->get('cid')); /** @var Contest|null $contest */ @@ -91,13 +91,4 @@ protected function getContestId(Request $request): int return $contest->getCid(); } - - protected function getContestIdField(): string - { - try { - return $this->eventLogService->externalIdFieldForEntity(Contest::class) ?? 'cid'; - } catch (Exception) { - return 'cid'; - } - } } diff --git a/webapp/src/Controller/API/AbstractRestController.php b/webapp/src/Controller/API/AbstractRestController.php index e59da07a47..ba52dd120f 100644 --- a/webapp/src/Controller/API/AbstractRestController.php +++ b/webapp/src/Controller/API/AbstractRestController.php @@ -39,25 +39,9 @@ protected function performSingleAction(Request $request, string $id): Response // by internal requests. $this->em->clear(); - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID. - $externalIdAlwaysAllowed = [ - 's.submitid', - 'clar.clarid', - ]; - $idField = $this->getIdField(); - if (in_array($idField, $externalIdAlwaysAllowed)) { - $table = explode('.', $idField)[0]; - $queryBuilder = $this->getQueryBuilder($request) - ->andWhere(sprintf('(%s.externalid IS NULL AND %s = :id) OR %s.externalid = :id', $table, $idField, $table)) - ->setParameter('id', $id); - } else { - $queryBuilder = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $idField)) - ->setParameter('id', $id); - } - - $object = $queryBuilder + $object = $this->getQueryBuilder($request) + ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -109,14 +93,12 @@ protected function renderData( /** * Render the given create data using the correct groups. - * - * @param string|int $id */ protected function renderCreateData( Request $request, mixed $data, string $routeType, - $id + int|string $id ): Response { $params = [ 'id' => $id, @@ -161,29 +143,10 @@ protected function listActionHelper(Request $request): array if ($request->query->has('ids')) { $ids = $request->query->all('ids'); - $ids = array_unique($ids); - - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID. - $externalIdAlwaysAllowed = [ - 's.submitid', - 'clar.clarid', - ]; - $idField = $this->getIdField(); - if (in_array($idField, $externalIdAlwaysAllowed)) { - $table = explode('.', $idField)[0]; - $or = $queryBuilder->expr()->orX(); - foreach ($ids as $index => $id) { - $or->add(sprintf('(%s.externalid IS NULL AND %s = :id%s) OR %s.externalid = :id%s', $table, $idField, $index, $table, $index)); - $queryBuilder->setParameter(sprintf('id%s', $index), $id); - } - $queryBuilder->andWhere($or); - } else { - $queryBuilder - ->andWhere(sprintf('%s IN (:ids)', $this->getIdField())) - ->setParameter('ids', $ids); - } + $queryBuilder + ->andWhere(sprintf('%s IN (:ids)', $this->getIdField())) + ->setParameter('ids', $ids); } /** @var array $objects */ diff --git a/webapp/src/Controller/API/AccountController.php b/webapp/src/Controller/API/AccountController.php index a10e4189f1..81f9f60008 100644 --- a/webapp/src/Controller/API/AccountController.php +++ b/webapp/src/Controller/API/AccountController.php @@ -110,7 +110,8 @@ protected function getQueryBuilder(Request $request): QueryBuilder if ($request->query->has('team')) { $queryBuilder - ->andWhere('u.team = :team') + ->leftJoin('u.team', 't') + ->andWhere('t.externalid = :team') ->setParameter('team', $request->query->get('team')); } @@ -119,6 +120,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('u.%s', $this->eventLogService->externalIdFieldForEntity(User::class) ?? 'userid'); + return 'u.externalid'; } } diff --git a/webapp/src/Controller/API/ClarificationController.php b/webapp/src/Controller/API/ClarificationController.php index 3d2f9daadc..e3fa66263c 100644 --- a/webapp/src/Controller/API/ClarificationController.php +++ b/webapp/src/Controller/API/ClarificationController.php @@ -128,8 +128,7 @@ public function addAction( ->join('cp.problem', 'p') ->join('cp.contest', 'c') ->select('cp, c') - ->andWhere(sprintf('p.%s = :problem', - $this->eventLogService->externalIdFieldForEntity(Problem::class) ?? 'probid')) + ->andWhere('p.externalid = :problem') ->andWhere('cp.contest = :contest') ->andWhere('cp.allowSubmit = 1') ->setParameter('problem', $problemId) @@ -151,8 +150,7 @@ public function addAction( $replyTo = $this->em->createQueryBuilder() ->from(Clarification::class, 'c') ->select('c') - ->andWhere(sprintf('c.%s = :clarification', - $this->eventLogService->externalIdFieldForEntity(Clarification::class) ?? 'clarid')) + ->andWhere('c.externalid = :clarification') ->andWhere('c.contest = :contest') ->setParameter('clarification', $replyToId) ->setParameter('contest', $contestId) @@ -169,15 +167,12 @@ public function addAction( // By default, use the team of the user $fromTeam = $this->isGranted('ROLE_API_WRITER') ? null : $this->dj->getUser()->getTeam(); if ($fromTeamId = $clarificationPost->fromTeamId) { - $idField = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $method = sprintf('get%s', ucfirst($idField)); - // If the user is an admin or API writer, allow it to specify the team if ($this->isGranted('ROLE_API_WRITER')) { - $fromTeam = $this->dj->loadTeam($idField, $fromTeamId, $contest); + $fromTeam = $this->dj->loadTeam($fromTeamId, $contest); } elseif (!$fromTeam) { throw new BadRequestHttpException('User does not belong to a team.'); - } elseif ((string)call_user_func([$fromTeam, $method]) !== (string)$fromTeamId) { + } elseif ($fromTeam->getExternalid() !== $fromTeamId) { throw new BadRequestHttpException('Can not create a clarification from a different team.'); } } elseif (!$this->isGranted('ROLE_API_WRITER') && !$fromTeam) { @@ -189,11 +184,9 @@ public function addAction( // By default, send to jury. $toTeam = null; if ($toTeamId = $clarificationPost->toTeamId) { - $idField = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - // If the user is an admin or API writer, allow it to specify the team. if ($this->isGranted('ROLE_API_WRITER')) { - $toTeam = $this->dj->loadTeam($idField, $toTeamId, $contest); + $toTeam = $this->dj->loadTeam($toTeamId, $contest); } else { throw new BadRequestHttpException('Can not create a clarification that is sent to a team.'); } @@ -287,7 +280,8 @@ protected function getQueryBuilder(Request $request): QueryBuilder ->leftJoin('clar.problem', 'p') ->select('clar, c, r, reply, p') ->andWhere('clar.contest = :cid') - ->setParameter('cid', $this->getContestId($request)); + ->setParameter('cid', $this->getContestId($request)) + ->orderBy('clar.clarid'); if (!$this->dj->checkrole('api_reader') && !$this->dj->checkrole('judgehost')) { @@ -313,6 +307,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('clar.%s', $this->eventLogService->externalIdFieldForEntity(Clarification::class) ?? 'clarid'); + return 'clar.externalid'; } } diff --git a/webapp/src/Controller/API/ContestController.php b/webapp/src/Controller/API/ContestController.php index 97312016e7..bea5588e4d 100644 --- a/webapp/src/Controller/API/ContestController.php +++ b/webapp/src/Controller/API/ContestController.php @@ -26,6 +26,7 @@ use Metadata\MetadataFactoryInterface; use Nelmio\ApiDocBundle\Annotation\Model; use OpenApi\Attributes as OA; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; @@ -63,6 +64,7 @@ public function __construct( ConfigurationService $config, EventLogService $eventLogService, protected readonly ImportExportService $importExportService, + protected readonly LoggerInterface $logger, protected readonly AssetUpdateService $assetUpdater ) { parent::__construct($entityManager, $dj, $config, $eventLogService); @@ -415,7 +417,7 @@ public function changeStartTimeAction( if (!$request->request->has('start_time') && !$request->request->has('scoreboard_thaw_time')) { throw new BadRequestHttpException('Missing "start_time" or "scoreboard_thaw_time" in request.'); } - if ($request->request->get('id') != $contest->getApiId($this->eventLogService)) { + if ($request->request->get('id') != $contest->getExternalid()) { throw new BadRequestHttpException('Invalid "id" in request.'); } if ($request->request->has('start_time') && $request->request->has('scoreboard_thaw_time')) { @@ -651,7 +653,7 @@ public function getEventFeedAction( $response->headers->set('Content-Type', 'application/x-ndjson'); $response->setCallback(function () use ($format, $cid, $contest, $request, $since_id, $types, $strict, $stream, $metadataFactory, $kernel) { $lastUpdate = 0; - $lastIdSent = $since_id; + $lastIdSent = max(0, $since_id); // Don't try to look for event_id=0 $typeFilter = false; if ($types) { $typeFilter = explode(',', $types); @@ -714,39 +716,83 @@ public function getEventFeedAction( // Reload the contest as the above method will clear the entity manager. $contest = $this->getContestWithId($request, $cid); + $missingEventRetries = 0; while (true) { // Add missing state events that should have happened already. $this->eventLogService->addMissingStateEvents($contest); - $qb = $this->em->createQueryBuilder() + // We fetch *all* events after the last seen to check that + // we don't skip events that are committed out of order. + $q = $this->em->createQueryBuilder() ->from(Event::class, 'e') ->select('e') ->andWhere('e.eventid > :lastIdSent') ->setParameter('lastIdSent', $lastIdSent) - ->andWhere('e.contest = :cid') - ->setParameter('cid', $contest->getCid()) - ->orderBy('e.eventid', 'ASC'); - - if ($typeFilter !== false) { - $qb = $qb - ->andWhere('e.endpointtype IN (:types)') - ->setParameter('types', $typeFilter); - } - if (!$canViewAll) { - $restricted_types = ['judgements', 'runs', 'clarifications']; - if ($contest->getStarttime() === null || Utils::now() < $contest->getStarttime()) { - $restricted_types[] = 'problems'; + ->orderBy('e.eventid', 'ASC') + ->getQuery(); + + /** @var Event[] $events */ + $events = $q->getResult(); + + // Look for any missing sequential events and wait for them to + // be committed if so. + $missingEvents = false; + $expectedId = $lastIdSent + 1; + $lastFoundId = null; + foreach ($events as $event) { + if ($event->getEventid() !== $expectedId) { + $missingEvents = true; + $lastFoundId = $event->getEventid(); + break; } - $qb = $qb - ->andWhere('e.endpointtype NOT IN (:restricted_types)') - ->setParameter('restricted_types', $restricted_types); + $expectedId++; } + if ($missingEvents) { + if ($missingEventRetries == 0) { + $this->logger->info( + 'Detected missing events %d ... %d, waiting for these to appear', + [$expectedId, $lastFoundId-1] + ); + } + if (++$missingEventRetries < 10) { + usleep(100 * 1000); + continue; + } - $q = $qb->getQuery(); + // We've decided to permanently ignore these non-existing + // events for this connection. The wait for any + // non-committed events was long enough. + // + // There might be multiple non-existing events. Log the + // first consecutive gap of non-existing events. A consecutive + // gap is guaranteed since the events are ordered. + $this->logger->warning( + 'Waited too long for missing events %d ... %d, skipping', + [$expectedId, $lastFoundId-1] + ); + } + $missingEventRetries = 0; - /** @var Event[] $events */ - $events = $q->getResult(); + $numEventsSent = 0; foreach ($events as $event) { + // Filter out unwanted events + if ($event->getContest()->getCid() !== $contest->getCid()) { + continue; + } + if ($typeFilter !== false && + !in_array($event->getEndpointtype(), $typeFilter)) { + continue; + } + if (!$canViewAll) { + $restricted_types = ['judgements', 'runs', 'clarifications']; + if ($contest->getStarttime() === null || Utils::now() < $contest->getStarttime()) { + $restricted_types[] = 'problems'; + } + if (in_array($event->getEndpointtype(), $restricted_types)) { + continue; + } + } + $data = $event->getContent(); // Filter fields with specific access restrictions. if (!$canViewAll) { @@ -814,9 +860,17 @@ public function getEventFeedAction( flush(); $lastUpdate = Utils::now(); $lastIdSent = $event->getEventid(); + $numEventsSent++; + + if ($missingEvents && $event->getEventid() >= $lastFoundId) { + // The first event after the first gap has been emitted. Stop + // emitting events and restart the gap detection logic to find + // any potential gaps after this last emitted event. + break; + } } - if (count($events) == 0) { + if ($numEventsSent == 0) { if (!$stream) { break; } @@ -891,7 +945,7 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('c.%s', $this->eventLogService->externalIdFieldForEntity(Contest::class) ?? 'cid'); + return 'c.externalid'; } /** diff --git a/webapp/src/Controller/API/GeneralInfoController.php b/webapp/src/Controller/API/GeneralInfoController.php index f47ea0c800..7aa0be7d14 100644 --- a/webapp/src/Controller/API/GeneralInfoController.php +++ b/webapp/src/Controller/API/GeneralInfoController.php @@ -137,8 +137,7 @@ public function getStatusAction(): array foreach ($contests as $contest) { $contestStats = $this->dj->getContestStats($contest); $result[] = new ExtendedContestStatus( - $this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_LOCAL - ? (string)$contest->getCid() : $contest->getExternalid(), + $contest->getExternalid(), $contestStats ); } @@ -385,16 +384,4 @@ public function addProblemAction(Request $request): array { return $this->importProblemService->importProblemFromRequest($request); } - - /** - * Get the field to use for getting contests by ID. - */ - protected function getContestIdField(): string - { - try { - return $this->eventLogService->externalIdFieldForEntity(Contest::class) ?? 'cid'; - } catch (Exception) { - return 'cid'; - } - } } diff --git a/webapp/src/Controller/API/GroupController.php b/webapp/src/Controller/API/GroupController.php index 712ee90c8f..40c0ec23b6 100644 --- a/webapp/src/Controller/API/GroupController.php +++ b/webapp/src/Controller/API/GroupController.php @@ -111,10 +111,9 @@ public function addAction( throw new BadRequestHttpException("Error while adding group: $message"); } + /** @var TeamCategory $group */ $group = $saved[0]; - $idField = $this->eventLogService->externalIdFieldForEntity(TeamCategory::class) ?? 'categoryid'; - $method = sprintf('get%s', ucfirst($idField)); - $id = call_user_func([$group, $method]); + $id = $group->getexternalid(); return $this->renderCreateData($request, $saved[0], 'group', $id); } @@ -163,10 +162,9 @@ public function updateAction( throw new BadRequestHttpException("Error while adding group: $message"); } + /** @var TeamCategory $group */ $group = $saved[0]; - $idField = $this->eventLogService->externalIdFieldForEntity(TeamCategory::class) ?? 'categoryid'; - $method = sprintf('get%s', ucfirst($idField)); - $id = call_user_func([$group, $method]); + $id = $group->getexternalid(); return $this->renderCreateData($request, $saved[0], 'group', $id); } @@ -190,6 +188,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('c.%s', $this->eventLogService->externalIdFieldForEntity(TeamCategory::class) ?? 'categoryid'); + return 'c.externalid'; } } diff --git a/webapp/src/Controller/API/JudgehostController.php b/webapp/src/Controller/API/JudgehostController.php index 77f5a3fafe..f9252cc5d3 100644 --- a/webapp/src/Controller/API/JudgehostController.php +++ b/webapp/src/Controller/API/JudgehostController.php @@ -288,7 +288,9 @@ public function updateJudgingAction( ): void { $judgehost = $this->em->getRepository(Judgehost::class)->findOneBy(['hostname' => $hostname]); if (!$judgehost) { - throw new BadRequestHttpException("Who are you and why are you sending us any data?"); + throw new BadRequestHttpException( + 'Register yourself first. You (' . $hostname . ') are not known to us yet.' + ); } $judgingRun = $this->em->getRepository(JudgingRun::class)->findOneBy(['judgetaskid' => $judgetaskid]); @@ -381,15 +383,18 @@ public function updateJudgingAction( } } } else { + $compileMetadata = $request->request->get('compile_metadata'); $this->em->wrapInTransaction(function () use ( $judgehost, $judging, $query, - $output_compile + $output_compile, + $compileMetadata ) { if ($judging->getOutputCompile() === null) { $judging ->setOutputCompile($output_compile) + ->setCompileMetadata(base64_decode($compileMetadata)) ->setResult(Judging::RESULT_COMPILER_ERROR) ->setEndtime(Utils::now()); $this->em->flush(); @@ -426,7 +431,7 @@ public function updateJudgingAction( ->setJudging($judging) ->setContest($judging->getContest()) ->setDescription('Compilation results are different for j' . $judging->getJudgingid()) - ->setJudgehostlog('New compilation output: ' . $output_compile) + ->setJudgehostlog(base64_encode('New compilation output: ' . $output_compile)) ->setTime(Utils::now()) ->setDisabled($disabled); $this->em->persist($error); diff --git a/webapp/src/Controller/API/LanguageController.php b/webapp/src/Controller/API/LanguageController.php index e84be740b0..0415d4120b 100644 --- a/webapp/src/Controller/API/LanguageController.php +++ b/webapp/src/Controller/API/LanguageController.php @@ -148,12 +148,11 @@ public function configureLanguagesAction(Request $request): Response $language->setAllowSubmit(false); } - $idField = $this->eventLogService->externalIdFieldForEntity(Language::class) ?? 'langid'; foreach ($newLanguages as $language) { /** @var Language $language */ $lang_id = $language['id']; $lang = $this->em->getRepository(Language::class)->findOneBy( - [$idField => $lang_id] + ['externalid' => $lang_id] ); if (!$lang) { // TODO: Decide how to handle this case, either erroring out or creating a new language. @@ -225,6 +224,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('lang.%s', $this->eventLogService->externalIdFieldForEntity(Language::class) ?? 'langid'); + return 'lang.externalid'; } } diff --git a/webapp/src/Controller/API/MetricsController.php b/webapp/src/Controller/API/MetricsController.php index 2024e0be3f..c3092f9633 100644 --- a/webapp/src/Controller/API/MetricsController.php +++ b/webapp/src/Controller/API/MetricsController.php @@ -106,6 +106,9 @@ public function prometheusAction(): Response new SubmissionRestriction(visible: true) ); foreach ($submissionCounts as $kind => $count) { + if (!array_key_exists('submissions_' . $kind, $m)) { + continue; + } $m['submissions_' . $kind]->set((int)$count, $labels); } // Get team submission stats for the contest. diff --git a/webapp/src/Controller/API/OrganizationController.php b/webapp/src/Controller/API/OrganizationController.php index 1269cb44b4..68137d99a1 100644 --- a/webapp/src/Controller/API/OrganizationController.php +++ b/webapp/src/Controller/API/OrganizationController.php @@ -110,7 +110,7 @@ public function logoAction(Request $request, string $id): Response { /** @var TeamAffiliation|null $teamAffiliation */ $teamAffiliation = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('ta.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -141,7 +141,7 @@ public function deleteLogoAction(Request $request, string $id): Response $contestId = null; /** @var TeamAffiliation|null $teamAffiliation */ $teamAffiliation = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('ta.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -193,7 +193,7 @@ public function setLogoAction(Request $request, string $id, ValidatorInterface $ { /** @var TeamAffiliation|null $teamAffiliation */ $teamAffiliation = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('ta.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -266,10 +266,9 @@ public function addAction( throw new BadRequestHttpException("Error while adding organization: $message"); } + /** @var TeamAffiliation $organization */ $organization = $saved[0]; - $idField = $this->eventLogService->externalIdFieldForEntity(TeamAffiliation::class) ?? 'affilid'; - $method = sprintf('get%s', ucfirst($idField)); - $id = call_user_func([$organization, $method]); + $id = $organization->getExternalid(); return $this->renderCreateData($request, $saved[0], 'organization', $id); } @@ -296,6 +295,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('ta.%s', $this->eventLogService->externalIdFieldForEntity(TeamAffiliation::class) ?? 'affilid'); + return 'ta.externalid'; } } diff --git a/webapp/src/Controller/API/ProblemController.php b/webapp/src/Controller/API/ProblemController.php index becdab9bbf..ecfab13749 100644 --- a/webapp/src/Controller/API/ProblemController.php +++ b/webapp/src/Controller/API/ProblemController.php @@ -102,10 +102,14 @@ public function addProblemsAction(Request $request): array // Note: we read the JSON as YAML, since any JSON is also YAML and this allows us // to import files with YAML inside them that match the JSON format $data = Yaml::parseFile($file->getRealPath(), Yaml::PARSE_DATETIME); - if ($this->importExportService->importProblemsData($contest, $data, $ids)) { + if ($this->importExportService->importProblemsData($contest, $data, $ids, $messages)) { return $ids; } - throw new BadRequestHttpException("Error while adding problems"); + $message = "Error while adding problems"; + if (!empty($messages)) { + $message .= ': ' . $this->dj->jsonEncode($messages); + } + throw new BadRequestHttpException($message); } /** @@ -154,8 +158,7 @@ public function listAction(Request $request): Response if ($contestProblem instanceof ContestProblemWrapper) { $contestProblem = $contestProblem->getContestProblem(); } - $probid = $this->getIdField() === 'p.probid' ? $contestProblem->getProbid() : $contestProblem->getExternalId(); - if (in_array($probid, $ids)) { + if (in_array($contestProblem->getExternalId(), $ids)) { $objects[] = $item; } } @@ -379,8 +382,7 @@ public function singleAction(Request $request, string $id): Response if ($contestProblem instanceof ContestProblemWrapper) { $contestProblem = $contestProblem->getContestProblem(); } - $probid = $this->getIdField() === 'p.probid' ? $contestProblem->getProbid() : $contestProblem->getExternalId(); - if ($probid == $id) { + if ($contestProblem->getExternalId() == $id) { $object = $item; break; } @@ -459,7 +461,7 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('p.%s', $this->eventLogService->externalIdFieldForEntity(Problem::class) ?? 'probid'); + return 'p.externalid'; } /** diff --git a/webapp/src/Controller/API/ScoreboardController.php b/webapp/src/Controller/API/ScoreboardController.php index f4c986ac39..b3ba4021d7 100644 --- a/webapp/src/Controller/API/ScoreboardController.php +++ b/webapp/src/Controller/API/ScoreboardController.php @@ -192,7 +192,7 @@ public function getScoreboardAction( $contestProblem = $scoreboard->getProblems()[$problemId]; $problem = new Problem( label: $contestProblem->getShortname(), - problemId: $contestProblem->getApiId($this->eventLogService), + problemId: $contestProblem->getProblem()->getExternalid(), numJudged: $matrixItem->numSubmissions, numPending: $matrixItem->numSubmissionsPending, solved: $matrixItem->isCorrect, @@ -217,7 +217,7 @@ public function getScoreboardAction( $row = new Row( rank: $teamScore->rank, - teamId: $teamScore->team->getApiId($this->eventLogService), + teamId: $teamScore->team->getExternalid(), score: $score, problems: $problems, ); diff --git a/webapp/src/Controller/API/SubmissionController.php b/webapp/src/Controller/API/SubmissionController.php index 1d7564056f..292efdf234 100644 --- a/webapp/src/Controller/API/SubmissionController.php +++ b/webapp/src/Controller/API/SubmissionController.php @@ -136,20 +136,18 @@ public function addSubmissionAction( // By default, use the user and team of the user. $user = $this->dj->getUser(); $team = $user->getTeam(); - if ($teamId = $addSubmission->teamId) { - $idField = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $method = sprintf('get%s', ucfirst($idField)); - + $teamId = $addSubmission->teamId; + if ($teamId) { // If the user is an admin or API writer, allow it to specify the team. if ($this->isGranted('ROLE_API_WRITER')) { /** @var Contest $contest */ $contest = $this->em->getRepository(Contest::class)->find($this->getContestId($request)); /** @var Team $team */ - $team = $this->dj->loadTeam($idField, $teamId, $contest); + $team = $this->dj->loadTeam($teamId, $contest); $user = $team->getUsers()->first() ?: null; } elseif (!$team) { throw new BadRequestHttpException('User does not belong to a team.'); - } elseif ((string)call_user_func([$team, $method]) !== (string)$teamId) { + } elseif ($team->getExternalid() !== $teamId) { throw new BadRequestHttpException('Can not submit for a different team.'); } } elseif (!$team) { @@ -187,8 +185,7 @@ public function addSubmissionAction( ->join('cp.problem', 'p') ->join('cp.contest', 'c') ->select('cp, c') - ->andWhere(sprintf('p.%s = :problem', - $this->eventLogService->externalIdFieldForEntity(Problem::class) ?? 'probid')) + ->andWhere('p.externalid = :problem') ->andWhere('cp.contest = :contest') ->andWhere('cp.allowSubmit = 1') ->setParameter('problem', $problemId) @@ -206,8 +203,7 @@ public function addSubmissionAction( $language = $this->em->createQueryBuilder() ->from(Language::class, 'lang') ->select('lang') - ->andWhere(sprintf('lang.%s = :language', - $this->eventLogService->externalIdFieldForEntity(Language::class) ?? 'langid')) + ->andWhere('lang.externalid = :language') ->andWhere('lang.allowSubmit = 1') ->setParameter('language', $languageId) ->getQuery() @@ -255,7 +251,7 @@ public function addSubmissionAction( $existingSubmission = $this->em->createQueryBuilder() ->from(Submission::class, 's') ->select('s') - ->andWhere('(s.externalid IS NULL AND s.submitid = :submitid) OR s.externalid = :submitid') + ->andWhere('s.externalid = :submitid') ->andWhere('s.contest = :contest') ->setParameter('submitid', $submissionId) ->setParameter('contest', $problem->getContest()) @@ -372,12 +368,7 @@ public function getSubmissionFilesAction(Request $request, string $id): Response ->select('s, f') ->setParameter('id', $id); - $idField = $this->getIdField(); - if ($idField === 's.submitid') { - $queryBuilder->andWhere('(s.externalid IS NULL AND s.submitid = :id) OR s.externalid = :id'); - } else { - $queryBuilder->andWhere(sprintf('%s = :id', $idField)); - } + $queryBuilder->andWhere('s.externalid = :id'); /** @var Submission[] $submissions */ $submissions = $queryBuilder->getQuery()->getResult(); @@ -487,6 +478,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('s.%s', $this->eventLogService->externalIdFieldForEntity(Submission::class) ?? 'submitid'); + return 's.externalid'; } } diff --git a/webapp/src/Controller/API/TeamController.php b/webapp/src/Controller/API/TeamController.php index eff212911e..ee7fb2c5e3 100644 --- a/webapp/src/Controller/API/TeamController.php +++ b/webapp/src/Controller/API/TeamController.php @@ -84,6 +84,9 @@ public function __construct( )] public function listAction(Request $request): Response { + if (!$this->config->get('enable_ranking') && !$this->dj->checkrole('jury')) { + throw new BadRequestHttpException("teams list not available."); + } return parent::performListAction($request); } @@ -101,6 +104,9 @@ public function listAction(Request $request): Response #[OA\Parameter(ref: '#/components/parameters/id')] public function singleAction(Request $request, string $id): Response { + if (!$this->config->get('enable_ranking') && !$this->dj->checkrole('jury')) { + throw new BadRequestHttpException("team not available."); + } return parent::performSingleAction($request, $id); } @@ -121,9 +127,12 @@ public function singleAction(Request $request, string $id): Response #[OA\Parameter(ref: '#/components/parameters/id')] public function photoAction(Request $request, string $id): Response { + if (!$this->config->get('enable_ranking') && !$this->dj->checkrole('jury')) { + throw new BadRequestHttpException("team photo not available."); + } /** @var Team|null $team */ $team = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('t.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -153,7 +162,7 @@ public function deletePhotoAction(Request $request, string $id): Response $contestId = null; /** @var Team|null $team */ $team = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('t.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -205,7 +214,7 @@ public function setPhotoAction(Request $request, string $id, ValidatorInterface { /** @var Team|null $team */ $team = $this->getQueryBuilder($request) - ->andWhere(sprintf('%s = :id', $this->getIdField())) + ->andWhere('t.externalid = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); @@ -284,10 +293,9 @@ public function addAction( throw new BadRequestHttpException("Error while adding team: $message"); } + /** @var Team $team */ $team = $saved[0]; - $idField = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $method = sprintf('get%s', ucfirst($idField)); - $id = call_user_func([$team, $method]); + $id = $team->getExternalid(); return $this->renderCreateData($request, $saved[0], 'team', $id); } @@ -332,6 +340,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('t.%s', $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'); + return 't.externalid'; } } diff --git a/webapp/src/Controller/API/UserController.php b/webapp/src/Controller/API/UserController.php index 00d3a70c5f..00d89a4dbb 100644 --- a/webapp/src/Controller/API/UserController.php +++ b/webapp/src/Controller/API/UserController.php @@ -343,7 +343,7 @@ public function updateAction( protected function addOrUpdateUser(AddUser $addUser, Request $request): Response { - if ($addUser instanceof UpdateUser && $this->eventLogService->externalIdFieldForEntity(User::class) && !$addUser->id) { + if ($addUser instanceof UpdateUser && !$addUser->id) { throw new BadRequestHttpException('`id` field is required'); } @@ -353,7 +353,7 @@ protected function addOrUpdateUser(AddUser $addUser, Request $request): Response $user = new User(); if ($addUser instanceof UpdateUser) { - $existingUser = $this->em->getRepository(User::class)->findOneBy([$this->eventLogService->externalIdFieldForEntity(User::class) => $addUser->id]); + $existingUser = $this->em->getRepository(User::class)->findOneBy(['externalid' => $addUser->id]); if ($existingUser) { $user = $existingUser; } @@ -375,8 +375,7 @@ protected function addOrUpdateUser(AddUser $addUser, Request $request): Response $team = $this->em->createQueryBuilder() ->from(Team::class, 't') ->select('t') - ->andWhere(sprintf('t.%s = :team', - $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid')) + ->andWhere('t.externalid = :team') ->setParameter('team', $addUser->teamId) ->getQuery() ->getOneOrNullResult(); @@ -433,6 +432,6 @@ protected function getQueryBuilder(Request $request): QueryBuilder protected function getIdField(): string { - return sprintf('u.%s', $this->eventLogService->externalIdFieldForEntity(User::class) ?? 'userid'); + return 'u.externalid'; } } diff --git a/webapp/src/Controller/BaseController.php b/webapp/src/Controller/BaseController.php index 64ee36b090..4a75c61a6f 100644 --- a/webapp/src/Controller/BaseController.php +++ b/webapp/src/Controller/BaseController.php @@ -2,9 +2,12 @@ namespace App\Controller; +use App\Doctrine\ExternalIdAlreadyExistsException; use App\Entity\BaseApiEntity; +use App\Entity\CalculatedExternalIdBasedOnRelatedFieldInterface; use App\Entity\Contest; use App\Entity\ContestProblem; +use App\Entity\ExternalIdFromInternalIdInterface; use App\Entity\Problem; use App\Entity\RankCache; use App\Entity\ScoreCache; @@ -20,6 +23,8 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -37,6 +42,13 @@ */ abstract class BaseController extends AbstractController { + public function __construct( + protected readonly EntityManagerInterface $em, + protected readonly EventLogService $eventLog, + protected readonly DOMJudgeService $dj, + protected readonly KernelInterface $kernel, + ) {} + /** * Check whether the referrer in the request is of the current application. */ @@ -53,8 +65,11 @@ protected function isLocalReferer(RouterInterface $router, Request $request): bo /** * Check whether the given referer is local. */ - protected function isLocalRefererUrl(RouterInterface $router, string $referer, string $prefix): bool - { + protected function isLocalRefererUrl( + RouterInterface $router, + string $referer, + string $prefix + ): bool { if (str_starts_with($referer, $prefix)) { $path = substr($referer, strlen($prefix)); if (($questionMark = strpos($path, '?')) !== false) { @@ -79,8 +94,11 @@ protected function isLocalRefererUrl(RouterInterface $router, string $referer, s /** * Redirect to the referrer if it is a known (local) route, otherwise redirect to the given URL. */ - protected function redirectToLocalReferrer(RouterInterface $router, Request $request, string $defaultUrl): RedirectResponse - { + protected function redirectToLocalReferrer( + RouterInterface $router, + Request $request, + string $defaultUrl + ): RedirectResponse { if ($this->isLocalReferer($router, $request)) { return $this->redirect($request->headers->get('referer')); } @@ -92,9 +110,6 @@ protected function redirectToLocalReferrer(RouterInterface $router, Request $req * Save the given entity, adding an eventlog and auditlog entry. */ protected function saveEntity( - EntityManagerInterface $entityManager, - EventLogService $eventLogService, - DOMJudgeService $DOMJudgeService, object $entity, mixed $id, bool $isNewEntity @@ -104,62 +119,62 @@ protected function saveEntity( // Call the prePersist lifecycle callbacks. // This used to work in preUpdate, but Doctrine has deprecated that feature. // See https://www.doctrine-project.org/projects/doctrine-orm/en/3.1/reference/events.html#events-overview. - $metadata = $entityManager->getClassMetadata($entity::class); + $metadata = $this->em->getClassMetadata($entity::class); foreach ($metadata->lifecycleCallbacks['prePersist'] ?? [] as $prePersistMethod) { $entity->$prePersistMethod(); } - $entityManager->persist($entity); - $entityManager->flush(); + $this->em->persist($entity); + $this->em->flush(); // If we have no ID but we do have a Doctrine entity, automatically // get the primary key if possible. if ($id === null) { try { - $metadata = $entityManager->getClassMetadata($entity::class); + $metadata = $this->em->getClassMetadata($entity::class); if (count($metadata->getIdentifierColumnNames()) === 1) { $primaryKey = $metadata->getIdentifierColumnNames()[0]; - $accessor = PropertyAccess::createPropertyAccessor(); - $id = $accessor->getValue($entity, $primaryKey); + $accessor = PropertyAccess::createPropertyAccessor(); + $id = $accessor->getValue($entity, $primaryKey); } } catch (MappingException) { // Entity is not actually a Doctrine entity, ignore. } } - if ($endpoint = $eventLogService->endpointForEntity($entity)) { - foreach ($this->contestsForEntity($entity, $DOMJudgeService) as $contest) { - $eventLogService->log($endpoint, $id, - $isNewEntity ? EventLogService::ACTION_CREATE : EventLogService::ACTION_UPDATE, - $contest->getCid()); + if ($endpoint = $this->eventLog->endpointForEntity($entity)) { + foreach ($this->contestsForEntity($entity) as $contest) { + $this->eventLog->log($endpoint, $id, + $isNewEntity ? EventLogService::ACTION_CREATE : EventLogService::ACTION_UPDATE, + $contest->getCid()); } } - $DOMJudgeService->auditlog($auditLogType, $id, $isNewEntity ? 'added' : 'updated'); + $this->dj->auditlog($auditLogType, $id, $isNewEntity ? 'added' : 'updated'); } /** * Helper function to get the database structure for an object. * * @param string[] $files + * * @return array> */ - protected function getDatabaseRelations(array $files, EntityManagerInterface $entityManager): array - { + protected function getDatabaseRelations(array $files): array { $relations = []; foreach ($files as $file) { - $parts = explode('/', $file); + $parts = explode('/', $file); $shortClass = str_replace('.php', '', $parts[count($parts) - 1]); - $class = sprintf('App\\Entity\\%s', $shortClass); + $class = sprintf('App\\Entity\\%s', $shortClass); if (class_exists($class) && !in_array($class, - [RankCache::class, ScoreCache::class, BaseApiEntity::class])) { - $metadata = $entityManager->getClassMetadata($class); + [RankCache::class, ScoreCache::class, BaseApiEntity::class])) { + $metadata = $this->em->getClassMetadata($class); $tableRelations = []; foreach ($metadata->getAssociationMappings() as $associationMapping) { if (isset($associationMapping['joinColumns']) && count($associationMapping['joinColumns']) === 1) { foreach ($associationMapping['joinColumns'] as $joinColumn) { - $type = $joinColumn['onDelete'] ?? null; + $type = $joinColumn['onDelete'] ?? null; $tableRelations[$associationMapping['fieldName']] = [ 'target' => $associationMapping['targetEntity'], 'targetColumn' => $joinColumn['referencedColumnName'], @@ -180,13 +195,7 @@ protected function getDatabaseRelations(array $files, EntityManagerInterface $en * * @param int[] $primaryKeyData */ - protected function commitDeleteEntity( - object $entity, - DOMJudgeService $DOMJudgeService, - EntityManagerInterface $entityManager, - array $primaryKeyData, - EventLogService $eventLogService - ): void { + protected function commitDeleteEntity(object $entity, array $primaryKeyData): void { // Used to remove data from the rank and score caches. $teamId = null; if ($entity instanceof Team) { @@ -195,7 +204,7 @@ protected function commitDeleteEntity( // Get the contests to trigger the event for. We do this before // deleting the entity, since linked data might have vanished. - $contestsForEntity = $this->contestsForEntity($entity, $DOMJudgeService); + $contestsForEntity = $this->contestsForEntity($entity); $cid = null; // Remember the cid to use it in the EventLog later. @@ -205,11 +214,11 @@ protected function commitDeleteEntity( // Add an audit log entry. $auditLogType = Utils::tableForEntity($entity); - $DOMJudgeService->auditlog($auditLogType, implode(', ', $primaryKeyData), 'deleted'); + $this->dj->auditlog($auditLogType, implode(', ', $primaryKeyData), 'deleted'); // Trigger the delete event. We need to do this before deleting the entity to make // sure we can still find the entity in the table. - if ($endpoint = $eventLogService->endpointForEntity($entity)) { + if ($endpoint = $this->eventLog->endpointForEntity($entity)) { foreach ($contestsForEntity as $contest) { // When the $entity is a contest it has no id anymore after the EntityManager->remove // for this reason we either remember it or check all other contests and use their cid. @@ -221,14 +230,14 @@ protected function commitDeleteEntity( $dataId = $entity->getProbid(); } // TODO: cascade deletes. Maybe use getDependentEntities()? - $eventLogService->log($endpoint, $dataId, + $this->eventLog->log($endpoint, $dataId, EventLogService::ACTION_DELETE, $cid, null, null, false); } } // Now actually delete the entity. - $entityManager->wrapInTransaction(function () use ($entityManager, $entity) { + $this->em->wrapInTransaction(function () use ($entity) { if ($entity instanceof Problem) { // Deleting a problem is a special case: // Its dependent tables do not form a tree (but something like a diamond shape), @@ -240,7 +249,7 @@ protected function commitDeleteEntity( // See also https://github.com/DOMjudge/domjudge/issues/243 and associated commits. // First delete judging_runs. - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE jr FROM judging_run jr INNER JOIN judging j ON jr.judgingid = j.judgingid INNER JOIN submission s ON j.submitid = s.submitid @@ -249,32 +258,32 @@ protected function commitDeleteEntity( ); // Then delete submissions which will cascade to judging, judgeTasks and queueTasks. - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE FROM submission WHERE probid = :probid', ['probid' => $entity->getProbid()] ); // Lastly, delete internal errors that are "connected" to this problem. $disabledJson = '{"kind":"problem","probid":' . $entity->getProbid() . '}'; - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE FROM internal_error WHERE disabled = :disabled', ['disabled' => $disabledJson] ); - $entityManager->clear(); - $entity = $entityManager->getRepository(Problem::class)->find($entity->getProbid()); + $this->em->clear(); + $entity = $this->em->getRepository(Problem::class)->find($entity->getProbid()); } - $entityManager->remove($entity); + $this->em->remove($entity); }); if ($entity instanceof Team) { // No need to do this in a transaction, since the chance of a team // with same ID being created at the same time is negligible. - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE FROM scorecache WHERE teamid = :teamid', ['teamid' => $teamId] ); - $entityManager->getConnection()->executeQuery( + $this->em->getConnection()->executeQuery( 'DELETE FROM rankcache WHERE teamid = :teamid', ['teamid' => $teamId] ); @@ -284,32 +293,29 @@ protected function commitDeleteEntity( /** * @param Object[] $entities * @param array> $relations + * * @return array{0: bool, 1: array, 2: string[]} */ - protected function buildDeleteTree( - array $entities, - array $relations, - EntityManagerInterface $entityManager - ): array { - $isError = false; + protected function buildDeleteTree(array $entities, array $relations): array { + $isError = false; $propertyAccessor = PropertyAccess::createPropertyAccessor(); - $inflector = InflectorFactory::create()->build(); - $readableType = str_replace('_', ' ', Utils::tableForEntity($entities[0])); - $metadata = $entityManager->getClassMetadata($entities[0]::class); - $primaryKeyData = []; - $messages = []; + $inflector = InflectorFactory::create()->build(); + $readableType = str_replace('_', ' ', Utils::tableForEntity($entities[0])); + $metadata = $this->em->getClassMetadata($entities[0]::class); + $primaryKeyData = []; + $messages = []; foreach ($entities as $entity) { $primaryKeyDataTemp = []; foreach ($metadata->getIdentifierColumnNames() as $primaryKeyColumn) { $primaryKeyColumnValue = $propertyAccessor->getValue($entity, $primaryKeyColumn); - $primaryKeyDataTemp[] = $primaryKeyColumnValue; + $primaryKeyDataTemp[] = $primaryKeyColumnValue; // Check all relationships. foreach ($relations as $table => $tableRelations) { foreach ($tableRelations as $column => $constraint) { // If the target class and column match, check if there are any entities with this value. if ($constraint['targetColumn'] === $primaryKeyColumn && $constraint['target'] === $entity::class) { - $count = (int)$entityManager->createQueryBuilder() + $count = (int)$this->em->createQueryBuilder() ->from($table, 't') ->select(sprintf('COUNT(t.%s) AS cnt', $column)) ->andWhere(sprintf('t.%s = :value', $column)) @@ -317,8 +323,8 @@ protected function buildDeleteTree( ->getQuery() ->getSingleScalarResult(); if ($count > 0) { - $parts = explode('\\', $table); - $targetEntityType = $parts[count($parts) - 1]; + $parts = explode('\\', $table); + $targetEntityType = $parts[count($parts) - 1]; $targetReadableType = str_replace( '_', ' ', $inflector->tableize($inflector->pluralize($targetEntityType)) @@ -326,13 +332,13 @@ protected function buildDeleteTree( switch ($constraint['type']) { case 'CASCADE': - $message = sprintf('Cascade to %s', $targetReadableType); + $message = sprintf('Cascade to %s', $targetReadableType); $dependentEntities = $this->getDependentEntities($table, $relations); if (!empty($dependentEntities)) { $dependentEntitiesReadable = []; foreach ($dependentEntities as $dependentEntity) { - $parts = explode('\\', $dependentEntity); - $dependentEntityType = $parts[count($parts) - 1]; + $parts = explode('\\', $dependentEntity); + $dependentEntityType = $parts[count($parts) - 1]; $dependentEntitiesReadable[] = str_replace( '_', ' ', $inflector->tableize($inflector->pluralize($dependentEntityType)) @@ -349,11 +355,11 @@ protected function buildDeleteTree( $messages[] = sprintf('Create dangling references in %s', $targetReadableType); break; case null: - $isError = true; + $isError = true; $messages = [ sprintf('%s with %s "%s" is still referenced in %s, cannot delete.', - ucfirst($readableType), $primaryKeyColumn, $primaryKeyColumnValue, - $targetReadableType) + ucfirst($readableType), $primaryKeyColumn, $primaryKeyColumnValue, + $targetReadableType), ]; break 4; } @@ -371,31 +377,32 @@ protected function buildDeleteTree( * Perform delete operation for the given entities. * * @param Object[] $entities + * * @throws DBALException * @throws NoResultException * @throws NonUniqueResultException */ protected function deleteEntities( Request $request, - EntityManagerInterface $entityManager, - DOMJudgeService $DOMJudgeService, - EventLogService $eventLogService, - KernelInterface $kernel, array $entities, string $redirectUrl - ) : Response { + ): Response { // Assume that we only delete entities of the same class. foreach ($entities as $entity) { assert($entities[0]::class === $entity::class); } // Determine all the relationships between all tables using Doctrine cache. - $dir = realpath(sprintf('%s/src/Entity', $kernel->getProjectDir())); - $files = glob($dir . '/*.php'); - $relations = $this->getDatabaseRelations($files, $entityManager); + $dir = realpath(sprintf('%s/src/Entity', $this->kernel->getProjectDir())); + $files = glob($dir . '/*.php'); + $relations = $this->getDatabaseRelations($files); $readableType = str_replace('_', ' ', Utils::tableForEntity($entities[0])); - $messages = []; + $messages = []; - [$isError, $primaryKeyData, $deleteTreeMessages] = $this->buildDeleteTree($entities, $relations, $entityManager); + [ + $isError, + $primaryKeyData, + $deleteTreeMessages, + ] = $this->buildDeleteTree($entities, $relations); if (!empty($deleteTreeMessages)) { $messages = $deleteTreeMessages; } @@ -407,10 +414,10 @@ protected function deleteEntities( $msgList = []; foreach ($entities as $id => $entity) { - $this->commitDeleteEntity($entity, $DOMJudgeService, $entityManager, $primaryKeyData[$id], $eventLogService); + $this->commitDeleteEntity($entity, $primaryKeyData[$id]); $description = $entity->getShortDescription(); $msgList[] = sprintf('Successfully deleted %s %s "%s"', - $readableType, implode(', ', $primaryKeyData[$id]), $description); + $readableType, implode(', ', $primaryKeyData[$id]), $description); } $msg = implode("\n", $msgList); @@ -481,7 +488,7 @@ protected function getDependentEntities(string $entityClass, array $relations): * * @return Contest[] */ - protected function contestsForEntity(mixed $entity, DOMJudgeService $dj): array + protected function contestsForEntity(mixed $entity): array { // Determine contests to emit an event for the given entity: // * If the entity is a Problem entity, use the getContest() @@ -495,7 +502,7 @@ protected function contestsForEntity(mixed $entity, DOMJudgeService $dj): array // Otherwise, use the currently active contests. $contests = []; if ($entity instanceof Team || $entity instanceof TeamCategory) { - $possibleContests = $dj->getCurrentContests(); + $possibleContests = $this->dj->getCurrentContests(); foreach ($possibleContests as $contest) { if ($entity->inContest($contest)) { $contests[] = $contest; @@ -512,7 +519,7 @@ protected function contestsForEntity(mixed $entity, DOMJudgeService $dj): array } elseif (method_exists($entity, 'getContests')) { $contests = $entity->getContests(); } else { - $contests = $dj->getCurrentContests(); + $contests = $this->dj->getCurrentContests(); } return $contests; @@ -536,4 +543,37 @@ protected function streamResponse(RequestStack $requestStack, callable $callback }); return $response; } + + /** + * @param callable(): string $urlGenerator + * @param callable(): ?Response|null $saveCallback + */ + protected function processAddFormForExternalIdEntity( + FormInterface $form, + ExternalIdFromInternalIdInterface|CalculatedExternalIdBasedOnRelatedFieldInterface $entity, + callable $urlGenerator, + ?callable $saveCallback = null + ): ?Response { + if ($form->isSubmitted() && $form->isValid()) { + try { + if ($saveCallback) { + if ($response = $saveCallback()) { + return $response; + } + } else { + $this->saveEntity($entity, null, true); + } + return $this->redirect($urlGenerator()); + } catch (ExternalIdAlreadyExistsException $e) { + $message = sprintf( + 'The auto assigned external ID \'%s\' is already in use. Please type one yourself.', + $e->externalid + ); + $form->get('externalid')->addError(new FormError($message)); + return null; + } + } + + return null; + } } diff --git a/webapp/src/Controller/Jury/AnalysisController.php b/webapp/src/Controller/Jury/AnalysisController.php index 56506ba77f..932f66757d 100644 --- a/webapp/src/Controller/Jury/AnalysisController.php +++ b/webapp/src/Controller/Jury/AnalysisController.php @@ -10,6 +10,7 @@ use App\Service\StatisticsService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr; +use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -99,6 +100,7 @@ public function teamAction(Team $team): Response #[Route(path: '/problem/{probid}', name: 'analysis_problem')] public function problemAction( + #[MapEntity(id: 'probid')] Problem $problem, #[MapQueryParameter] ?string $view = null @@ -118,4 +120,25 @@ public function problemAction( $this->stats->getProblemStats($contest, $problem, $view) ); } + + #[Route(path: '/languages', name: 'analysis_languages')] + public function languagesAction( + #[MapQueryParameter] + ?string $view = null + ): Response { + $contest = $this->dj->getCurrentContest(); + + if ($contest === null) { + return $this->render('jury/error.html.twig', [ + 'error' => 'No contest selected', + ]); + } + + $filterKeys = array_keys(StatisticsService::FILTERS); + $view = $view ?: reset($filterKeys); + + return $this->render('jury/analysis/languages.html.twig', + $this->stats->getLanguagesStats($contest, $view) + ); + } } diff --git a/webapp/src/Controller/Jury/ClarificationController.php b/webapp/src/Controller/Jury/ClarificationController.php index dd73acbbf0..6ec9782859 100644 --- a/webapp/src/Controller/Jury/ClarificationController.php +++ b/webapp/src/Controller/Jury/ClarificationController.php @@ -115,7 +115,6 @@ public function indexAction( 'oldClarifications' => $oldClarifications, 'generalClarifications' => $generalClarifications, 'queues' => $queues, - 'showExternalId' => $this->eventLogService->externalIdFieldForEntity(Clarification::class), 'currentQueue' => $currentQueue, 'currentFilter' => $currentFilter, 'categories' => $categories, @@ -140,7 +139,6 @@ public function viewAction(Request $request, int $id): Response } $parameters = ['list' => []]; - $parameters['showExternalId'] = $this->eventLogService->externalIdFieldForEntity(Clarification::class); $formData = [ 'recipient' => JuryClarificationType::RECIPIENT_MUST_SELECT, diff --git a/webapp/src/Controller/Jury/ContestController.php b/webapp/src/Controller/Jury/ContestController.php index 2fc04de7b4..949c1c4fef 100644 --- a/webapp/src/Controller/Jury/ContestController.php +++ b/webapp/src/Controller/Jury/ContestController.php @@ -54,13 +54,15 @@ class ContestController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, - protected readonly AssetUpdateService $assetUpdater - ) {} + protected readonly AssetUpdateService $assetUpdater, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NonUniqueResultException @@ -81,6 +83,7 @@ public function indexAction(Request $request): Response $table_fields = [ 'cid' => ['title' => 'CID', 'sort' => true], + 'externalid' => ['title' => "external ID", 'sort' => true], 'shortname' => ['title' => 'shortname', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], 'activatetime' => ['title' => 'activate', 'sort' => true], @@ -128,13 +131,6 @@ public function indexAction(Request $request): Response 'num_problems' => ['title' => '# problems', 'sort' => true], ]); - // Insert external ID field when configured to use it - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(Contest::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => "external ID", 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $contests_table = []; foreach ($contests as $contest) { @@ -550,24 +546,19 @@ public function editAction(Request $request, int $contestId): Response $deletedProblems = $getDeletedEntities($contest->getProblems(), 'getProbid'); $this->assetUpdater->updateAssets($contest); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $contest, - $contest->getCid(), false); - - $teamEndpoint = $this->eventLogService->endpointForEntity(Team::class); - $teamCategoryEndpoint = $this->eventLogService->endpointForEntity(TeamCategory::class); - $problemEndpoint = $this->eventLogService->endpointForEntity(Problem::class); + $this->saveEntity($contest, $contest->getCid(), false); // TODO: cascade deletes. Maybe use getDependentEntities()? foreach ($deletedTeams as $team) { - $this->eventLogService->log($teamEndpoint, $team->getTeamid(), + $this->eventLogService->log('teams', $team->getTeamid(), EventLogService::ACTION_DELETE, $contest->getCid(), null, null, false); } foreach ($deletedTeamCategories as $category) { - $this->eventLogService->log($teamCategoryEndpoint, $category->getCategoryid(), + $this->eventLogService->log('groups', $category->getCategoryid(), EventLogService::ACTION_DELETE, $contest->getCid(), null, null, false); } foreach ($deletedProblems as $problem) { - $this->eventLogService->log($problemEndpoint, $problem->getProbid(), + $this->eventLogService->log('problems', $problem->getProbid(), EventLogService::ACTION_DELETE, $contest->getCid(), null, null, false); } return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getcid()]); @@ -595,8 +586,7 @@ public function deleteAction(Request $request, int $contestId): Response return $this->redirectToRoute('jury_contest', ['contestId' => $contestId]); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$contest], $this->generateUrl('jury_contests')); + return $this->deleteEntities($request, [$contest], $this->generateUrl('jury_contests')); } #[IsGranted('ROLE_ADMIN')] @@ -619,8 +609,7 @@ public function deleteProblemAction(Request $request, int $contestId, int $probI return $this->redirectToRoute('jury_contest', ['contestId' => $contestId]); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$contestProblem], $this->generateUrl('jury_contest', ['contestId' => $contestId])); + return $this->deleteEntities($request, [$contestProblem], $this->generateUrl('jury_contest', ['contestId' => $contestId])); } #[IsGranted('ROLE_ADMIN')] @@ -635,38 +624,45 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $response = $this->checkTimezones($form); - if ($response !== null) { - return $response; - } - - $this->em->wrapInTransaction(function () use ($contest) { - // A little 'hack': we need to first persist and save the - // contest, before we can persist and save the problem, - // because we need a contest ID. - /** @var ContestProblem[] $problems */ - $problems = $contest->getProblems()->toArray(); - foreach ($contest->getProblems() as $problem) { - $contest->removeProblem($problem); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $contest, + fn () => $this->generateUrl('jury_contest', ['contestId' => $contest->getcid()]), + function () use ($form, $contest) { + $response = $this->checkTimezones($form); + if ($response !== null) { + return $response; } - $this->em->persist($contest); - $this->em->flush(); - // Now we can assign the problems to the contest and persist them. - foreach ($problems as $problem) { - $problem->setContest($contest); - $this->em->persist($problem); - } - $this->assetUpdater->updateAssets($contest); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $contest, null, true); - // Note that we do not send out create events for problems, - // teams and team categories for this contest. This happens - // when someone connects to the event feed (or we have a - // dependent event) anyway and adding the code here would - // overcomplicate this function. - }); - return $this->redirectToRoute('jury_contest', ['contestId' => $contest->getcid()]); + $this->em->wrapInTransaction(function () use ($contest) { + // A little 'hack': we need to first persist and save the + // contest, before we can persist and save the problem, + // because we need a contest ID. + /** @var ContestProblem[] $problems */ + $problems = $contest->getProblems()->toArray(); + foreach ($contest->getProblems() as $problem) { + $contest->removeProblem($problem); + } + $this->em->persist($contest); + $this->em->flush(); + + // Now we can assign the problems to the contest and persist them. + foreach ($problems as $problem) { + $problem->setContest($contest); + $this->em->persist($problem); + } + $this->assetUpdater->updateAssets($contest); + $this->saveEntity($contest, null, true); + // Note that we do not send out create events for problems, + // teams and team categories for this contest. This happens + // when someone connects to the event feed (or we have a + // dependent event) anyway and adding the code here would + // overcomplicate this function. + }); + + return null; + } + )) { + return $response; } return $this->render('jury/contest_add.html.twig', [ diff --git a/webapp/src/Controller/Jury/ExecutableController.php b/webapp/src/Controller/Jury/ExecutableController.php index 63954b325b..b49ef5e184 100644 --- a/webapp/src/Controller/Jury/ExecutableController.php +++ b/webapp/src/Controller/Jury/ExecutableController.php @@ -34,12 +34,14 @@ class ExecutableController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, - protected readonly EventLogService $eventLogService - ) {} + KernelInterface $kernel, + protected readonly EventLogService $eventLogService, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_executables')] public function indexAction(Request $request): Response @@ -357,8 +359,7 @@ public function viewAction( $executable->setImmutableExecutable( $this->dj->createImmutableExecutable($zip) ); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $executable, - $executable->getExecid(), false); + $this->saveEntity($executable, $executable->getExecid(), false); return $this->redirectToRoute('jury_executable', ['execId' => $executable->getExecid()]); } @@ -482,8 +483,7 @@ public function deleteAction(Request $request, string $execId): Response throw new NotFoundHttpException(sprintf('Executable with ID %s not found', $execId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$executable], $this->generateUrl('jury_executables')); + return $this->deleteEntities($request, [$executable], $this->generateUrl('jury_executables')); } /** diff --git a/webapp/src/Controller/Jury/ExternalContestController.php b/webapp/src/Controller/Jury/ExternalContestController.php index 971a54919d..d96759cc54 100644 --- a/webapp/src/Controller/Jury/ExternalContestController.php +++ b/webapp/src/Controller/Jury/ExternalContestController.php @@ -25,13 +25,15 @@ class ExternalContestController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EventLogService $eventLog, - protected readonly KernelInterface $kernel, - private readonly ExternalContestSourceService $sourceService - ) {} + EventLogService $eventLog, + KernelInterface $kernel, + private readonly ExternalContestSourceService $sourceService, + ) { + parent::__construct($em, $eventLog, $dj, $kernel); + } #[Route(path: '/', name: 'jury_external_contest')] public function indexAction(Request $request): Response @@ -156,7 +158,7 @@ public function manageAction(Request $request): Response if ($form->isSubmitted() && $form->isValid()) { $this->em->persist($externalContestSource); - $this->saveEntity($this->em, $this->eventLog, $this->dj, $externalContestSource, null, true); + $this->saveEntity($externalContestSource, null, true); return $this->redirectToRoute('jury_external_contest'); } diff --git a/webapp/src/Controller/Jury/ImportExportController.php b/webapp/src/Controller/Jury/ImportExportController.php index 922d5de1a8..f8c28b07a1 100644 --- a/webapp/src/Controller/Jury/ImportExportController.php +++ b/webapp/src/Controller/Jury/ImportExportController.php @@ -3,6 +3,7 @@ namespace App\Controller\Jury; use App\Controller\BaseController; +use App\DataTransferObject\ResultRow; use App\Entity\Clarification; use App\Entity\Contest; use App\Entity\ContestProblem; @@ -10,6 +11,7 @@ use App\Entity\TeamCategory; use App\Form\Type\ContestExportType; use App\Form\Type\ContestImportType; +use App\Form\Type\ExportResultsType; use App\Form\Type\ICPCCmsType; use App\Form\Type\JsonImportType; use App\Form\Type\ProblemsImportType; @@ -30,6 +32,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\SubmitButton; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; @@ -44,23 +47,28 @@ use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Twig\Environment; -#[IsGranted('ROLE_ADMIN')] #[Route(path: '/jury/import-export')] +#[IsGranted('ROLE_JURY')] class ImportExportController extends BaseController { public function __construct( protected readonly ICPCCmsService $icpcCmsService, protected readonly ImportExportService $importExportService, - protected readonly EntityManagerInterface $em, + EntityManagerInterface $em, protected readonly ScoreboardService $scoreboardService, - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly EventLogService $eventLogService, protected readonly ImportProblemService $importProblemService, + KernelInterface $kernel, #[Autowire('%domjudge.version%')] - protected readonly string $domjudgeVersion - ) {} + protected readonly string $domjudgeVersion, + protected readonly Environment $twig, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws ClientExceptionInterface @@ -70,6 +78,7 @@ public function __construct( * @throws TransportExceptionInterface */ #[Route(path: '', name: 'jury_import_export')] + #[IsGranted('ROLE_ADMIN')] public function indexAction(Request $request): Response { $tsvForm = $this->createForm(TsvImportType::class); @@ -238,30 +247,86 @@ public function indexAction(Request $request): Response $this->addFlash('danger', "Parse error in YAML/JSON file (" . $file->getClientOriginalName() . "): " . $e->getMessage()); return $this->redirectToRoute('jury_import_export'); } - if ($this->importExportService->importProblemsData($problemsImportForm->get('contest')->getData(), $data)) { + if ($this->importExportService->importProblemsData($problemsImportForm->get('contest')->getData(), $data, $ids, $messages)) { $this->addFlash('success', sprintf('The file %s is successfully imported.', $file->getClientOriginalName())); } else { - $this->addFlash('danger', 'Failed importing problems'); + if (!empty($messages)) { + $this->postMessages($messages); + } else { + $this->addFlash('danger', 'Failed importing problems'); + } } return $this->redirectToRoute('jury_import_export'); } - /** @var TeamCategory[] $teamCategories */ - $teamCategories = $this->em->createQueryBuilder() - ->from(TeamCategory::class, 'c', 'c.categoryid') - ->select('c.sortorder, c.name') - ->where('c.visible = 1') - ->orderBy('c.sortorder') - ->getQuery() - ->getResult(); - $sortOrders = []; - foreach ($teamCategories as $teamCategory) { - $sortOrder = $teamCategory['sortorder']; - if (!array_key_exists($sortOrder, $sortOrders)) { - $sortOrders[$sortOrder] = []; + $exportResultsForm = $this->createForm(ExportResultsType::class); + + $exportResultsForm->handleRequest($request); + + if ($exportResultsForm->isSubmitted() && $exportResultsForm->isValid()) { + $contest = $this->dj->getCurrentContest(); + if ($contest === null) { + throw new BadRequestHttpException('No current contest'); } - $sortOrders[$sortOrder][] = $teamCategory['name']; + + $data = $exportResultsForm->getData(); + $format = $data['format']; + $sortOrder = $data['sortorder']; + $individuallyRanked = $data['individually_ranked']; + $honors = $data['honors']; + + $extension = match ($format) { + 'html_inline', 'html_download' => 'html', + 'tsv' => 'tsv', + default => throw new BadRequestHttpException('Invalid format'), + }; + $contentType = match ($format) { + 'html_inline' => 'text/html', + 'html_download' => 'text/html', + 'tsv' => 'text/csv', + default => throw new BadRequestHttpException('Invalid format'), + }; + $contentDisposition = match ($format) { + 'html_inline' => 'inline', + 'html_download', 'tsv' => 'attachment', + default => throw new BadRequestHttpException('Invalid format'), + }; + $filename = 'results.' . $extension; + + $response = new StreamedResponse(); + $response->setCallback(function () use ( + $format, + $sortOrder, + $individuallyRanked, + $honors + ) { + if ($format === 'tsv') { + $data = $this->importExportService->getResultsData( + $sortOrder->sort_order, + $individuallyRanked, + $honors, + ); + + echo "results\t1\n"; + foreach ($data as $row) { + echo implode("\t", array_map(fn($field) => Utils::toTsvField((string)$field), $row->toArray())) . "\n"; + } + } else { + echo $this->getResultsHtml( + $sortOrder->sort_order, + $individuallyRanked, + $honors, + ); + } + }); + $response->headers->set('Content-Type', $contentType); + $response->headers->set('Content-Disposition', "$contentDisposition; filename=\"$filename\""); + $response->headers->set('Content-Transfer-Encoding', 'binary'); + $response->headers->set('Connection', 'Keep-Alive'); + $response->headers->set('Accept-Ranges', 'bytes'); + + return $response; } return $this->render('jury/import_export.html.twig', [ @@ -272,16 +337,13 @@ public function indexAction(Request $request): Response 'contest_export_form' => $contestExportForm, 'contest_import_form' => $contestImportForm, 'problems_import_form' => $problemsImportForm, - 'sort_orders' => $sortOrders, + 'export_results_form' => $exportResultsForm, ]); } #[Route(path: '/export/{type}.tsv', name: 'jury_tsv_export')] - public function exportTsvAction( - string $type, - #[MapQueryParameter(name: 'sort_order')] - ?int $sortOrder, - ): Response { + public function exportTsvAction(string $type): Response + { $data = []; $tsvType = $type; try { @@ -292,14 +354,6 @@ public function exportTsvAction( case 'teams': $data = $this->importExportService->getTeamData(); break; - case 'wf_results': - $data = $this->importExportService->getResultsData($sortOrder); - $tsvType = 'results'; - break; - case 'full_results': - $data = $this->importExportService->getResultsData($sortOrder, full: true); - $tsvType = 'results'; - break; } } catch (BadRequestHttpException $e) { $this->addFlash('danger', $e->getMessage()); @@ -322,29 +376,22 @@ public function exportTsvAction( return $response; } - #[Route(path: '/export/{type}.html', name: 'jury_html_export')] - public function exportHtmlAction(Request $request, string $type): Response + #[Route(path: '/export/clarifications.html', name: 'jury_html_export_clarifications')] + public function exportClarificationsHtmlAction(): Response { try { - switch ($type) { - case 'wf_results': - return $this->getResultsHtml($request); - case 'full_results': - return $this->getResultsHtml($request, full: true); - case 'clarifications': - return $this->getClarificationsHtml(); - default: - $this->addFlash('danger', "Unknown export type '" . $type . "' requested."); - return $this->redirectToRoute('jury_import_export'); - } + return $this->getClarificationsHtml(); } catch (BadRequestHttpException $e) { $this->addFlash('danger', $e->getMessage()); return $this->redirectToRoute('jury_import_export'); } } - protected function getResultsHtml(Request $request, bool $full = false): Response - { + protected function getResultsHtml( + int $sortOrder, + bool $individuallyRanked, + bool $honors + ): string { /** @var TeamCategory[] $categories */ $categories = $this->em->createQueryBuilder() ->from(TeamCategory::class, 'c', 'c.categoryid') @@ -377,36 +424,37 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons $ranked = []; $honorable = []; $regionWinners = []; + $rankPerTeam = []; - $sortOrder = $request->query->getInt('sort_order'); - - foreach ($this->importExportService->getResultsData($sortOrder, full: $full) as $row) { - $team = $teamNames[$row[0]]; + foreach ($this->importExportService->getResultsData($sortOrder, $individuallyRanked, $honors) as $row) { + $team = $teamNames[$row->teamId]; + $rankPerTeam[$row->teamId] = $row->rank; - if ($row[6] !== '') { + if ($row->groupWinner) { $regionWinners[] = [ - 'group' => $row[6], + 'group' => $row->groupWinner, 'team' => $team, + 'rank' => $row->rank ?? '-', ]; } $row = [ 'team' => $team, - 'rank' => $row[1], - 'award' => $row[2], - 'solved' => $row[3], - 'total_time' => $row[4], - 'max_time' => $row[5], + 'rank' => $row->rank, + 'award' => $row->award, + 'solved' => $row->numSolved, + 'total_time' => $row->totalTime, + 'max_time' => $row->timeOfLastSubmission, ]; if (preg_match('/^(.*) Medal$/', $row['award'], $matches)) { $row['class'] = strtolower($matches[1]); } else { $row['class'] = ''; } - if ($row['rank'] === '') { + if ($row['rank'] === null) { $honorable[] = $row['team']; - } elseif ($row['award'] === 'Ranked') { - $ranked[] = $row; + } elseif (in_array($row['award'], ['Ranked', 'Highest Honors', 'High Honors', 'Honors'], true)) { + $ranked[$row['award']][] = $row; } else { $awarded[] = $row; } @@ -416,13 +464,16 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons $collator = new Collator('en_US'); $collator->sort($honorable); - usort($ranked, function (array $a, array $b) use ($collator): int { - if ($a['rank'] !== $b['rank']) { - return $a['rank'] <=> $b['rank']; - } + foreach ($ranked as &$rankedTeams) { + usort($rankedTeams, function (array $a, array $b) use ($collator): int { + if ($a['rank'] !== $b['rank']) { + return $a['rank'] <=> $b['rank']; + } - return $collator->compare($a['team'], $b['team']); - }); + return $collator->compare($a['team'], $b['team']); + }); + } + unset($rankedTeams); $problems = $scoreboard->getProblems(); $matrix = $scoreboard->getMatrix(); @@ -434,6 +485,7 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons 'problem_name' => $problem->getProblem()->getName(), 'team' => null, 'time' => null, + 'rank' => null, ]; foreach ($teams as $team) { if (!isset($categories[$team->getCategory()->getCategoryid()]) || $team->getCategory()->getSortorder() !== $sortOrder) { @@ -446,6 +498,7 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons 'problem' => $problem->getShortname(), 'problem_name' => $problem->getProblem()->getName(), 'team' => $teamNames[$team->getIcpcId()], + 'rank' => $rankPerTeam[$team->getIcpcId()] ?: '-', 'time' => Utils::scoretime($matrixItem->time, $scoreIsInSeconds), ]; } @@ -473,16 +526,10 @@ protected function getResultsHtml(Request $request, bool $full = false): Respons 'firstToSolve' => $firstToSolve, 'domjudgeVersion' => $this->domjudgeVersion, 'title' => sprintf('Results for %s', $contest->getName()), - 'download' => $request->query->getBoolean('download'), 'sortOrder' => $sortOrder, ]; - $response = $this->render('jury/export/results.html.twig', $data); - - if ($request->query->getBoolean('download')) { - $response->headers->set('Content-disposition', 'attachment; filename=results.html'); - } - return $response; + return $this->twig->render('jury/export/results.html.twig', $data); } protected function getClarificationsHtml(): Response diff --git a/webapp/src/Controller/Jury/InternalErrorController.php b/webapp/src/Controller/Jury/InternalErrorController.php index 6db1b2aa59..7f78c8ecaa 100644 --- a/webapp/src/Controller/Jury/InternalErrorController.php +++ b/webapp/src/Controller/Jury/InternalErrorController.php @@ -9,10 +9,12 @@ use App\Entity\JudgeTask; use App\Entity\Problem; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\RejudgingService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -26,11 +28,15 @@ class InternalErrorController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly RejudgingService $rejudgingService, protected readonly RequestStack $requestStack, - ) {} + protected readonly EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_internal_errors')] public function indexAction(): Response @@ -124,10 +130,6 @@ public function viewAction(int $errorId): Response 'internalError' => $internalError, 'affectedLink' => $affectedLink, 'affectedText' => $affectedText, - 'refresh' => [ - 'after' => 15, - 'url' => $this->generateUrl('jury_internal_error', ['errorId' => $internalError->getErrorid()]), - ] ]); } diff --git a/webapp/src/Controller/Jury/JudgehostController.php b/webapp/src/Controller/Jury/JudgehostController.php index 1e0e2dcd7d..eb23b44ee3 100644 --- a/webapp/src/Controller/Jury/JudgehostController.php +++ b/webapp/src/Controller/Jury/JudgehostController.php @@ -33,12 +33,14 @@ class JudgehostController extends BaseController // Note: when adding or modifying routes, make sure they do not clash with the /judgehosts/{hostname} route. public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EventLogService $eventLog, - protected readonly KernelInterface $kernel - ) {} + EventLogService $eventLog, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLog, $dj, $kernel); + } #[Route(path: '', name: 'jury_judgehosts')] public function indexAction(Request $request): Response @@ -269,8 +271,7 @@ public function deleteAction(Request $request, int $judgehostid): Response ->getQuery() ->getOneOrNullResult(); - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLog, $this->kernel, - [$judgehost], $this->generateUrl('jury_judgehosts')); + return $this->deleteEntities($request, [$judgehost], $this->generateUrl('jury_judgehosts')); } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/Jury/JuryMiscController.php b/webapp/src/Controller/Jury/JuryMiscController.php index d24863f573..77ab13c849 100644 --- a/webapp/src/Controller/Jury/JuryMiscController.php +++ b/webapp/src/Controller/Jury/JuryMiscController.php @@ -13,12 +13,14 @@ use App\Entity\TeamAffiliation; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\ScoreboardService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -34,10 +36,14 @@ class JuryMiscController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, + protected readonly EventLogService $eventLogService, protected readonly RequestStack $requestStack, - ) {} + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[IsGranted(new Expression("is_granted('ROLE_JURY') or is_granted('ROLE_BALLOON') or is_granted('ROLE_CLARIFICATION_RW')"))] #[Route(path: '', name: 'jury_index')] diff --git a/webapp/src/Controller/Jury/LanguageController.php b/webapp/src/Controller/Jury/LanguageController.php index 160b170c94..734bfc1f13 100644 --- a/webapp/src/Controller/Jury/LanguageController.php +++ b/webapp/src/Controller/Jury/LanguageController.php @@ -32,12 +32,14 @@ class LanguageController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, - protected readonly EventLogService $eventLogService - ) {} + KernelInterface $kernel, + protected readonly EventLogService $eventLogService, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_languages')] public function indexAction(): Response @@ -51,6 +53,7 @@ public function indexAction(): Response ->getQuery()->getResult(); $table_fields = [ 'langid' => ['title' => 'ID', 'sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true, 'default_sort' => true], 'entrypoint' => ['title' => 'entry point', 'sort' => true], 'allowjudge' => ['title' => 'allow judge', 'sort' => true], @@ -59,13 +62,6 @@ public function indexAction(): Response 'executable' => ['title' => 'executable', 'sort' => true], ]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(Language::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $enabled_languages = []; $disabled_languages = []; @@ -161,15 +157,22 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - // Normalize extensions - if ($language->getExtensions()) { - $language->setExtensions(array_values($language->getExtensions())); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $language, + fn() => $this->generateUrl('jury_language', ['langId' => $language->getLangid()]), + function () use ($language) { + // Normalize extensions + if ($language->getExtensions()) { + $language->setExtensions(array_values($language->getExtensions())); + } + $this->em->persist($language); + $this->saveEntity($language, + $language->getLangid(), true); + + return null; } - $this->em->persist($language); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $language, - $language->getLangid(), true); - return $this->redirectToRoute('jury_language', ['langId' => $language->getLangid()]); + )) { + return $response; } return $this->render('jury/language_add.html.twig', [ @@ -200,8 +203,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, - 'showExternalResult' => $this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'refresh' => [ 'after' => 15, 'url' => $this->generateUrl('jury_language', ['langId' => $language->getLangid()]), @@ -297,8 +299,7 @@ public function editAction(Request $request, string $langId): Response if ($language->getExtensions()) { $language->setExtensions(array_values($language->getExtensions())); } - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $language, - $language->getLangid(), false); + $this->saveEntity($language, $language->getLangid(), false); if ($language->getAllowJudge()) { $this->dj->unblockJudgeTasksForLanguage($langId); } @@ -320,8 +321,7 @@ public function deleteAction(Request $request, string $langId): Response throw new NotFoundHttpException(sprintf('Language with ID %s not found', $langId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$language], $this->generateUrl('jury_languages') + return $this->deleteEntities($request, [$language], $this->generateUrl('jury_languages') ); } diff --git a/webapp/src/Controller/Jury/PrintController.php b/webapp/src/Controller/Jury/PrintController.php index 430648dc87..26bb541657 100644 --- a/webapp/src/Controller/Jury/PrintController.php +++ b/webapp/src/Controller/Jury/PrintController.php @@ -7,12 +7,14 @@ use App\Form\Type\PrintType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -21,10 +23,14 @@ class PrintController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, - protected readonly ConfigurationService $config - ) {} + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, + protected readonly ConfigurationService $config, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_print')] public function showAction(Request $request): Response diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 56ed94d5f8..6f810122b2 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -52,14 +52,16 @@ class ProblemController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, protected readonly SubmissionService $submissionService, - protected readonly ImportProblemService $importProblemService - ) {} + protected readonly ImportProblemService $importProblemService, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_problems')] public function indexAction(): Response @@ -79,6 +81,7 @@ public function indexAction(): Response } $table_fields = [ 'probid' => ['title' => 'ID', 'sort' => true, 'default_sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], 'badges' => ['title' => $badgeTitle, 'sort' => false], 'num_contests' => ['title' => '# contests', 'sort' => true], @@ -88,13 +91,6 @@ public function indexAction(): Response 'num_testcases' => ['title' => '# test cases', 'sort' => true], ]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(Problem::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $contestCountData = $this->em->createQueryBuilder() ->from(ContestProblem::class, 'cp') ->select('COUNT(cp.shortname) AS count', 'p.probid') @@ -498,8 +494,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic 'defaultRunExecutable' => (string)$this->config->get('default_run'), 'defaultCompareExecutable' => (string)$this->config->get('default_compare'), 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, - 'showExternalResult' => $this->config->get('data_source') === - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'lockedProblem' => $lockedProblem, 'refresh' => [ 'after' => 15, @@ -931,8 +926,7 @@ public function editAction(Request $request, int $probId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $problem, - $problem->getProbid(), false); + $this->saveEntity($problem, $problem->getProbid(), false); return $this->redirectToRoute('jury_problem', ['probId' => $problem->getProbid()]); } @@ -1003,8 +997,7 @@ public function deleteAction(Request $request, int $probId): Response } } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$problem], $this->generateUrl('jury_problems')); + return $this->deleteEntities($request, [$problem], $this->generateUrl('jury_problems')); } #[Route(path: '/attachments/{attachmentId<\d+>}', name: 'jury_attachment_fetch')] @@ -1039,8 +1032,7 @@ public function deleteAttachmentAction(Request $request, int $attachmentId): Res } } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$attachment], $this->generateUrl('jury_problem', ['probId' => $probId])); + return $this->deleteEntities($request, [$attachment], $this->generateUrl('jury_problem', ['probId' => $probId])); } #[IsGranted('ROLE_ADMIN')] @@ -1086,10 +1078,11 @@ 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()]); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $problem, + fn() => $this->generateUrl('jury_problem', ['probId' => $problem->getProbid()]) + )) { + return $response; } return $this->render('jury/problem_add.html.twig', [ diff --git a/webapp/src/Controller/Jury/QueueTaskController.php b/webapp/src/Controller/Jury/QueueTaskController.php index e775857539..39079654c2 100644 --- a/webapp/src/Controller/Jury/QueueTaskController.php +++ b/webapp/src/Controller/Jury/QueueTaskController.php @@ -5,8 +5,11 @@ use App\Controller\BaseController; use App\Entity\JudgeTask; use App\Entity\QueueTask; +use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Utils\Utils; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; @@ -30,8 +33,13 @@ class QueueTaskController extends BaseController JudgeTask::PRIORITY_HIGH => 'thermometer-full', ]; - public function __construct(private readonly EntityManagerInterface $em) - { + public function __construct( + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); } #[Route(path: '', name: 'jury_queue_tasks')] diff --git a/webapp/src/Controller/Jury/RejudgingController.php b/webapp/src/Controller/Jury/RejudgingController.php index d08b239129..f9a185bc5a 100644 --- a/webapp/src/Controller/Jury/RejudgingController.php +++ b/webapp/src/Controller/Jury/RejudgingController.php @@ -18,6 +18,7 @@ use App\Form\Type\RejudgingType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\RejudgingService; use App\Service\SubmissionService; use App\Utils\Utils; @@ -26,6 +27,7 @@ use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Request; @@ -43,13 +45,17 @@ class RejudgingController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly RejudgingService $rejudgingService, protected readonly RouterInterface $router, - protected readonly RequestStack $requestStack - ) {} + protected readonly RequestStack $requestStack, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NoResultException @@ -215,6 +221,21 @@ public function viewAction( ->getQuery() ->getOneOrNullResult(); + $disabledProblems = []; + $disabledLangs = []; + foreach ($rejudging->getJudgings() as $judging) { + $submission = $judging->getSubmission(); + $problem = $submission->getContestProblem(); + $language = $submission->getLanguage(); + + if (!$problem->getAllowJudge()) { + $disabledProblems[$submission->getProblemId()] = $submission->getProblem()->getName(); + } + if (!$language->getAllowJudge()) { + $disabledLangs[$submission->getLanguage()->getLangid()] = $submission->getLanguage()->getName(); + } + } + if (!$rejudging) { throw new NotFoundHttpException(sprintf('Rejudging with ID %s not found', $rejudgingId)); } @@ -381,14 +402,15 @@ public function viewAction( 'newverdict' => $newverdict, 'repetitions' => array_column($repetitions, 'rejudgingid'), 'showStatistics' => $showStatistics, - 'showExternalResult' => $this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'stats' => $stats, 'refresh' => [ 'after' => 15, 'url' => $request->getRequestUri(), 'ajax' => true, ], + 'disabledProbs' => $disabledProblems, + 'disabledLangs' => $disabledLangs, ]; if ($request->isXmlHttpRequest()) { $data['ajax'] = true; diff --git a/webapp/src/Controller/Jury/ShadowDifferencesController.php b/webapp/src/Controller/Jury/ShadowDifferencesController.php index ac9938ef98..bb31239040 100644 --- a/webapp/src/Controller/Jury/ShadowDifferencesController.php +++ b/webapp/src/Controller/Jury/ShadowDifferencesController.php @@ -5,6 +5,7 @@ use App\DataTransferObject\SubmissionRestriction; use App\Entity\ExternalContestSource; use App\Service\ConfigurationService; +use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; @@ -16,6 +17,7 @@ use App\Service\DOMJudgeService; use App\Service\SubmissionService; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -27,12 +29,16 @@ class ShadowDifferencesController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly SubmissionService $submissions, protected readonly RequestStack $requestStack, - protected readonly EntityManagerInterface $em - ) {} + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NoResultException @@ -50,13 +56,8 @@ public function indexAction( #[MapQueryParameter] string $local = 'all', ): Response { - $shadowMode = DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL; - $dataSource = $this->config->get('data_source'); - if ($dataSource != $shadowMode) { - $this->addFlash('danger', sprintf( - 'Shadow differences only supported when data_source is %d', - $shadowMode - )); + if (!$this->dj->shadowMode()) { + $this->addFlash('danger', 'Shadow differences only supported when shadow_mode is true'); return $this->redirectToRoute('jury_index'); } diff --git a/webapp/src/Controller/Jury/SubmissionController.php b/webapp/src/Controller/Jury/SubmissionController.php index 4b56ca30ec..b8ea182f17 100644 --- a/webapp/src/Controller/Jury/SubmissionController.php +++ b/webapp/src/Controller/Jury/SubmissionController.php @@ -49,6 +49,7 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -60,12 +61,16 @@ class SubmissionController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly SubmissionService $submissionService, - protected readonly RouterInterface $router - ) {} + protected readonly RouterInterface $router, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_submissions')] public function indexAction( @@ -118,6 +123,16 @@ public function indexAction( /** @var Submission[] $submissions */ [$submissions, $submissionCounts] = $this->submissionService->getSubmissionList($contests, $restrictions, $limit); + $disabledProblems = []; + $disabledLangs = []; + foreach ($submissions as $submission) { + if (!$submission->getContestProblem()->getAllowJudge()) { + $disabledProblems[$submission->getProblemId()] = $submission->getProblem()->getName(); + } + if (!$submission->getLanguage()->getAllowJudge()) { + $disabledLangs[$submission->getLanguage()->getLangid()] = $submission->getLanguage()->getName(); + } + } // Load preselected filters $filters = $this->dj->jsonDecode((string)$this->dj->getCookie('domjudge_submissionsfilter') ?: '[]'); @@ -135,9 +150,10 @@ public function indexAction( 'showContest' => count($contests) > 1, 'hasFilters' => !empty($filters), 'results' => $results, - 'showExternalResult' => $this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'showTestcases' => count($submissions) <= $latestCount, + 'disabledProbs' => $disabledProblems, + 'disabledLangs' => $disabledLangs, ]; // For ajax requests, only return the submission list partial. @@ -390,9 +406,10 @@ public function viewAction( $runResult['hostname'] = $firstJudgingRun->getJudgeTask()->getJudgehost()->getHostname(); $runResult['judgehostid'] = $firstJudgingRun->getJudgeTask()->getJudgehost()->getJudgehostid(); } - $runResult['is_output_run_truncated'] = $outputDisplayLimit >= 0 && preg_match( + $runResult['is_output_run_truncated_in_db'] = preg_match( '/\[output storage truncated after \d* B\]/', - (string)$runResult['output_run_last_bytes'] + $outputDisplayLimit >= 0 ? + (string)$runResult['output_run_last_bytes'] : (string)$runResult['output_run'] ); if ($firstJudgingRun) { $runResult['testcasedir'] = $firstJudgingRun->getTestcaseDir(); @@ -453,7 +470,7 @@ public function viewAction( } $unjudgableReasons = []; - if ($runsOutstanding) { + if ($runsOutstanding || $submission->getResult() == null) { // Determine if this submission is unjudgable. $numActiveJudgehosts = (int)$this->em->createQueryBuilder() @@ -667,6 +684,29 @@ public function viewForExternalJudgementAction(ExternalJudgement $externalJudgem ]); } + #[Route(path: '/by-contest-and-external-id/{externalContestId}/{externalId}', name: 'jury_submission_by_context_external_id')] + public function viewForContestExternalIdAction(string $externalContestId, string $externalId): Response + { + $contest = $this->em->getRepository(Contest::class)->findOneBy(['externalid' => $externalContestId]); + if ($contest === null) { + throw new NotFoundHttpException(sprintf('No contest found with external ID %s', $externalContestId)); + } + + $submission = $this->em->getRepository(Submission::class) + ->findOneBy([ + 'contest' => $contest, + 'externalid' => $externalId + ]); + + if (!$submission) { + throw new NotFoundHttpException(sprintf('No submission found with external ID %s', $externalId)); + } + + return $this->redirectToRoute('jury_submission', [ + 'submitId' => $submission->getSubmitid(), + ]); + } + #[Route(path: '/by-external-id/{externalId}', name: 'jury_submission_by_external_id')] public function viewForExternalIdAction(string $externalId): RedirectResponse { @@ -696,7 +736,7 @@ public function teamOutputAction(Submission $submission, Contest $contest, Judgi throw new BadRequestHttpException('Integrity problem while fetching team output.'); } if ($run->getOutput() === null) { - throw new BadRequestHttpException('No team output available (yet).'); + throw new NotFoundHttpException('No team output available (yet).'); } $filename = sprintf('p%d.t%d.%s.run%d.team%d.out', $submission->getProblem()->getProbid(), $run->getTestcase()->getRank(), diff --git a/webapp/src/Controller/Jury/TeamAffiliationController.php b/webapp/src/Controller/Jury/TeamAffiliationController.php index e374e89caf..23d68439c3 100644 --- a/webapp/src/Controller/Jury/TeamAffiliationController.php +++ b/webapp/src/Controller/Jury/TeamAffiliationController.php @@ -25,13 +25,15 @@ class TeamAffiliationController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, - protected readonly AssetUpdateService $assetUpdater - ) {} + protected readonly AssetUpdateService $assetUpdater, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_team_affiliations')] public function indexAction( @@ -51,6 +53,7 @@ public function indexAction( $table_fields = [ 'affilid' => ['title' => 'ID', 'sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'icpcid' => ['title' => 'ICPC ID', 'sort' => true], 'shortname' => ['title' => 'shortname', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true, 'default_sort' => true], @@ -63,13 +66,6 @@ public function indexAction( $table_fields['num_teams'] = ['title' => '# teams', 'sort' => true]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(TeamAffiliation::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $team_affiliations_table = []; foreach ($teamAffiliations as $teamAffiliationData) { @@ -182,8 +178,7 @@ public function editAction(Request $request, int $affilId): Response if ($form->isSubmitted() && $form->isValid()) { $this->assetUpdater->updateAssets($teamAffiliation); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $teamAffiliation, - $teamAffiliation->getAffilid(), false); + $this->saveEntity($teamAffiliation, $teamAffiliation->getAffilid(), false); return $this->redirectToRoute('jury_team_affiliation', ['affilId' => $teamAffiliation->getAffilid()]); } @@ -202,8 +197,7 @@ public function deleteAction(Request $request, int $affilId): Response throw new NotFoundHttpException(sprintf('Team affiliation with ID %s not found', $affilId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$teamAffiliation], $this->generateUrl('jury_team_affiliations')); + return $this->deleteEntities($request, [$teamAffiliation], $this->generateUrl('jury_team_affiliations')); } #[IsGranted('ROLE_ADMIN')] @@ -216,11 +210,17 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $this->em->persist($teamAffiliation); - $this->assetUpdater->updateAssets($teamAffiliation); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $teamAffiliation, null, true); - return $this->redirectToRoute('jury_team_affiliation', ['affilId' => $teamAffiliation->getAffilid()]); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $teamAffiliation, + fn() => $this->generateUrl('jury_team_affiliation', ['affilId' => $teamAffiliation->getAffilid()]), + function () use ($teamAffiliation) { + $this->em->persist($teamAffiliation); + $this->assetUpdater->updateAssets($teamAffiliation); + $this->saveEntity($teamAffiliation, null, true); + return null; + } + )) { + return $response; } return $this->render('jury/team_affiliation_add.html.twig', [ diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index 2cdba5b922..18f6255f0b 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -32,12 +32,14 @@ class TeamCategoryController extends BaseController use JudgeRemainingTrait; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, - protected readonly EventLogService $eventLogService - ) {} + KernelInterface $kernel, + protected readonly EventLogService $eventLogService, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_team_categories')] public function indexAction(): Response @@ -53,6 +55,7 @@ public function indexAction(): Response ->getQuery()->getResult(); $table_fields = [ 'categoryid' => ['title' => 'ID', 'sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'icpcid' => ['title' => 'ICPC ID', 'sort' => true], 'sortorder' => ['title' => 'sort', 'sort' => true, 'default_sort' => true], 'name' => ['title' => 'name', 'sort' => true], @@ -61,13 +64,6 @@ public function indexAction(): Response 'allow_self_registration' => ['title' => 'self-registration', 'sort' => true], ]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(TeamCategory::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $team_categories_table = []; foreach ($teamCategories as $teamCategoryData) { @@ -140,8 +136,7 @@ public function viewAction(Request $request, SubmissionService $submissionServic 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, - 'showExternalResult' => $this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'refresh' => [ 'after' => 15, 'url' => $this->generateUrl('jury_team_category', ['categoryId' => $teamCategory->getCategoryid()]), @@ -172,13 +167,12 @@ public function editAction(Request $request, int $categoryId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $teamCategory, - $teamCategory->getCategoryid(), false); + $this->saveEntity($teamCategory, $teamCategory->getCategoryid(), false); // Also emit an update event for all teams of the category, since the hidden property might have changed $teams = $teamCategory->getTeams(); if (!$teams->isEmpty()) { $teamIds = array_map(fn(Team $team) => $team->getTeamid(), $teams->toArray()); - foreach ($this->contestsForEntity($teamCategory, $this->dj) as $contest) { + foreach ($this->contestsForEntity($teamCategory) as $contest) { $this->eventLogService->log( 'teams', $teamIds, @@ -209,8 +203,7 @@ public function deleteAction(Request $request, int $categoryId): Response throw new NotFoundHttpException(sprintf('Team category with ID %s not found', $categoryId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$teamCategory], $this->generateUrl('jury_team_categories')); + return $this->deleteEntities($request, [$teamCategory], $this->generateUrl('jury_team_categories')); } #[IsGranted('ROLE_ADMIN')] @@ -223,10 +216,11 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $this->em->persist($teamCategory); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $teamCategory, null, true); - return $this->redirectToRoute('jury_team_category', ['categoryId' => $teamCategory->getCategoryid()]); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $teamCategory, + fn() => $this->generateUrl('jury_team_category', ['categoryId' => $teamCategory->getCategoryid()]) + )) { + return $response; } return $this->render('jury/team_category_add.html.twig', [ @@ -250,7 +244,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/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index ccd2b7e55c..08741779e2 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -32,13 +32,15 @@ class TeamController extends BaseController { public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, - protected readonly AssetUpdateService $assetUpdater - ) {} + protected readonly AssetUpdateService $assetUpdater, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_teams')] public function indexAction(): Response @@ -88,6 +90,7 @@ public function indexAction(): Response $table_fields = [ 'teamid' => ['title' => 'ID', 'sort' => true, 'default_sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'label' => ['title' => 'label', 'sort' => true,], 'effective_name' => ['title' => 'name', 'sort' => true,], 'category' => ['title' => 'category', 'sort' => true,], @@ -99,13 +102,6 @@ public function indexAction(): Response 'stats' => ['title' => 'stats', 'sort' => true,], ]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(Team::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $userDataPerTeam = $this->em->createQueryBuilder() ->from(Team::class, 't', 't.teamid') ->leftJoin('t.users', 'u') @@ -295,8 +291,7 @@ public function viewAction( $data['restrictionText'] = $restrictionText; $data['submissions'] = $submissions; $data['submissionCounts'] = $submissionCounts; - $data['showExternalResult'] = $this->config->get('data_source') === - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL; + $data['showExternalResult'] = $this->dj->shadowMode(); $data['showContest'] = count($this->dj->getCurrentContests(honorCookie: true)) > 1; if ($request->isXmlHttpRequest()) { @@ -322,9 +317,9 @@ public function editAction(Request $request, int $teamId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + $this->possiblyAddUser($team); $this->assetUpdater->updateAssets($team); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $team, - $team->getTeamid(), false); + $this->saveEntity($team, $team->getTeamid(), false); return $this->redirectToRoute('jury_team', ['teamId' => $team->getTeamid()]); } @@ -343,8 +338,7 @@ public function deleteAction(Request $request, int $teamId): Response throw new NotFoundHttpException(sprintf('Team with ID %s not found', $teamId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$team], $this->generateUrl('jury_teams')); + return $this->deleteEntities($request, [$team], $this->generateUrl('jury_teams')); } #[IsGranted('ROLE_ADMIN')] @@ -357,26 +351,18 @@ public function addAction(Request $request): Response $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - /** @var User $user */ - $user = $team->getUsers()->first(); - if ($team->getAddUserForTeam() === Team::CREATE_NEW_USER) { - // Create a user for the team. - $user = new User(); - $user->setUsername($team->getNewUsername()); - $team->addUser($user); - // Make sure the user has the team role to make validation work. - $role = $this->em->getRepository(Role::class)->findOneBy(['dj_role' => 'team']); - $user->addUserRole($role); - // Set the user's name to the team name when creating a new user. - $user->setName($team->getEffectiveName()); - } elseif ($team->getAddUserForTeam() === Team::ADD_EXISTING_USER) { - $team->addUser($team->getExistingUser()); + if ($response = $this->processAddFormForExternalIdEntity( + $form, $team, + fn() => $this->generateUrl('jury_team', ['teamId' => $team->getTeamid()]), + function () use ($team) { + $this->possiblyAddUser($team); + $this->em->persist($team); + $this->assetUpdater->updateAssets($team); + $this->saveEntity($team, null, true); + return null; } - $this->em->persist($team); - $this->assetUpdater->updateAssets($team); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $team, null, true); - return $this->redirectToRoute('jury_team', ['teamId' => $team->getTeamid()]); + )) { + return $response; } return $this->render('jury/team_add.html.twig', [ @@ -384,4 +370,25 @@ public function addAction(Request $request): Response 'form' => $form, ]); } + + /** + * Add an existing or new user to a team if configured to do so + */ + protected function possiblyAddUser(Team $team): void + { + if ($team->getAddUserForTeam() === Team::CREATE_NEW_USER) { + // Create a user for the team. + $user = new User(); + $user->setUsername($team->getNewUsername()); + $user->setExternalid($team->getNewUsername()); + $team->addUser($user); + // Make sure the user has the team role to make validation work. + $role = $this->em->getRepository(Role::class)->findOneBy(['dj_role' => 'team']); + $user->addUserRole($role); + // Set the user's name to the team name when creating a new user. + $user->setName($team->getEffectiveName()); + } elseif ($team->getAddUserForTeam() === Team::ADD_EXISTING_USER) { + $team->addUser($team->getExistingUser()); + } + } } diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index 7f0278dbaf..10572a7357 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -36,13 +36,15 @@ class UserController extends BaseController public const MIN_PASSWORD_LENGTH = 10; public function __construct( - protected readonly EntityManagerInterface $em, - protected readonly DOMJudgeService $dj, + EntityManagerInterface $em, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly KernelInterface $kernel, + KernelInterface $kernel, protected readonly EventLogService $eventLogService, - protected readonly TokenStorageInterface $tokenStorage - ) {} + protected readonly TokenStorageInterface $tokenStorage, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_users')] public function indexAction(): Response @@ -58,6 +60,7 @@ public function indexAction(): Response $table_fields = [ 'username' => ['title' => 'username', 'sort' => true, 'default_sort' => true], + 'externalid' => ['title' => 'external ID', 'sort' => true], 'name' => ['title' => 'name', 'sort' => true], 'email' => ['title' => 'email', 'sort' => true], 'user_roles' => ['title' => 'roles', 'sort' => true], @@ -70,13 +73,6 @@ public function indexAction(): Response $table_fields['last_ip_address'] = ['title' => 'last IP', 'sort' => true]; $table_fields['status'] = ['title' => '', 'sort' => true]; - // Insert external ID field when configured to use it. - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity(User::class)) { - $table_fields = array_slice($table_fields, 0, 1, true) + - [$externalIdField => ['title' => 'external ID', 'sort' => true]] + - array_slice($table_fields, 1, null, true); - } - $propertyAccessor = PropertyAccess::createPropertyAccessor(); $users_table = []; $timeFormat = (string)$this->config->get('time_format'); @@ -190,8 +186,7 @@ public function viewAction(int $userId, SubmissionService $submissionService): R 'submissions' => $submissions, 'submissionCounts' => $submissionCounts, 'showContest' => count($this->dj->getCurrentContests(honorCookie: true)) > 1, - 'showExternalResult' => $this->config->get('data_source') === - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + 'showExternalResult' => $this->dj->shadowMode(), 'refresh' => [ 'after' => 3, 'url' => $this->generateUrl('jury_user', ['userId' => $user->getUserid()]), @@ -231,9 +226,7 @@ public function editAction(Request $request, int $userId): Response if ($errorResult = $this->checkPasswordLength($user, $form)) { return $errorResult; } - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $user, - $user->getUserid(), - false); + $this->saveEntity($user, $user->getUserid(), false); // If we save the currently logged in used, update the login token. if ($user->getUserid() === $this->dj->getUser()->getUserid()) { @@ -265,8 +258,7 @@ public function deleteAction(Request $request, int $userId): Response throw new NotFoundHttpException(sprintf('User with ID %s not found', $userId)); } - return $this->deleteEntities($request, $this->em, $this->dj, $this->eventLogService, $this->kernel, - [$user], $this->generateUrl('jury_users')); + return $this->deleteEntities($request, [$user], $this->generateUrl('jury_users')); } #[IsGranted('ROLE_ADMIN')] @@ -285,13 +277,19 @@ public function addAction( $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - if ($errorResult = $this->checkPasswordLength($user, $form)) { - return $errorResult; + if ($response = $this->processAddFormForExternalIdEntity( + $form, $user, + fn() => $this->generateUrl('jury_user', ['userId' => $user->getUserid()]), + function () use ($user, $form) { + if ($errorResult = $this->checkPasswordLength($user, $form)) { + return $errorResult; + } + $this->em->persist($user); + $this->saveEntity($user, null, true); + return null; } - $this->em->persist($user); - $this->saveEntity($this->em, $this->eventLogService, $this->dj, $user, null, true); - return $this->redirectToRoute('jury_user', ['userId' => $user->getUserid()]); + )) { + return $response; } return $this->render('jury/user_add.html.twig', [ diff --git a/webapp/src/Controller/Jury/VersionController.php b/webapp/src/Controller/Jury/VersionController.php index 311e918219..f8c0ab2f8b 100644 --- a/webapp/src/Controller/Jury/VersionController.php +++ b/webapp/src/Controller/Jury/VersionController.php @@ -5,9 +5,12 @@ use App\Controller\BaseController; use App\Entity\Language; use App\Entity\Version; +use App\Service\DOMJudgeService; +use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -15,7 +18,14 @@ #[Route(path: '/jury/versions')] class VersionController extends BaseController { - public function __construct(private readonly EntityManagerInterface $em) {} + public function __construct( + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'jury_versions')] public function indexAction(): Response diff --git a/webapp/src/Controller/PublicController.php b/webapp/src/Controller/PublicController.php index 0eb7b07fea..f09e2791d0 100644 --- a/webapp/src/Controller/PublicController.php +++ b/webapp/src/Controller/PublicController.php @@ -8,6 +8,7 @@ use App\Entity\TeamCategory; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\ScoreboardService; use App\Service\StatisticsService; use Doctrine\ORM\EntityManagerInterface; @@ -20,6 +21,7 @@ use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\RouterInterface; @@ -27,14 +29,19 @@ class PublicController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly ScoreboardService $scoreboardService, protected readonly StatisticsService $stats, - protected readonly EntityManagerInterface $em - ) {} + EntityManagerInterface $em, + EventLogService $eventLog, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLog, $dj, $kernel); + } #[Route(path: '', name: 'public_index')] + #[Route(path: '/scoreboard')] public function scoreboardAction( Request $request, #[MapQueryParameter(name: 'contest')] diff --git a/webapp/src/Controller/RootController.php b/webapp/src/Controller/RootController.php index eb330a17f1..16085edb6e 100644 --- a/webapp/src/Controller/RootController.php +++ b/webapp/src/Controller/RootController.php @@ -3,12 +3,15 @@ namespace App\Controller; use App\Service\DOMJudgeService; +use App\Service\EventLogService; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Twig\Extra\Markdown\MarkdownRuntime; @@ -16,10 +19,6 @@ #[Route(path: '')] class RootController extends BaseController { - public function __construct(protected readonly DOMJudgeService $dj) - { - } - #[Route(path: '', name: 'root')] public function redirectAction(AuthorizationCheckerInterface $authorizationChecker): RedirectResponse { diff --git a/webapp/src/Controller/SecurityController.php b/webapp/src/Controller/SecurityController.php index e0033e020e..4b74f801a4 100644 --- a/webapp/src/Controller/SecurityController.php +++ b/webapp/src/Controller/SecurityController.php @@ -105,7 +105,7 @@ public function registerAction( $plainPass = $registration_form->get('plainPassword')->getData(); $password = $passwordHasher->hashPassword($user, $plainPass); $user->setPassword($password); - if ($user->getName() === null) { + if ((string)$user->getName() === '') { $user->setName($user->getUsername()); } diff --git a/webapp/src/Controller/Team/ClarificationController.php b/webapp/src/Controller/Team/ClarificationController.php index ab376d91b2..35ef682ae9 100644 --- a/webapp/src/Controller/Team/ClarificationController.php +++ b/webapp/src/Controller/Team/ClarificationController.php @@ -16,6 +16,7 @@ use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; @@ -34,12 +35,15 @@ class ClarificationController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EntityManagerInterface $em, + EntityManagerInterface $em, protected readonly EventLogService $eventLogService, - protected readonly FormFactoryInterface $formFactory - ) {} + protected readonly FormFactoryInterface $formFactory, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NonUniqueResultException diff --git a/webapp/src/Controller/Team/LanguageController.php b/webapp/src/Controller/Team/LanguageController.php index 2ae1df664c..1eb7b2e7b8 100644 --- a/webapp/src/Controller/Team/LanguageController.php +++ b/webapp/src/Controller/Team/LanguageController.php @@ -5,10 +5,13 @@ use App\Controller\BaseController; use App\Entity\Language; use App\Service\ConfigurationService; +use App\Service\DOMJudgeService; +use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -21,9 +24,14 @@ class LanguageController extends BaseController { public function __construct( - protected readonly ConfigurationService $config, - protected readonly EntityManagerInterface $em - ) {} + protected readonly ConfigurationService $config, + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '', name: 'team_languages')] public function languagesAction(): Response diff --git a/webapp/src/Controller/Team/MiscController.php b/webapp/src/Controller/Team/MiscController.php index e714d6737b..4c79f9df16 100644 --- a/webapp/src/Controller/Team/MiscController.php +++ b/webapp/src/Controller/Team/MiscController.php @@ -21,6 +21,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -41,13 +42,16 @@ class MiscController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EntityManagerInterface $em, + EntityManagerInterface $em, protected readonly ScoreboardService $scoreboardService, protected readonly SubmissionService $submissionService, - protected readonly EventLogService $eventLogService - ) {} + protected readonly EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NoResultException @@ -180,13 +184,10 @@ public function printAction(Request $request): Response $propertyAccessor = PropertyAccess::createPropertyAccessor(); $team = $this->dj->getUser()->getTeam(); - $externalIdField = $this->eventLogService->externalIdFieldForEntity($team); if ($team->getLabel()) { $teamId = $team->getLabel(); - } elseif ($externalIdField && ($externalId = $propertyAccessor->getValue($team, $externalIdField))) { - $teamId = $externalId; } else { - $teamId = (string)$team->getTeamid(); + $teamId = $team->getExternalid(); } $ret = $this->dj->printFile($realfile, $originalfilename, $langid, $username, $team->getEffectiveName(), $teamId, $team->getLocation()); diff --git a/webapp/src/Controller/Team/ProblemController.php b/webapp/src/Controller/Team/ProblemController.php index 2e99d6bfc7..c8f3bd6b57 100644 --- a/webapp/src/Controller/Team/ProblemController.php +++ b/webapp/src/Controller/Team/ProblemController.php @@ -7,10 +7,12 @@ use App\Entity\ContestProblem; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\StatisticsService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -27,11 +29,15 @@ class ProblemController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly StatisticsService $stats, - protected readonly EntityManagerInterface $em - ) {} + protected readonly EventLogService $eventLogService, + EntityManagerInterface $em, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } /** * @throws NonUniqueResultException diff --git a/webapp/src/Controller/Team/ScoreboardController.php b/webapp/src/Controller/Team/ScoreboardController.php index e1feefbe69..add967a227 100644 --- a/webapp/src/Controller/Team/ScoreboardController.php +++ b/webapp/src/Controller/Team/ScoreboardController.php @@ -6,10 +6,12 @@ use App\Entity\Team; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\ScoreboardService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -24,11 +26,15 @@ class ScoreboardController extends BaseController { public function __construct( - protected readonly DOMJudgeService $dj, + DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly ScoreboardService $scoreboardService, - protected readonly EntityManagerInterface $em - ) {} + EntityManagerInterface $em, + protected readonly EventLogService $eventLogService, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '/scoreboard', name: 'team_scoreboard')] public function scoreboardAction(Request $request): Response diff --git a/webapp/src/Controller/Team/SubmissionController.php b/webapp/src/Controller/Team/SubmissionController.php index e6cfbf223f..35e1a382e7 100644 --- a/webapp/src/Controller/Team/SubmissionController.php +++ b/webapp/src/Controller/Team/SubmissionController.php @@ -11,11 +11,13 @@ use App\Form\Type\SubmitProblemType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; +use App\Service\EventLogService; use App\Service\SubmissionService; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\Query\Expr\Join; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -37,12 +39,16 @@ class SubmissionController extends BaseController final public const ALWAYS_SHOW_COMPILE_OUTPUT = 2; public function __construct( - protected readonly EntityManagerInterface $em, + EntityManagerInterface $em, protected readonly SubmissionService $submissionService, - protected readonly DOMJudgeService $dj, + protected readonly EventLogService $eventLogService, + DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly FormFactoryInterface $formFactory - ) {} + protected readonly FormFactoryInterface $formFactory, + KernelInterface $kernel, + ) { + parent::__construct($em, $eventLogService, $dj, $kernel); + } #[Route(path: '/submit/{problem}', name: 'team_submit')] public function createAction(Request $request, ?Problem $problem = null): Response diff --git a/webapp/src/DataFixtures/DefaultData/LanguageFixture.php b/webapp/src/DataFixtures/DefaultData/LanguageFixture.php index 7ec96a0b68..6c82704e40 100644 --- a/webapp/src/DataFixtures/DefaultData/LanguageFixture.php +++ b/webapp/src/DataFixtures/DefaultData/LanguageFixture.php @@ -25,29 +25,29 @@ public function load(ObjectManager $manager): void $data = [ // ID external ID name extensions require entry point allow allow time compile compiler version runner version // entry point description submit judge factor script command command - ['adb', 'ada', 'Ada', ['adb', 'ads'], false, null, false, true, 1, 'adb', '', ''], - ['awk', 'awk', 'AWK', ['awk'], false, null, false, true, 1, 'awk', '', ''], - ['bash', 'bash', 'Bash shell', ['bash'], false, 'Main file', false, true, 1, 'bash', '', ''], + ['adb', 'ada', 'Ada', ['adb', 'ads'], false, null, false, true, 1, 'adb', 'gnatmake --version', ''], + ['awk', 'awk', 'AWK', ['awk'], false, null, false, true, 1, 'awk', 'awk --version', 'awk --version'], + ['bash', 'bash', 'Bash shell', ['bash'], false, 'Main file', false, true, 1, 'bash', 'bash --version', 'bash --version'], ['c', 'c', 'C', ['c'], false, null, true, true, 1, 'c', 'gcc --version', ''], ['cpp', 'cpp', 'C++', ['cpp', 'cc', 'cxx', 'c++'], false, null, true, true, 1, 'cpp', 'g++ --version', ''], - ['csharp', 'csharp', 'C#', ['csharp', 'cs'], false, null, false, true, 1, 'csharp', '', ''], - ['f95', 'f95', 'Fortran', ['f95', 'f90'], false, null, false, true, 1, 'f95', '', ''], - ['hs', 'haskell', 'Haskell', ['hs', 'lhs'], false, null, false, true, 1, 'hs', '', ''], + ['csharp', 'csharp', 'C#', ['csharp', 'cs'], false, null, false, true, 1, 'csharp', 'mcs --version', 'mono --version'], + ['f95', 'f95', 'Fortran', ['f95', 'f90'], false, null, false, true, 1, 'f95', 'gfortran --version', ''], + ['hs', 'haskell', 'Haskell', ['hs', 'lhs'], false, null, false, true, 1, 'hs', 'ghc --version', ''], ['java', 'java', 'Java', ['java'], false, 'Main class', true, true, 1, 'java_javac_detect', 'javac -version', 'java -version'], - ['js', 'javascript', 'JavaScript', ['js'], false, 'Main file', false, true, 1, 'js', '', ''], - ['lua', 'lua', 'Lua', ['lua'], false, null, false, true, 1, 'lua', '', ''], + ['js', 'javascript', 'JavaScript', ['js', 'mjs'], false, 'Main file', false, true, 1, 'js', 'nodejs --version', 'nodejs --version'], + ['lua', 'lua', 'Lua', ['lua'], false, null, false, true, 1, 'lua', 'luac -v', 'lua -v'], ['kt', 'kotlin', 'Kotlin', ['kt'], true, 'Main class', false, true, 1, 'kt', 'kotlinc -version', 'kotlin -version'], - ['pas', 'pascal', 'Pascal', ['pas', 'p'], false, 'Main file', false, true, 1, 'pas', '', ''], - ['pl', 'pl', 'Perl', ['pl'], false, 'Main file', false, true, 1, 'pl', '', ''], - ['plg', 'prolog', 'Prolog', ['plg'], false, 'Main file', false, true, 1, 'plg', '', ''], + ['pas', 'pascal', 'Pascal', ['pas', 'p'], false, 'Main file', false, true, 1, 'pas', 'fpc -iW', ''], + ['pl', 'pl', 'Perl', ['pl'], false, 'Main file', false, true, 1, 'pl', 'perl -v', 'perl -v'], + ['plg', 'prolog', 'Prolog', ['plg'], false, 'Main file', false, true, 1, 'plg', 'swipl --version', ''], ['py3', 'python3', 'Python 3', ['py'], false, 'Main file', true, true, 1, 'py3', 'pypy3 --version', 'pypy3 --version'], ['ocaml', 'ocaml', 'OCaml', ['ml'], false, null, false, true, 1, 'ocaml', 'ocamlopt --version', ''], - ['r', 'r', 'R', ['R'], false, 'Main file', false, true, 1, 'r', '', ''], - ['rb', 'ruby', 'Ruby', ['rb'], false, 'Main file', false, true, 1, 'rb', '', ''], - ['rs', 'rust', 'Rust', ['rs'], false, null, false, true, 1, 'rs', '', ''], - ['scala', 'scala', 'Scala', ['scala'], false, null, false, true, 1, 'scala', '', ''], - ['sh', 'sh', 'POSIX shell', ['sh'], false, 'Main file', false, true, 1, 'sh', '', ''], - ['swift', 'swift', 'Swift', ['swift'], false, 'Main file', false, true, 1, 'swift', '', ''], + ['r', 'r', 'R', ['R'], false, 'Main file', false, true, 1, 'r', 'Rscript --version', 'Rscript --version'], + ['rb', 'ruby', 'Ruby', ['rb'], false, 'Main file', false, true, 1, 'rb', 'ruby --version', 'ruby --version'], + ['rs', 'rust', 'Rust', ['rs'], false, null, false, true, 1, 'rs', 'rustc --version', ''], + ['scala', 'scala', 'Scala', ['scala'], false, null, false, true, 1, 'scala', 'scalac -version', 'scala -version'], + ['sh', 'sh', 'POSIX shell', ['sh'], false, 'Main file', false, true, 1, 'sh', 'md5sum /bin/sh', 'md5sum /bin/sh'], + ['swift', 'swift', 'Swift', ['swift'], false, 'Main file', false, true, 1, 'swift', 'swiftc --version', ''], ]; foreach ($data as $item) { diff --git a/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsOneFixture.php b/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsOneFixture.php new file mode 100644 index 0000000000..8207c6f070 --- /dev/null +++ b/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsOneFixture.php @@ -0,0 +1,16 @@ +getRepository(Team::class)->findOneBy(['name' => 'DOMjudge']); + $team->setExternalid('1'); + $manager->flush(); + } +} diff --git a/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsTwoFixture.php b/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsTwoFixture.php new file mode 100644 index 0000000000..50ee1ffc37 --- /dev/null +++ b/webapp/src/DataFixtures/Test/TeamWithExternalIdEqualsTwoFixture.php @@ -0,0 +1,16 @@ +getRepository(Team::class)->findOneBy(['name' => 'Example teamname']); + $team->setExternalid('2'); + $manager->flush(); + } +} diff --git a/webapp/src/DataTransferObject/Award.php b/webapp/src/DataTransferObject/Award.php index eb5f2ade8d..fa3fc8faeb 100644 --- a/webapp/src/DataTransferObject/Award.php +++ b/webapp/src/DataTransferObject/Award.php @@ -11,7 +11,7 @@ class Award */ public function __construct( public readonly string $id, - public readonly string $citation, + public readonly ?string $citation, #[Serializer\Type('array')] public readonly array $teamIds, ) {} diff --git a/webapp/src/DataTransferObject/ContestState.php b/webapp/src/DataTransferObject/ContestState.php index e10d0ef6ed..9296df9b5e 100644 --- a/webapp/src/DataTransferObject/ContestState.php +++ b/webapp/src/DataTransferObject/ContestState.php @@ -5,11 +5,11 @@ class ContestState { public function __construct( - public readonly ?string $started, - public readonly ?string $ended, - public readonly ?string $frozen, - public readonly ?string $thawed, - public readonly ?string $finalized, - public readonly ?string $endOfUpdates, + public readonly ?string $started = null, + public readonly ?string $ended = null, + public readonly ?string $frozen = null, + public readonly ?string $thawed = null, + public readonly ?string $finalized = null, + public readonly ?string $endOfUpdates = null, ) {} } diff --git a/webapp/src/DataTransferObject/ResultRow.php b/webapp/src/DataTransferObject/ResultRow.php new file mode 100644 index 0000000000..6f85e40a21 --- /dev/null +++ b/webapp/src/DataTransferObject/ResultRow.php @@ -0,0 +1,40 @@ + $this->teamId, + 'rank' => $this->rank, + 'award' => $this->award, + 'num_solved' => $this->numSolved, + 'total_time' => $this->totalTime, + 'time_of_last_submission' => $this->timeOfLastSubmission, + 'group_winner' => $this->groupWinner, + ]; + } +} diff --git a/webapp/src/DataTransferObject/Scoreboard/Problem.php b/webapp/src/DataTransferObject/Scoreboard/Problem.php index 4f0d363825..b83613aa06 100644 --- a/webapp/src/DataTransferObject/Scoreboard/Problem.php +++ b/webapp/src/DataTransferObject/Scoreboard/Problem.php @@ -9,7 +9,7 @@ class Problem { public function __construct( #[Serializer\Groups([ARC::GROUP_NONSTRICT])] - public readonly string $label, + public readonly ?string $label, public readonly string $problemId, public readonly int $numJudged, public readonly int $numPending, diff --git a/webapp/src/DataTransferObject/Shadowing/ContestEvent.php b/webapp/src/DataTransferObject/Shadowing/ContestEvent.php index a4161fd6a3..6f8dbabbe2 100644 --- a/webapp/src/DataTransferObject/Shadowing/ContestEvent.php +++ b/webapp/src/DataTransferObject/Shadowing/ContestEvent.php @@ -12,7 +12,7 @@ public function __construct( public readonly int $penaltyTime, public readonly ?string $formalName, public readonly ?string $startTime, - public readonly ?int $countdownPauseTime, + public readonly ?string $countdownPauseTime, public readonly ?string $scoreboardFreezeDuration, public readonly ?string $scoreboardThawTime, ) {} diff --git a/webapp/src/DataTransferObject/Shadowing/EventType.php b/webapp/src/DataTransferObject/Shadowing/EventType.php index 24d06b720a..dfa66f332e 100644 --- a/webapp/src/DataTransferObject/Shadowing/EventType.php +++ b/webapp/src/DataTransferObject/Shadowing/EventType.php @@ -12,6 +12,7 @@ enum EventType: string case JUDGEMENTS = 'judgements'; case JUDGEMENT_TYPES = 'judgement-types'; case LANGUAGES = 'languages'; + case MAP_INFO = 'map-info'; case ORGANIZATIONS = 'organizations'; case PERSONS = 'persons'; case PROBLEMS = 'problems'; 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/Doctrine/ExternalIdAlreadyExistsException.php b/webapp/src/Doctrine/ExternalIdAlreadyExistsException.php new file mode 100644 index 0000000000..e7cb9e6d17 --- /dev/null +++ b/webapp/src/Doctrine/ExternalIdAlreadyExistsException.php @@ -0,0 +1,20 @@ +externalid + ); + parent::__construct($message); + } +} diff --git a/webapp/src/Doctrine/ExternalIdAssigner.php b/webapp/src/Doctrine/ExternalIdAssigner.php new file mode 100644 index 0000000000..c7e7ac3b6e --- /dev/null +++ b/webapp/src/Doctrine/ExternalIdAssigner.php @@ -0,0 +1,60 @@ +getObject(); + if ((!$entity instanceof ExternalIdFromInternalIdInterface) && (!$entity instanceof CalculatedExternalIdBasedOnRelatedFieldInterface)) { + return; + } + + if ($entity->getExternalId()) { + return; + } + + $entityClass = get_class($entity); + if ($entity instanceof CalculatedExternalIdBasedOnRelatedFieldInterface) { + $externalid = $entity->getCalculatedExternalId(); + } else { + $metadata = $this->em->getClassMetadata($entityClass); + $primaryKeyField = $metadata->getSingleIdentifierFieldName(); + $externalid = (string)$metadata->getFieldValue($entity, $primaryKeyField); + if ($entity instanceof PrefixedExternalIdInterface) { + $externalid = 'dj-' . $externalid; + } elseif ($this->dj->shadowMode() && $entity instanceof PrefixedExternalIdInShadowModeInterface) { + $externalid = 'dj-' . $externalid; + } + } + + // Check if there is already an entity with that external ID + $existingEntity = $this->em->getRepository($entityClass)->findOneBy(['externalid' => $externalid]); + if ($existingEntity) { + throw new ExternalIdAlreadyExistsException($entityClass, $externalid); + } + + $entity->setExternalId($externalid); + + // Note: flushing in postPersist is not safe in general, but we make sure we only do this + // once, so we can't have an infinite loop here. + $this->em->flush(); + } +} diff --git a/webapp/src/Entity/BaseApiEntity.php b/webapp/src/Entity/BaseApiEntity.php index 791edd8c6d..1ff4d4f4d3 100644 --- a/webapp/src/Entity/BaseApiEntity.php +++ b/webapp/src/Entity/BaseApiEntity.php @@ -9,21 +9,4 @@ */ abstract class BaseApiEntity { - /** - * Get the API ID field name for this entity. - */ - public function getApiIdField(EventLogService $eventLogService): string - { - return $eventLogService->apiIdFieldForEntity($this); - } - - /** - * Get the API ID for this entity. - */ - public function getApiId(EventLogService $eventLogService): string - { - $field = $eventLogService->apiIdFieldForEntity($this); - $method = 'get'.ucfirst($field); - return (string)$this->{$method}(); - } } diff --git a/webapp/src/Entity/CalculatedExternalIdBasedOnRelatedFieldInterface.php b/webapp/src/Entity/CalculatedExternalIdBasedOnRelatedFieldInterface.php new file mode 100644 index 0000000000..6ec12ac1e9 --- /dev/null +++ b/webapp/src/Entity/CalculatedExternalIdBasedOnRelatedFieldInterface.php @@ -0,0 +1,12 @@ + [null, 190]] )] #[UniqueEntity(fields: 'externalid')] -class Clarification extends BaseApiEntity implements ExternalRelationshipEntityInterface +class Clarification extends BaseApiEntity implements + HasExternalIdInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInShadowModeInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Clarification ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('clarid')] + #[Serializer\Groups([ARC::GROUP_RESTRICTED_NONSTRICT])] protected int $clarid; #[ORM\Column( @@ -48,7 +51,7 @@ class Clarification extends BaseApiEntity implements ExternalRelationshipEntityI ] )] #[OA\Property(nullable: true)] - #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( @@ -236,13 +239,25 @@ public function getProblem(): ?Problem return $this->problem; } + public function getContestProblem(): ?ContestProblem + { + if (!$this->problem) { + return null; + } + return $this->contest->getContestProblem($this->problem); + } + + public function getProblemId(): ?int + { + return $this->getProblem()?->getProbid(); + } + #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('problem_id')] - #[Serializer\Type('string')] - public function getProblemId(): ?int + public function getApiProblemId(): ?string { - return $this->getProblem()?->getProbid(); + return $this->getProblem()?->getExternalid(); } public function setContest(?Contest $contest = null): Clarification @@ -270,10 +285,9 @@ public function getInReplyTo(): ?Clarification #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('reply_to_id')] - #[Serializer\Type('string')] - public function getInReplyToId(): ?int + public function getInReplyToId(): ?string { - return $this->getInReplyTo()?->getClarid(); + return $this->getInReplyTo()?->getExternalid(); } public function addReply(Clarification $reply): Clarification @@ -304,10 +318,9 @@ public function getSender(): ?Team #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('from_team_id')] - #[Serializer\Type('string')] - public function getSenderId(): ?int + public function getSenderId(): ?string { - return $this->getSender()?->getTeamid(); + return $this->getSender()?->getExternalid(); } public function setRecipient(?Team $recipient = null): Clarification @@ -324,29 +337,9 @@ public function getRecipient(): ?Team #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('to_team_id')] - #[Serializer\Type('string')] - public function getRecipientId(): ?int + public function getRecipientId(): ?string { - return $this->getRecipient()?->getTeamid(); - } - - /** - * Get the entities to check for external ID's while serializing. - * - * This method should return an array with as keys the JSON field names and as values the actual entity - * objects that the SetExternalIdVisitor should check for applicable external ID's - * - * @return array{from_team_id: Team|null, to_team_id: Team|null, - * problem_id: Problem|null, reply_to_id: Clarification|null} - */ - public function getExternalRelationships(): array - { - return [ - 'from_team_id' => $this->getSender(), - 'to_team_id' => $this->getRecipient(), - 'problem_id' => $this->getProblem(), - 'reply_to_id' => $this->getInReplyTo() - ]; + return $this->getRecipient()?->getExternalid(); } public function getSummary(): string diff --git a/webapp/src/Entity/Contest.php b/webapp/src/Entity/Contest.php index 32edb7beed..936c005629 100644 --- a/webapp/src/Entity/Contest.php +++ b/webapp/src/Entity/Contest.php @@ -50,23 +50,25 @@ )] #[UniqueEntity(fields: 'shortname')] #[UniqueEntity(fields: 'externalid')] -class Contest extends BaseApiEntity implements AssetEntityInterface +class Contest extends BaseApiEntity implements + HasExternalIdInterface, + AssetEntityInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInterface { final public const STARTTIME_UPDATE_MIN_SECONDS_BEFORE = 30; #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Contest ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected ?int $cid = null; #[ORM\Column( nullable: true, options: ['comment' => 'Contest ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\SerializedName('external_id')] - #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column(options: ['comment' => 'Descriptive name'])] @@ -913,6 +915,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; @@ -1093,6 +1105,11 @@ public function getDataForJuryInterface(): array $resultItem = []; $method = sprintf('get%stime', ucfirst($time)); $timeValue = $this->{$method}(); + $timeValueString = ''; + if ($time !== 'finalize') { + $method = sprintf('get%stimeString', ucfirst($time)); + $timeValueString = $this->{$method}(); + } if ($time === 'start' && !$this->getStarttimeEnabled()) { $resultItem['icon'] = 'ellipsis-h'; $timeValue = $this->getStarttime(false); @@ -1109,7 +1126,10 @@ public function getDataForJuryInterface(): array } $resultItem['label'] = sprintf('%s time', ucfirst($time)); - $resultItem['time'] = Utils::printtime($timeValue, 'Y-m-d H:i:s (T)'); + $resultItem['time'] = $timeValueString; + if (empty($resultItem['time'])) { + $resultItem['time'] = Utils::printtime($timeValue, 'Y-m-d H:i:s (T)'); + } if ($time === 'start' && !$this->getStarttimeEnabled()) { $resultItem['class'] = 'ignore'; } diff --git a/webapp/src/Entity/ContestProblem.php b/webapp/src/Entity/ContestProblem.php index 8d966ce35b..238d48e04d 100644 --- a/webapp/src/Entity/ContestProblem.php +++ b/webapp/src/Entity/ContestProblem.php @@ -1,6 +1,7 @@ [null, 190]])] #[Serializer\VirtualProperty( - name: 'id', + name: 'probid', exp: 'object.getProblem().getProbid()', - options: [new Serializer\Type('string')] + options: [new Serializer\Groups([ARC::GROUP_NONSTRICT])] )] + #[Serializer\VirtualProperty( name: 'short_name', exp: 'object.getShortname()', - options: [new Serializer\Groups(['Nonstrict']), new Serializer\Type('string')] + options: [new Serializer\Groups([ARC::GROUP_NONSTRICT]), new Serializer\Type('string')] )] class ContestProblem extends BaseApiEntity { @@ -256,11 +258,6 @@ public function getExternalId(): string return $this->getProblem()->getExternalid(); } - public function getApiId(EventLogService $eventLogService): string - { - return $this->getProblem()->getApiId($eventLogService); - } - #[Assert\Callback] public function validate(ExecutionContextInterface $context): void { diff --git a/webapp/src/Entity/DebugPackage.php b/webapp/src/Entity/DebugPackage.php index e8b579730f..2340a012ce 100644 --- a/webapp/src/Entity/DebugPackage.php +++ b/webapp/src/Entity/DebugPackage.php @@ -28,7 +28,7 @@ class DebugPackage private string $filename; #[ORM\ManyToOne] - #[ORM\JoinColumn(name: 'judgehostid', referencedColumnName: 'judgehostid')] + #[ORM\JoinColumn(name: 'judgehostid', referencedColumnName: 'judgehostid', onDelete: 'SET NULL')] private Judgehost $judgehost; public function getDebugPackageId(): int diff --git a/webapp/src/Entity/ExternalIdFromInternalIdInterface.php b/webapp/src/Entity/ExternalIdFromInternalIdInterface.php new file mode 100644 index 0000000000..37128a78bc --- /dev/null +++ b/webapp/src/Entity/ExternalIdFromInternalIdInterface.php @@ -0,0 +1,16 @@ + - */ - public function getExternalRelationships(): array; -} diff --git a/webapp/src/Entity/HasExternalIdInterface.php b/webapp/src/Entity/HasExternalIdInterface.php new file mode 100644 index 0000000000..9313ce0466 --- /dev/null +++ b/webapp/src/Entity/HasExternalIdInterface.php @@ -0,0 +1,11 @@ +submission; } - #[Serializer\VirtualProperty] - #[Serializer\SerializedName('submission_id')] - #[Serializer\Type('string')] public function getSubmissionId(): int { return $this->getSubmission()->getSubmitid(); } + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('submission_id')] + public function getApiSubmissionId(): string + { + return $this->getSubmission()->getExternalid(); + } + public function setContest(?Contest $contest = null): Judging { $this->contest = $contest; @@ -439,19 +443,6 @@ public function getInternalError(): ?InternalError return $this->internalError; } - /** - * Get the entities to check for external ID's while serializing. - * - * This method should return an array with as keys the JSON field names and as values the actual entity - * objects that the SetExternalIdVisitor should check for applicable external ID's. - * - * @return array{submission_id: Submission} - */ - public function getExternalRelationships(): array - { - return ['submission_id' => $this->getSubmission()]; - } - /** * Check whether this judging has started judging */ diff --git a/webapp/src/Entity/JudgingRun.php b/webapp/src/Entity/JudgingRun.php index 2480a215f2..88fddf5436 100644 --- a/webapp/src/Entity/JudgingRun.php +++ b/webapp/src/Entity/JudgingRun.php @@ -66,7 +66,7 @@ class JudgingRun extends BaseApiEntity private Judging $judging; #[ORM\ManyToOne(inversedBy: 'judging_runs')] - #[ORM\JoinColumn(name: 'testcaseid', referencedColumnName: 'testcaseid')] + #[ORM\JoinColumn(name: 'testcaseid', referencedColumnName: 'testcaseid', onDelete: 'CASCADE')] #[Serializer\Exclude] private Testcase $testcase; @@ -82,7 +82,7 @@ class JudgingRun extends BaseApiEntity private Collection $output; #[ORM\ManyToOne(inversedBy: 'judging_runs')] - #[ORM\JoinColumn(name: 'judgetaskid', referencedColumnName: 'judgetaskid')] + #[ORM\JoinColumn(name: 'judgetaskid', referencedColumnName: 'judgetaskid', onDelete: 'CASCADE')] #[Serializer\Exclude] private ?JudgeTask $judgetask = null; diff --git a/webapp/src/Entity/Language.php b/webapp/src/Entity/Language.php index b5fc9b2aa9..ef92d53afc 100644 --- a/webapp/src/Entity/Language.php +++ b/webapp/src/Entity/Language.php @@ -25,7 +25,9 @@ #[ORM\UniqueConstraint(name: 'externalid', columns: ['externalid'], options: ['lengths' => [190]])] #[UniqueEntity(fields: 'langid')] #[UniqueEntity(fields: 'externalid')] -class Language extends BaseApiEntity +class Language extends BaseApiEntity implements + HasExternalIdInterface, + ExternalIdFromInternalIdInterface { #[ORM\Id] #[ORM\Column(length: 32, options: ['comment' => 'Language ID (string)'])] @@ -259,7 +261,7 @@ public function getLangid(): ?string return $this->langid; } - public function setExternalid(string $externalid): Language + public function setExternalid(?string $externalid): Language { $this->externalid = $externalid; return $this; @@ -403,14 +405,15 @@ public function getSubmissions(): Collection public function getAceLanguage(): string { return match ($this->getLangid()) { + 'adb' => 'ada', + 'bash' => 'sh', 'c', 'cpp', 'cxx' => 'c_cpp', - 'pas' => 'pascal', 'hs' => 'haskell', + 'kt' => 'kotlin', + 'pas' => 'pascal', 'pl' => 'perl', - 'bash' => 'sh', - 'py2', 'py3' => 'python', - 'adb' => 'ada', 'plg' => 'prolog', + 'py2', 'py3' => 'python', 'rb' => 'ruby', 'rs' => 'rust', default => $this->getLangid(), diff --git a/webapp/src/Entity/PrefixedExternalIdInShadowModeInterface.php b/webapp/src/Entity/PrefixedExternalIdInShadowModeInterface.php new file mode 100644 index 0000000000..f481c42ec8 --- /dev/null +++ b/webapp/src/Entity/PrefixedExternalIdInShadowModeInterface.php @@ -0,0 +1,16 @@ + 'utf8mb4_bin', ] )] - #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column(options: ['comment' => 'Descriptive name'])] diff --git a/webapp/src/Entity/Submission.php b/webapp/src/Entity/Submission.php index 46f12f6f7d..74f481c4d0 100644 --- a/webapp/src/Entity/Submission.php +++ b/webapp/src/Entity/Submission.php @@ -36,13 +36,16 @@ options: ['lengths' => [null, 190]] )] #[UniqueEntity(fields: 'externalid')] -class Submission extends BaseApiEntity implements ExternalRelationshipEntityInterface +class Submission extends BaseApiEntity implements + HasExternalIdInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInShadowModeInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Submission ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('submitid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected int $submitid; #[ORM\Column( @@ -53,8 +56,7 @@ class Submission extends BaseApiEntity implements ExternalRelationshipEntityInte ] )] #[OA\Property(nullable: true)] - #[Serializer\SerializedName('external_id')] - #[Serializer\Groups([ARC::GROUP_NONSTRICT])] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( @@ -116,7 +118,7 @@ class Submission extends BaseApiEntity implements ExternalRelationshipEntityInte private Team $team; #[ORM\ManyToOne(inversedBy: 'submissions')] - #[ORM\JoinColumn(name: 'userid', referencedColumnName: 'userid', onDelete: 'CASCADE')] + #[ORM\JoinColumn(name: 'userid', referencedColumnName: 'userid', onDelete: 'SET NULL')] #[Serializer\Exclude] private ?User $user = null; @@ -218,7 +220,6 @@ public function getExternalid(): ?string #[Serializer\VirtualProperty] #[Serializer\SerializedName('language_id')] - #[Serializer\Type('string')] public function getLanguageId(): string { return $this->getLanguage()->getExternalid(); @@ -312,14 +313,18 @@ public function getTeam(): Team return $this->team; } - #[Serializer\VirtualProperty] - #[Serializer\SerializedName('team_id')] - #[Serializer\Type('string')] public function getTeamId(): int { return $this->getTeam()->getTeamid(); } + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('team_id')] + public function getApiTeamId(): string + { + return $this->getTeam()->getExternalid(); + } + public function setUser(?User $user = null): Submission { $this->user = $user; @@ -415,14 +420,18 @@ public function getProblem(): Problem return $this->problem; } - #[Serializer\VirtualProperty] - #[Serializer\SerializedName('problem_id')] - #[Serializer\Type('string')] public function getProblemId(): int { return $this->getProblem()->getProbid(); } + #[Serializer\VirtualProperty] + #[Serializer\SerializedName('problem_id')] + public function getApiProblemId(): string + { + return $this->getProblem()->getExternalid(); + } + public function setContestProblem(?ContestProblem $contestProblem = null): Submission { $this->contest_problem = $contestProblem; @@ -445,23 +454,6 @@ public function getRejudging(): ?Rejudging return $this->rejudging; } - /** - * Get the entities to check for external ID's while serializing. - * - * This method should return an array with as keys the JSON field names and as values the actual entity - * objects that the SetExternalIdVisitor should check for applicable external ID's. - * - * @return array{language_id: Language, problem_id: Problem, team_id: Team|null} - */ - public function getExternalRelationships(): array - { - return [ - 'language_id' => $this->getLanguage(), - 'problem_id' => $this->getProblem(), - 'team_id' => $this->getTeam(), - ]; - } - public function isAfterFreeze(): bool { return $this->getContest()->getFreezetime() !== null && (float)$this->getSubmittime() >= (float)$this->getContest()->getFreezetime(); diff --git a/webapp/src/Entity/Team.php b/webapp/src/Entity/Team.php index d789052f7a..717ec37eb0 100644 --- a/webapp/src/Entity/Team.php +++ b/webapp/src/Entity/Team.php @@ -26,7 +26,11 @@ #[ORM\UniqueConstraint(name: 'externalid', columns: ['externalid'], options: ['lengths' => [190]])] #[ORM\UniqueConstraint(name: 'label', columns: ['label'])] #[UniqueEntity(fields: 'externalid')] -class Team extends BaseApiEntity implements ExternalRelationshipEntityInterface, AssetEntityInterface +class Team extends BaseApiEntity implements + HasExternalIdInterface, + AssetEntityInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInterface { final public const DONT_ADD_USER = 'dont-add-user'; final public const CREATE_NEW_USER = 'create-new-user'; @@ -35,15 +39,15 @@ class Team extends BaseApiEntity implements ExternalRelationshipEntityInterface, #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Team ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('teamid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected ?int $teamid = null; #[ORM\Column( nullable: true, options: ['comment' => 'Team ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\Exclude] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( @@ -428,10 +432,9 @@ public function getAffiliation(): ?TeamAffiliation #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('organization_id')] - #[Serializer\Type('string')] - public function getAffiliationId(): ?int + public function getAffiliationId(): ?string { - return $this->getAffiliation()?->getAffilid(); + return $this->getAffiliation()?->getExternalid(); } public function setCategory(?TeamCategory $category = null): Team @@ -567,7 +570,7 @@ public function getUnreadClarifications(): Collection #[Serializer\Type('array')] public function getGroupIds(): array { - return $this->getCategory() ? [$this->getCategory()->getCategoryid()] : []; + return $this->getCategory() ? [$this->getCategory()->getExternalid()] : []; } #[OA\Property(nullable: true)] @@ -597,17 +600,6 @@ public function canViewClarification(Clarification $clarification): bool ($clarification->getSender() === null && $clarification->getRecipient() === null)); } - /** - * @return array{organization_id: TeamAffiliation|null, group_ids: TeamCategory[]} - */ - public function getExternalRelationships(): array - { - return [ - 'organization_id' => $this->getAffiliation(), - 'group_ids' => array_values(array_filter([$this->getCategory()])), - ]; - } - #[Assert\Callback] public function validate(ExecutionContextInterface $context): void { diff --git a/webapp/src/Entity/TeamAffiliation.php b/webapp/src/Entity/TeamAffiliation.php index f1f63a0298..d77958d864 100644 --- a/webapp/src/Entity/TeamAffiliation.php +++ b/webapp/src/Entity/TeamAffiliation.php @@ -1,6 +1,7 @@ 'Team affiliation ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('affilid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected ?int $affilid = null; #[ORM\Column( nullable: true, options: ['comment' => 'Team affiliation ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\Exclude] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( diff --git a/webapp/src/Entity/TeamCategory.php b/webapp/src/Entity/TeamCategory.php index 59f3e49383..0ae161ff87 100644 --- a/webapp/src/Entity/TeamCategory.php +++ b/webapp/src/Entity/TeamCategory.php @@ -28,20 +28,24 @@ options: [new Serializer\Type('boolean'), new Serializer\Groups(['Nonstrict'])] )] #[UniqueEntity(fields: 'externalid')] -class TeamCategory extends BaseApiEntity implements Stringable +class TeamCategory extends BaseApiEntity implements + Stringable, + HasExternalIdInterface, + ExternalIdFromInternalIdInterface, + PrefixedExternalIdInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'Team category ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('categoryid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] protected ?int $categoryid = null; #[ORM\Column( nullable: true, options: ['comment' => 'Team category ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\Exclude] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column( diff --git a/webapp/src/Entity/User.php b/webapp/src/Entity/User.php index dffd6ba111..9a3eee4274 100644 --- a/webapp/src/Entity/User.php +++ b/webapp/src/Entity/User.php @@ -1,6 +1,7 @@ [190]])] #[ORM\UniqueConstraint(name: 'externalid', columns: ['externalid'], options: ['lengths' => [190]])] #[UniqueEntity(fields: 'username', message: "The username '{{ value }}' is already in use.")] -class User extends BaseApiEntity implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface, ExternalRelationshipEntityInterface +class User extends BaseApiEntity implements + UserInterface, + PasswordAuthenticatedUserInterface, + EquatableInterface, + HasExternalIdInterface, + CalculatedExternalIdBasedOnRelatedFieldInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(options: ['comment' => 'User ID', 'unsigned' => true])] - #[Serializer\SerializedName('id')] - #[Serializer\Type('string')] + #[Serializer\SerializedName('userid')] + #[Serializer\Groups([ARC::GROUP_NONSTRICT])] private ?int $userid = null; #[ORM\Column( nullable: true, options: ['comment' => 'User ID in an external system', 'collation' => 'utf8mb4_bin'] )] - #[Serializer\Exclude] + #[Serializer\SerializedName('id')] protected ?string $externalid = null; #[ORM\Column(options: ['comment' => 'User login name'])] @@ -364,13 +370,18 @@ public function getTeamName(): ?string return $this->getTeam()?->getEffectiveName(); } + public function getTeamId(): ?int + { + return $this->getTeam()?->getTeamid(); + } + #[OA\Property(nullable: true)] #[Serializer\VirtualProperty] #[Serializer\SerializedName('team_id')] #[Serializer\Type('string')] - public function getTeamId(): ?int + public function getApiTeamId(): ?string { - return $this->getTeam()?->getTeamid(); + return $this->getTeam()?->getExternalid(); } public function __construct() @@ -500,13 +511,8 @@ public function getUserIdentifier(): string return $this->getUsername(); } - /** - * @return array{team_id: Team|null} - */ - public function getExternalRelationships(): array + public function getCalculatedExternalId(): string { - return [ - 'team_id' => $this->getTeam(), - ]; + return $this->getUsername(); } } diff --git a/webapp/src/Entity/Version.php b/webapp/src/Entity/Version.php index 5b1d2b1c55..f78a98212a 100644 --- a/webapp/src/Entity/Version.php +++ b/webapp/src/Entity/Version.php @@ -37,11 +37,11 @@ class Version private ?string $compilerVersionCommand = null; #[ORM\ManyToOne(targetEntity: Language::class, inversedBy: "versions")] - #[ORM\JoinColumn(name: 'langid', referencedColumnName: 'langid')] + #[ORM\JoinColumn(name: 'langid', referencedColumnName: 'langid', onDelete: 'CASCADE')] private Language $language; #[ORM\ManyToOne(targetEntity: Judgehost::class)] - #[ORM\JoinColumn(name: 'judgehostid', referencedColumnName: 'judgehostid')] + #[ORM\JoinColumn(name: 'judgehostid', referencedColumnName: 'judgehostid', onDelete: 'SET NULL')] private Judgehost $judgehost; #[ORM\Column( diff --git a/webapp/src/EventListener/ApiHeadersListener.php b/webapp/src/EventListener/ApiHeadersListener.php deleted file mode 100644 index 8c06d6f65e..0000000000 --- a/webapp/src/EventListener/ApiHeadersListener.php +++ /dev/null @@ -1,21 +0,0 @@ -getRequest(); - // Check if this is an API request. - if (str_starts_with($request->getPathInfo(), '/api')) { - // It is, so add CORS headers. - $response = $event->getResponse(); - $response->headers->set('Access-Control-Allow-Origin', '*'); - } - } -} diff --git a/webapp/src/EventListener/NoSessionCookieForApiListener.php b/webapp/src/EventListener/NoSessionCookieForApiListener.php new file mode 100644 index 0000000000..69e8e64454 --- /dev/null +++ b/webapp/src/EventListener/NoSessionCookieForApiListener.php @@ -0,0 +1,24 @@ +getRequest(); + $response = $event->getResponse(); + if ($request->attributes->get('_firewall_context') === 'security.firewall.map.context.api') { + $response->headers->removeCookie($request->getSession()->getName()); + } + } +} diff --git a/webapp/src/Form/Type/AbstractExternalIdEntityType.php b/webapp/src/Form/Type/AbstractExternalIdEntityType.php index 4c787c05c5..19d477f85f 100644 --- a/webapp/src/Form/Type/AbstractExternalIdEntityType.php +++ b/webapp/src/Form/Type/AbstractExternalIdEntityType.php @@ -2,6 +2,7 @@ namespace App\Form\Type; +use App\Entity\ExternalIdFromInternalIdInterface; use App\Service\DOMJudgeService; use App\Service\EventLogService; use Symfony\Component\Form\AbstractType; @@ -24,21 +25,19 @@ public function __construct(protected readonly EventLogService $eventLogService) */ protected function addExternalIdField(FormBuilderInterface $builder, string $entity): void { - if ($this->eventLogService->externalIdFieldForEntity($entity) !== null) { - $builder->add('externalid', TextType::class, [ - 'label' => 'External ID', - 'required' => false, - 'empty_data' => '', - 'constraints' => [ - new Regex( - [ - 'pattern' => DOMJudgeService::EXTERNAL_IDENTIFIER_REGEX, - 'message' => 'Only letters, numbers, dashes, underscores and dots are allowed', - ] - ), - new NotBlank(), - ] - ]); - } + $builder->add('externalid', TextType::class, [ + 'label' => 'External ID', + 'help' => 'Leave empty to generate automatically.', + 'required' => false, + 'empty_data' => '', + 'constraints' => [ + new Regex( + [ + 'pattern' => DOMJudgeService::EXTERNAL_IDENTIFIER_REGEX, + 'message' => 'Only letters, numbers, dashes, underscores and dots are allowed.', + ] + ), + ] + ]); } } diff --git a/webapp/src/Form/Type/ContestType.php b/webapp/src/Form/Type/ContestType.php index f186bb8d55..56e3e648dc 100644 --- a/webapp/src/Form/Type/ContestType.php +++ b/webapp/src/Form/Type/ContestType.php @@ -208,7 +208,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $contest = $event->getData(); $form = $event->getForm(); - $id = $contest?->getApiId($this->eventLogService); + $id = $contest?->getExternalid(); if (!$contest || !$this->dj->assetPath($id, 'contest')) { $form->remove('clearBanner'); diff --git a/webapp/src/Form/Type/ExportResultsType.php b/webapp/src/Form/Type/ExportResultsType.php new file mode 100644 index 0000000000..15b4541798 --- /dev/null +++ b/webapp/src/Form/Type/ExportResultsType.php @@ -0,0 +1,78 @@ +em->createQueryBuilder() + ->from(TeamCategory::class, 'c', 'c.categoryid') + ->select('c.sortorder, c.name') + ->where('c.visible = 1') + ->orderBy('c.sortorder') + ->getQuery() + ->getResult(); + $sortOrders = []; + foreach ($teamCategories as $teamCategory) { + $sortOrder = $teamCategory['sortorder']; + if (!array_key_exists($sortOrder, $sortOrders)) { + $sortOrders[$sortOrder] = new stdClass(); + $sortOrders[$sortOrder]->sort_order = $sortOrder; + $sortOrders[$sortOrder]->categories = []; + } + $sortOrders[$sortOrder]->categories[] = $teamCategory['name']; + } + + $builder->add('sortorder', ChoiceType::class, [ + 'choices' => $sortOrders, + 'group_by' => null, + 'choice_label' => fn(stdClass $sortOrder) => sprintf( + '%d with %d %s', + $sortOrder->sort_order, + count($sortOrder->categories), + count($sortOrder->categories) === 1 ? 'category' : 'categories', + ), + 'choice_value' => 'sort_order', + 'choice_attr' => fn(stdClass $sortOrder) => [ + 'data-categories' => json_encode($sortOrder->categories), + ], + 'label' => 'Sort order', + 'help' => '[will be replaced by categories]', + ]); + $builder->add('individually_ranked', ChoiceType::class, [ + 'choices' => [ + 'Yes' => true, + 'No' => false, + ], + 'label' => 'Individually ranked?', + ]); + $builder->add('honors', ChoiceType::class, [ + 'choices' => [ + 'Yes' => true, + 'No' => false, + ], + 'label' => 'Honors?', + ]); + $builder->add('format', ChoiceType::class, [ + 'choices' => [ + 'HTML (display inline)' => 'html_inline', + 'HTML (download)' => 'html_download', + 'TSV' => 'tsv', + ], + 'label' => 'Format', + ]); + $builder->add('export', SubmitType::class, ['icon' => 'fa-download']); + } +} diff --git a/webapp/src/Form/Type/JuryClarificationType.php b/webapp/src/Form/Type/JuryClarificationType.php index c07ef6487d..376ca92f1a 100644 --- a/webapp/src/Form/Type/JuryClarificationType.php +++ b/webapp/src/Form/Type/JuryClarificationType.php @@ -23,7 +23,6 @@ public function __construct( private readonly EntityManagerInterface $em, private readonly ConfigurationService $config, private readonly DOMJudgeService $dj, - private readonly EventLogService $eventLogService, ) {} public function buildForm(FormBuilderInterface $builder, array $options): void @@ -119,10 +118,6 @@ private function getTeamLabel(Team $team): string return sprintf('%s (%s)', $team->getEffectiveName(), $team->getLabel()); } - if ($this->eventLogService->externalIdFieldForEntity($team)) { - return sprintf('%s (%s)', $team->getEffectiveName(), $team->getExternalId()); - } - - return sprintf('%s (t%s)', $team->getEffectiveName(), $team->getTeamid()); + return sprintf('%s (%s)', $team->getEffectiveName(), $team->getExternalId()); } } diff --git a/webapp/src/Form/Type/TeamAffiliationType.php b/webapp/src/Form/Type/TeamAffiliationType.php index 6c4b8cc291..46799a0dbd 100644 --- a/webapp/src/Form/Type/TeamAffiliationType.php +++ b/webapp/src/Form/Type/TeamAffiliationType.php @@ -82,7 +82,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $affiliation = $event->getData(); $form = $event->getForm(); - $id = $affiliation?->getApiId($this->eventLogService); + $id = $affiliation?->getExternalid(); if (!$affiliation || !$this->dj->assetPath($id, 'affiliation')) { $form->remove('clearLogo'); diff --git a/webapp/src/Form/Type/UserRegistrationType.php b/webapp/src/Form/Type/UserRegistrationType.php index 84edde72f7..66ac1b91e3 100644 --- a/webapp/src/Form/Type/UserRegistrationType.php +++ b/webapp/src/Form/Type/UserRegistrationType.php @@ -155,6 +155,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'mapped' => false, 'choice_label' => 'name', 'placeholder' => '-- Select affiliation --', + 'query_builder' => fn(EntityRepository $er) => $er + ->createQueryBuilder('a') + ->orderBy('a.name'), 'attr' => [ 'placeholder' => 'Affiliation', ], diff --git a/webapp/src/Logger/VarargsLogMessageProcessor.php b/webapp/src/Logger/VarargsLogMessageProcessor.php index 787841be6d..ba142c64b5 100644 --- a/webapp/src/Logger/VarargsLogMessageProcessor.php +++ b/webapp/src/Logger/VarargsLogMessageProcessor.php @@ -2,7 +2,8 @@ /* * A simply message processor for Monolog, that uses printf style argument - * passing. + * passing. Only apply this if the message looks like a printf format string + * (aka contains at least one `%`) and the context is a plain list array. */ namespace App\Logger; @@ -17,7 +18,8 @@ class VarargsLogMessageProcessor implements ProcessorInterface { public function __invoke(LogRecord $record): LogRecord { - if (!str_contains($record->message, '%') || empty($record->context)) { + if (!str_contains($record->message, '%') || + empty($record->context) || !array_is_list($record->context)) { return $record; } 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/Security/DOMJudgeBasicAuthenticator.php b/webapp/src/Security/DOMJudgeBasicAuthenticator.php index b39c329060..a2947dbce3 100644 --- a/webapp/src/Security/DOMJudgeBasicAuthenticator.php +++ b/webapp/src/Security/DOMJudgeBasicAuthenticator.php @@ -67,7 +67,11 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio // Otherwise, we pass along to the next authenticator. if ($exception instanceof BadCredentialsException || $exception instanceof UserNotFoundException) { $resp = new Response('', Response::HTTP_UNAUTHORIZED); - $resp->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', 'Secured Area')); + + if (!$request->isXmlHttpRequest()) { + $resp->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', 'Secured Area')); + } + return $resp; } diff --git a/webapp/src/Serializer/ContestProblemVisitor.php b/webapp/src/Serializer/ContestProblemVisitor.php index d11eb2b22c..e4ac6ca5db 100644 --- a/webapp/src/Serializer/ContestProblemVisitor.php +++ b/webapp/src/Serializer/ContestProblemVisitor.php @@ -5,20 +5,14 @@ use App\DataTransferObject\FileWithName; use App\Entity\ContestProblem; use App\Service\DOMJudgeService; -use App\Service\EventLogService; -use App\Utils\Utils; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; use JMS\Serializer\JsonSerializationVisitor; -use JMS\Serializer\Metadata\StaticPropertyMetadata; class ContestProblemVisitor implements EventSubscriberInterface { - public function __construct( - protected readonly DOMJudgeService $dj, - protected readonly EventLogService $eventLogService - ) {} + public function __construct(protected readonly DOMJudgeService $dj) {} /** * @return array @@ -47,8 +41,8 @@ public function onPreSerialize(ObjectEvent $event): void $route = $this->dj->apiRelativeUrl( 'v4_app_api_problem_statement', [ - 'cid' => $contestProblem->getContest()->getApiId($this->eventLogService), - 'id' => $contestProblem->getApiId($this->eventLogService), + 'cid' => $contestProblem->getContest()->getExternalid(), + 'id' => $contestProblem->getExternalId(), ] ); $contestProblem->getProblem()->setStatementForApi(new FileWithName( diff --git a/webapp/src/Serializer/ContestVisitor.php b/webapp/src/Serializer/ContestVisitor.php index 4dc1e7b4a9..b39e3a7fae 100644 --- a/webapp/src/Serializer/ContestVisitor.php +++ b/webapp/src/Serializer/ContestVisitor.php @@ -7,7 +7,6 @@ use App\Entity\Contest; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; -use App\Service\EventLogService; use App\Utils\Utils; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; @@ -20,7 +19,6 @@ class ContestVisitor implements EventSubscriberInterface public function __construct( protected readonly ConfigurationService $config, protected readonly DOMJudgeService $dj, - protected readonly EventLogService $eventLogService ) {} /** @@ -50,7 +48,7 @@ public function onPreSerialize(ObjectEvent $event): void ); $contest->setPenaltyTimeForApi((int)$this->config->get('penalty_time')); - $id = $contest->getApiId($this->eventLogService); + $id = $contest->getExternalid(); // Banner if ($banner = $this->dj->assetPath($id, 'contest', true)) { @@ -81,7 +79,7 @@ public function onPreSerialize(ObjectEvent $event): void $route = $this->dj->apiRelativeUrl( 'v4_contest_problemset', [ - 'cid' => $contest->getApiId($this->eventLogService), + 'cid' => $id, ] ); $mimeType = match ($contest->getContestProblemsetType()) { diff --git a/webapp/src/Serializer/SetExternalIdVisitor.php b/webapp/src/Serializer/SetExternalIdVisitor.php deleted file mode 100644 index 6b400993b9..0000000000 --- a/webapp/src/Serializer/SetExternalIdVisitor.php +++ /dev/null @@ -1,111 +0,0 @@ - - */ - public static function getSubscribedEvents(): array - { - return [ - [ - 'event' => Events::POST_SERIALIZE, - 'format' => 'json', - 'method' => 'onPostSerialize' - ], - ]; - } - - public function onPostSerialize(ObjectEvent $event): void - { - /** @var JsonSerializationVisitor $visitor */ - $visitor = $event->getVisitor(); - $object = $event->getObject(); - - try { - if ($externalIdField = $this->eventLogService->externalIdFieldForEntity($object::class)) { - $method = sprintf('get%s', ucfirst($externalIdField)); - if (method_exists($object, $method)) { - $property = new StaticPropertyMetadata( - $object::class, - 'id', - null - ); - $visitor->visitProperty($property, $object->{$method}()); - } - } elseif (($object instanceof Submission || $object instanceof Clarification) && $object->getExternalid() !== null) { - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID - $property = new StaticPropertyMetadata( - $object::class, - 'id', - null - ); - $visitor->visitProperty($property, $object->getExternalid()); - } - } catch (BadMethodCallException) { - // Ignore these exceptions, as this means this is not an entity or it is not configured. - } - - if ($object instanceof ExternalRelationshipEntityInterface) { - foreach ($object->getExternalRelationships() as $field => $entity) { - try { - if (is_array($entity)) { - if (empty($entity) || !($externalIdField = $this->eventLogService->externalIdFieldForEntity($entity[0]::class))) { - continue; - } - $method = sprintf('get%s', ucfirst($externalIdField)); - $property = new StaticPropertyMetadata( - $object::class, - $field, - null - ); - $data = []; - foreach ($entity as $item) { - $data[] = $item->{$method}(); - } - $visitor->visitProperty($property, $data); - } elseif ($entity && $externalIdField = $this->eventLogService->externalIdFieldForEntity($entity::class)) { - $method = sprintf('get%s', ucfirst($externalIdField)); - if (method_exists($entity, $method)) { - $property = new StaticPropertyMetadata( - $object::class, - $field, - null - ); - $visitor->visitProperty($property, $entity->{$method}()); - } - } elseif ($entity && ($entity instanceof Submission || $entity instanceof Clarification) && $entity->getExternalid() !== null) { - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID - $property = new StaticPropertyMetadata( - $entity::class, - $field, - null - ); - $visitor->visitProperty($property, $entity->getExternalid()); - } - } catch (BadMethodCallException) { - // Ignore these exceptions, as this means this is not an entity or it is not configured. - } - } - } - } -} diff --git a/webapp/src/Serializer/Shadowing/EventDataDenormalizer.php b/webapp/src/Serializer/Shadowing/EventDataDenormalizer.php index 191db01583..c95ac90a1d 100644 --- a/webapp/src/Serializer/Shadowing/EventDataDenormalizer.php +++ b/webapp/src/Serializer/Shadowing/EventDataDenormalizer.php @@ -28,7 +28,7 @@ public function denormalize( string $type, ?string $format = null, array $context = [] - ) { + ): mixed { if (!$this->supportsDenormalization($data, $type, $format, $context)) { throw new InvalidArgumentException('Unsupported data.'); } diff --git a/webapp/src/Serializer/SubmissionVisitor.php b/webapp/src/Serializer/SubmissionVisitor.php index 45d45b8ea9..d85bb1d33b 100644 --- a/webapp/src/Serializer/SubmissionVisitor.php +++ b/webapp/src/Serializer/SubmissionVisitor.php @@ -6,20 +6,17 @@ use App\DataTransferObject\FileWithName; use App\Entity\Submission; use App\Service\DOMJudgeService; -use App\Service\EventLogService; use Doctrine\ORM\EntityManagerInterface; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; use JMS\Serializer\EventDispatcher\ObjectEvent; -use JMS\Serializer\JsonSerializationVisitor; use JMS\Serializer\Metadata\StaticPropertyMetadata; class SubmissionVisitor implements EventSubscriberInterface { public function __construct( protected readonly DOMJudgeService $dj, - protected readonly EventLogService $eventLogService, - protected readonly EntityManagerInterface $em + protected readonly EntityManagerInterface $em, ) {} /** @@ -45,8 +42,8 @@ public function onPreSerialize(ObjectEvent $event): void $route = $this->dj->apiRelativeUrl( 'v4_submission_files', [ - 'cid' => $submission->getContest()->getApiId($this->eventLogService), - 'id' => $submission->getExternalid() ?? $submission->getSubmitid(), + 'cid' => $submission->getContest()->getExternalid(), + 'id' => $submission->getExternalid(), ] ); $property = new StaticPropertyMetadata( diff --git a/webapp/src/Serializer/TeamAffiliationVisitor.php b/webapp/src/Serializer/TeamAffiliationVisitor.php index 98fe4ab536..3a65c8b37e 100644 --- a/webapp/src/Serializer/TeamAffiliationVisitor.php +++ b/webapp/src/Serializer/TeamAffiliationVisitor.php @@ -6,7 +6,6 @@ use App\Entity\TeamAffiliation; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; -use App\Service\EventLogService; use App\Utils\Utils; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; @@ -18,8 +17,7 @@ class TeamAffiliationVisitor implements EventSubscriberInterface public function __construct( protected readonly DOMJudgeService $dj, protected readonly ConfigurationService $config, - protected readonly EventLogService $eventLogService, - protected readonly RequestStack $requestStack + protected readonly RequestStack $requestStack, ) {} /** @@ -42,7 +40,7 @@ public function onPreSerialize(ObjectEvent $event): void /** @var TeamAffiliation $affiliation */ $affiliation = $event->getObject(); - $id = $affiliation->getApiId($this->eventLogService); + $id = $affiliation->getExternalid(); // Country flag if ($this->config->get('show_flags') && $affiliation->getCountry()) { diff --git a/webapp/src/Serializer/TeamVisitor.php b/webapp/src/Serializer/TeamVisitor.php index 2b8edf0a5f..45b61e0f73 100644 --- a/webapp/src/Serializer/TeamVisitor.php +++ b/webapp/src/Serializer/TeamVisitor.php @@ -5,7 +5,6 @@ use App\DataTransferObject\ImageFile; use App\Entity\Team; use App\Service\DOMJudgeService; -use App\Service\EventLogService; use App\Utils\Utils; use JMS\Serializer\EventDispatcher\Events; use JMS\Serializer\EventDispatcher\EventSubscriberInterface; @@ -18,7 +17,6 @@ class TeamVisitor implements EventSubscriberInterface { public function __construct( protected readonly DOMJudgeService $dj, - protected readonly EventLogService $eventLogService, protected readonly RequestStack $requestStack ) {} @@ -57,7 +55,7 @@ public function onPostSerialize(ObjectEvent $event): void 'label', null ); - $visitor->visitProperty($property, $team->getApiId($this->eventLogService)); + $visitor->visitProperty($property, $team->getExternalid()); } } @@ -66,7 +64,7 @@ public function onPreSerialize(ObjectEvent $event): void /** @var Team $team */ $team = $event->getObject(); - $id = $team->getApiId($this->eventLogService); + $id = $team->getExternalid(); // Check if the asset actually exists if (!($teamPhoto = $this->dj->assetPath($id, 'team', true))) { diff --git a/webapp/src/Service/AssetUpdateService.php b/webapp/src/Service/AssetUpdateService.php index 2866b1ea2d..feb6936e35 100644 --- a/webapp/src/Service/AssetUpdateService.php +++ b/webapp/src/Service/AssetUpdateService.php @@ -21,7 +21,7 @@ public function updateAssets(AssetEntityInterface &$entity): void foreach ($entity->getAssetProperties() as $assetProperty) { $assetPaths = []; foreach (DOMJudgeService::MIMETYPE_TO_EXTENSION as $mimetype => $extension) { - $assetPaths[$mimetype] = $this->dj->fullAssetPath($entity, $assetProperty, $this->eventLog->externalIdFieldForEntity($entity) !== null, $extension); + $assetPaths[$mimetype] = $this->dj->fullAssetPath($entity, $assetProperty, $extension); } if ($entity->isClearAsset($assetProperty)) { foreach ($assetPaths as $assetPath) { diff --git a/webapp/src/Service/AwardService.php b/webapp/src/Service/AwardService.php index c01acdabe1..0631d00538 100644 --- a/webapp/src/Service/AwardService.php +++ b/webapp/src/Service/AwardService.php @@ -12,24 +12,20 @@ class AwardService /** @var array */ protected array $awardCache = []; - public function __construct(protected readonly EventLogService $eventLogService) - { - } - protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void { $group_winners = $problem_winners = $problem_shortname = []; $groups = []; foreach ($scoreboard->getTeams() as $team) { - $teamid = $team->getApiId($this->eventLogService); + $teamid = $team->getExternalid(); if ($scoreboard->isBestInCategory($team)) { - $catId = $team->getCategory()->getApiId($this->eventLogService); + $catId = $team->getCategory()->getExternalid(); $group_winners[$catId][] = $teamid; $groups[$catId] = $team->getCategory()->getName(); } foreach ($scoreboard->getProblems() as $problem) { $shortname = $problem->getShortname(); - $probid = $problem->getApiId($this->eventLogService); + $probid = $problem->getExternalId(); if ($scoreboard->solvedFirst($team, $problem)) { $problem_winners[$probid][] = $teamid; $problem_shortname[$probid] = $shortname; @@ -56,6 +52,10 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void $overall_winners = $medal_winners = []; $additionalBronzeMedals = $contest->getB() ?? 0; + // Do not consider additional bronze medals until the contest is unfrozen. + if (!$scoreboard->hasRestrictedAccess()) { + $additionalBronzeMedals = 0; + } $currentSortOrder = -1; @@ -74,7 +74,7 @@ protected function loadAwards(Contest $contest, Scoreboard $scoreboard): void continue; } $rank = $teamScore->rank; - $teamid = $teamScore->team->getApiId($this->eventLogService); + $teamid = $teamScore->team->getExternalid(); if ($rank === 1) { $overall_winners[] = $teamid; } @@ -141,7 +141,7 @@ public function getAward(Contest $contest, Scoreboard $scoreboard, string $reque public function medalType(Team $team, Contest $contest, Scoreboard $scoreboard): ?string { - $teamid = $team->getApiId($this->eventLogService); + $teamid = $team->getExternalid(); if (!isset($this->awardCache[$contest->getCid()])) { $this->loadAwards($contest, $scoreboard); } diff --git a/webapp/src/Service/CheckConfigService.php b/webapp/src/Service/CheckConfigService.php index 65070d675c..4fa00537c2 100644 --- a/webapp/src/Service/CheckConfigService.php +++ b/webapp/src/Service/CheckConfigService.php @@ -5,6 +5,7 @@ use App\DataTransferObject\ConfigCheckItem; use App\Entity\ContestProblem; use App\Entity\Executable; +use App\Entity\HasExternalIdInterface; use App\Entity\Language; use App\Entity\Problem; use App\Entity\Team; @@ -485,7 +486,7 @@ public function checkContestBanners(): ConfigCheckItem $desc = ''; $result = 'O'; foreach ($contests as $contest) { - if ($cid = $contest->getApiId($this->eventLogService)) { + if ($cid = $contest->getExternalid()) { $bannerpath = $this->dj->assetPath($cid, 'contest', true); $contestName = 'c' . $contest->getCid() . ' (' . $contest->getShortname() . ')'; if ($bannerpath) { @@ -585,6 +586,16 @@ public function checkProblemsValidate(): ConfigCheckItem } } } + + foreach ($problem->getContestProblems() as $contestProblem) { + if (!$contestProblem->getAllowJudge()) { + $result = 'E'; + $moreproblemerrors[$probid] .= sprintf( + "p%s is disabled in contest '%s'\n", + $probid, $contestProblem->getContest()->getName() + ); + } + } } $desc = ''; @@ -638,6 +649,11 @@ public function checkLanguagesValidate(): ConfigCheckItem $morelanguageerrors[$langid] .= sprintf("Compile script %s exists but is of wrong type (%s instead of compile) for %s\n", $compile, $exec->getType(), $langid); } } + + if ($language->getAllowSubmit() && !$language->getAllowJudge()) { + $result = 'E'; + $morelanguageerrors[$langid] .= sprintf("Language '%s' is allowed to be submit, but not judged.\n", $langid); + } } $desc = ''; @@ -670,7 +686,7 @@ public function checkTeamPhotos(): ConfigCheckItem $desc = ''; $result = 'O'; foreach ($teams as $team) { - if ($tid = $team->getApiId($this->eventLogService)) { + if ($tid = $team->getExternalid()) { $photopath = $this->dj->assetPath($tid, 'team', true); if ($photopath && ($filesize = filesize($photopath)) > 5 * 1024 * 1024) { $result = 'W'; @@ -714,7 +730,7 @@ public function checkAffiliations(): ConfigCheckItem continue; } - if ($aid = $affiliation->getApiId($this->eventLogService)) { + if ($aid = $affiliation->getExternalid()) { $logopath = $this->dj->assetPath($aid, 'affiliation', true); $logopathMask = str_replace('.jpg', '.{jpg,png,svg}', $this->dj->assetPath($aid, 'affiliation', true, 'jpg')); if (!$logopath) { @@ -830,10 +846,8 @@ public function checkAllExternalIdentifiers(): array $class = sprintf('App\\Entity\\%s', $shortClass); try { if (class_exists($class) - // ContestProblem is checked using Problem. - && $class != ContestProblem::class - && ($externalIdField = $this->eventLogService->externalIdFieldForEntity($class))) { - $result[$shortClass] = $this->checkExternalIdentifiers($class, $externalIdField); + && is_a($class, HasExternalIdInterface::class, true)) { + $result[$shortClass] = $this->checkExternalIdentifiers($class); } } catch (BadMethodCallException) { // Ignore, this entity does not have an API endpoint. @@ -847,7 +861,7 @@ public function checkAllExternalIdentifiers(): array /** * @param class-string $class */ - protected function checkExternalIdentifiers(string $class, string $externalIdField): ConfigCheckItem + protected function checkExternalIdentifiers(string $class): ConfigCheckItem { $this->stopwatch->start(__FUNCTION__); $parts = explode('\\', $class); @@ -857,7 +871,7 @@ protected function checkExternalIdentifiers(string $class, string $externalIdFie $rowsWithoutExternalId = $this->em->createQueryBuilder() ->from($class, 'e') ->select('e') - ->andWhere(sprintf('e.%s IS NULL or e.%s = :empty', $externalIdField, $externalIdField)) + ->andWhere('e.externalid IS NULL or e.externalid = :empty') ->setParameter('empty', '') ->getQuery() ->getResult(); diff --git a/webapp/src/Service/Compare/AbstractCompareService.php b/webapp/src/Service/Compare/AbstractCompareService.php new file mode 100644 index 0000000000..783bacba21 --- /dev/null +++ b/webapp/src/Service/Compare/AbstractCompareService.php @@ -0,0 +1,84 @@ +addMessage(MessageType::ERROR, sprintf('File "%s" does not exist', $file1)); + $success = false; + } + if (!file_exists($file2)) { + $this->addMessage(MessageType::ERROR, sprintf('File "%s" does not exist', $file2)); + $success = false; + } + if (!$success) { + return $this->messages; + } + + try { + $object1 = $this->parseFile($file1); + } catch (ExceptionInterface $e) { + $this->addMessage(MessageType::ERROR, sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); + } + try { + $object2 = $this->parseFile($file2); + } catch (ExceptionInterface $e) { + $this->addMessage(MessageType::ERROR, sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); + } + + if (!isset($object1) || !isset($object2)) { + return $this->messages; + } + + $this->compare($object1, $object2); + + return $this->messages; + } + + /** + * @return T|null + * @throws ExceptionInterface + */ + abstract protected function parseFile(string $file); + + /** + * @param T $object1 + * @param T $object2 + */ + abstract public function compare($object1, $object2): void; + + protected function addMessage( + MessageType $type, + string $message, + ?string $source = null, + ?string $target = null, + ): void { + $this->messages[] = new Message($type, $message, $source, $target); + } + + /** + * @return Message[] + */ + public function getMessages(): array + { + return $this->messages; + } +} diff --git a/webapp/src/Service/Compare/AwardCompareService.php b/webapp/src/Service/Compare/AwardCompareService.php new file mode 100644 index 0000000000..961de608c7 --- /dev/null +++ b/webapp/src/Service/Compare/AwardCompareService.php @@ -0,0 +1,61 @@ + + */ +class AwardCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + return $this->serializer->deserialize(file_get_contents($file), Award::class . '[]', 'json'); + } + + public function compare($object1, $object2): void + { + $awards1Indexed = []; + foreach ($object1 as $award) { + $awards1Indexed[$award->id] = $award; + } + + $awards2Indexed = []; + foreach ($object2 as $award) { + $awards2Indexed[$award->id] = $award; + } + + foreach ($awards1Indexed as $awardId => $award) { + if (!isset($awards2Indexed[$awardId])) { + if (!$award->teamIds) { + $this->addMessage(MessageType::INFO, sprintf('Award "%s" not found in second file, but has no team ID\'s in first file', $awardId)); + } else { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" not found in second file', $awardId)); + } + } else { + $award2 = $awards2Indexed[$awardId]; + if ($award->citation !== $award2->citation) { + $this->addMessage(MessageType::WARNING, sprintf('Award "%s" has different citation', $awardId), $award->citation, $award2->citation); + } + $award1TeamIds = $award->teamIds; + sort($award1TeamIds); + $award2TeamIds = $award2->teamIds; + sort($award2TeamIds); + if ($award1TeamIds !== $award2TeamIds) { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" has different team ID\'s', $awardId), implode(', ', $award->teamIds), implode(', ', $award2->teamIds)); + } + } + } + + foreach ($awards2Indexed as $awardId => $award) { + if (!isset($awards1Indexed[$awardId])) { + if (!$award->teamIds) { + $this->addMessage(MessageType::INFO, sprintf('Award "%s" not found in first file, but has no team ID\'s in second file', $awardId)); + } else { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" not found in first file', $awardId)); + } + } + } + } +} diff --git a/webapp/src/Service/Compare/Message.php b/webapp/src/Service/Compare/Message.php new file mode 100644 index 0000000000..cd198a3436 --- /dev/null +++ b/webapp/src/Service/Compare/Message.php @@ -0,0 +1,13 @@ + + */ +class ResultsCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + $resultsContents = file_get_contents($file); + if (!str_starts_with($resultsContents, "results\t1")) { + $this->addMessage(MessageType::ERROR, sprintf("File \"%s\" does not start with \"results\t1\"", $file)); + return null; + } + + $resultsContents = substr($resultsContents, strpos($resultsContents, "\n") + 1); + + // Prefix file with a fake header, so we can deserialize them + $resultsContents = "team_id\trank\taward\tnum_solved\ttotal_time\ttime_of_last_submission\tgroup_winner\n" . $resultsContents; + + $results = $this->serializer->deserialize($resultsContents, ResultRow::class . '[]', 'csv', [ + CsvEncoder::DELIMITER_KEY => "\t", + ]); + + // Sort results: first by num_solved, then by total_time + usort($results, fn( + ResultRow $a, + ResultRow $b + ) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved); + + return $results; + } + + public function compare($object1, $object2): void + { + /** @var array $results1Indexed */ + $results1Indexed = []; + foreach ($object1 as $result) { + $results1Indexed[$result->teamId] = $result; + } + + /** @var array $results2Indexed */ + $results2Indexed = []; + foreach ($object2 as $result) { + $results2Indexed[$result->teamId] = $result; + } + + foreach ($object1 as $result) { + if (!isset($results2Indexed[$result->teamId])) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" not found in second file', $result->teamId)); + } else { + $result2 = $results2Indexed[$result->teamId]; + if ($result->rank !== $result2->rank) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different rank', $result->teamId), (string)$result->rank, (string)$result2->rank); + } + if ($result->award !== $result2->award) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different award', $result->teamId), $result->award, $result2->award); + } + if ($result->numSolved !== $result2->numSolved) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different num solved', $result->teamId), (string)$result->numSolved, (string)$result2->numSolved); + } + if ($result->totalTime !== $result2->totalTime) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different total time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime); + } + if ($result->timeOfLastSubmission !== $result2->timeOfLastSubmission) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different last time', $result->teamId), (string)$result->timeOfLastSubmission, (string)$result2->timeOfLastSubmission); + } + if ($result->groupWinner !== $result2->groupWinner) { + $this->addMessage(MessageType::WARNING, sprintf('Team "%s" has different group winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner); + } + } + } + + foreach ($object2 as $result) { + if (!isset($results1Indexed[$result->teamId])) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" not found in first file', $result->teamId)); + } + } + } +} diff --git a/webapp/src/Service/Compare/ScoreboardCompareService.php b/webapp/src/Service/Compare/ScoreboardCompareService.php new file mode 100644 index 0000000000..1f05941ae9 --- /dev/null +++ b/webapp/src/Service/Compare/ScoreboardCompareService.php @@ -0,0 +1,134 @@ + + */ +class ScoreboardCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + return $this->serializer->deserialize(file_get_contents($file), Scoreboard::class, 'json', ['disable_type_enforcement' => true]); + } + + public function compare($object1, $object2): void + { + if ($object1->eventId !== $object2->eventId) { + $this->addMessage(MessageType::INFO, 'Event ID does not match', $object1->eventId, $object2->eventId); + } + + if ($object1->time !== $object2->time) { + $this->addMessage(MessageType::INFO, 'Time does not match', $object1->time, $object2->time); + } + + if ($object1->contestTime !== $object2->contestTime) { + $this->addMessage(MessageType::INFO, 'Contest time does not match', $object1->contestTime, $object2->contestTime); + } + + if (($object1->state->started ?? '') !== ($object2->state->started ?? '')) { + $this->addMessage(MessageType::WARNING, 'State started does not match', $object1->state->started, $object2->state->started); + } + + if (($object1->state->ended ?? '') !== ($object2->state->ended ?? '')) { + $this->addMessage(MessageType::WARNING, 'State ended does not match', $object1->state->ended, $object2->state->ended); + } + + if (($object1->state->frozen ?? '') !== ($object2->state->frozen ?? '')) { + $this->addMessage(MessageType::WARNING, 'State frozen does not match', $object1->state->frozen, $object2->state->frozen); + } + + if (($object1->state->thawed ?? '') !== ($object2->state->thawed ?? '')) { + $this->addMessage(MessageType::WARNING, 'State thawed does not match', $object1->state->thawed, $object2->state->thawed); + } + + if (($object1->state->finalized ?? '') !== ($object2->state->finalized ?? '')) { + $this->addMessage(MessageType::WARNING, 'State finalized does not match', $object1->state->finalized, $object2->state->finalized); + } + + if (($object1->state->endOfUpdates ?? '') !== ($object2->state->endOfUpdates ?? '')) { + $this->addMessage(MessageType::WARNING, 'State end of updates does not match', $object1->state->endOfUpdates, $object2->state->endOfUpdates); + } + + if (count($object1->rows) !== count($object2->rows)) { + $this->addMessage(MessageType::ERROR, 'Number of rows does not match', (string)count($object1->rows), (string)count($object2->rows)); + } + + foreach ($object1->rows as $index => $row) { + if ($row->teamId !== $object2->rows[$index]->teamId) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: team ID does not match', $index), $row->teamId, $object2->rows[$index]->teamId); + } + + if ($row->rank !== $object2->rows[$index]->rank) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: rank does not match', $index), (string)$row->rank, (string)$object2->rows[$index]->rank); + } + + if ($row->score->numSolved !== $object2->rows[$index]->score->numSolved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: num solved does not match', $index), (string)$row->score->numSolved, (string)$object2->rows[$index]->score->numSolved); + } + + if ($row->score->totalTime !== $object2->rows[$index]->score->totalTime) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: total time does not match', $index), (string)$row->score->totalTime, (string)$object2->rows[$index]->score->totalTime); + } + + foreach ($row->problems as $problem) { + /** @var Problem|null $problemForSecond */ + $problemForSecond = null; + + foreach ($object2->rows[$index]->problems as $problem2) { + // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` + if ($problem->problemId === $problem2->problemId) { + $problemForSecond = $problem2; + break; + } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { + $problemForSecond = $problem2; + break; + } + } + + if ($problemForSecond === null && $problem->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in first file, but not found in second file', $index, $problem->problemId)); + } elseif ($problemForSecond !== null && $problem->solved !== $problemForSecond->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved does not match', $index, $problem->problemId), (string)$problem->solved, (string)$problemForSecond->solved); + } + + if ($problemForSecond) { + if ($problem->numJudged !== $problemForSecond->numJudged) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s num judged does not match', $index, $problem->problemId), (string)$problem->numJudged, (string)$problemForSecond->numJudged); + } + + if ($problem->numPending !== $problemForSecond->numPending) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s num pending does not match', $index, $problem->problemId), (string)$problem->numPending, (string)$problemForSecond->numPending); + } + + if ($problem->time !== $problemForSecond->time) { + // This is an info message for now, since PC^2 doesn't expose time info + $this->addMessage(MessageType::INFO, sprintf('Row %d: Problem %s time does not match', $index, $problem->problemId), (string)$problem->time, (string)$problemForSecond->time); + } + } + } + + foreach ($object2->rows[$index]->problems as $problem2) { + $problemForFirst = null; + + foreach ($row->problems as $problem) { + // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` + if ($problem->problemId === $problem2->problemId) { + $problemForFirst = $problem; + break; + } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { + $problemForFirst = $problem; + break; + } + } + + if ($problemForFirst === null && $problem2->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in second file, but not found in first file', $index, $problem2->problemId)); + } + } + } + } +} diff --git a/webapp/src/Service/DOMJudgeService.php b/webapp/src/Service/DOMJudgeService.php index fa787d8ee1..423f7b3f96 100644 --- a/webapp/src/Service/DOMJudgeService.php +++ b/webapp/src/Service/DOMJudgeService.php @@ -67,9 +67,6 @@ class DOMJudgeService protected ?Executable $defaultCompareExecutable = null; protected ?Executable $defaultRunExecutable = null; - final public const DATA_SOURCE_LOCAL = 0; - final public const DATA_SOURCE_CONFIGURATION_EXTERNAL = 1; - final public const DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL = 2; final public const EVAL_DEFAULT = 0; final public const EVAL_LAZY = 1; final public const EVAL_FULL = 2; @@ -99,7 +96,7 @@ public function __construct( protected readonly Environment $twig, #[Autowire('%kernel.project_dir%')] protected string $projectDir, - #[Autowire('%domjudge.libvendordir%')] + #[Autowire('%domjudge.vendordir%')] protected string $vendorDir, ) {} @@ -385,7 +382,7 @@ public function getUpdates(): array ->setParameter('status', 'open') ->getQuery()->getResult(); - if ($this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL) { + if ($this->shadowMode()) { if ($contest) { $shadow_difference_count = $this->em->createQueryBuilder() ->from(Submission::class, 's') @@ -422,18 +419,18 @@ public function getUpdates(): array } } - if ($this->checkrole('balloon')) { + if ($this->checkrole('balloon') && $contest) { $balloonsQuery = $this->em->createQueryBuilder() - ->select('b.balloonid', 't.name', 't.location', 'p.name AS pname') - ->from(Balloon::class, 'b') - ->leftJoin('b.submission', 's') - ->leftJoin('s.problem', 'p') - ->leftJoin('s.contest', 'co') - ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'co.cid = cp.contest AND p.probid = cp.problem') - ->leftJoin('s.team', 't') - ->andWhere('co.cid = :cid') - ->andWhere('b.done = 0') - ->setParameter('cid', $contest->getCid()); + ->select('b.balloonid', 't.name', 't.location', 'p.name AS pname') + ->from(Balloon::class, 'b') + ->leftJoin('b.submission', 's') + ->leftJoin('s.problem', 'p') + ->leftJoin('s.contest', 'co') + ->leftJoin('p.contest_problems', 'cp', Join::WITH, 'co.cid = cp.contest AND p.probid = cp.problem') + ->leftJoin('s.team', 't') + ->andWhere('co.cid = :cid') + ->andWhere('b.done = 0') + ->setParameter('cid', $contest->getCid()); $freezetime = $contest->getFreezeTime(); if ($freezetime !== null && !(bool)$this->config->get('show_balloons_postfreeze')) { @@ -710,14 +707,18 @@ public function getCacheDir(): string public function openZipFile(string $filename): ZipArchive { $zip = new ZipArchive(); - $res = $zip->open($filename, ZIPARCHIVE::CHECKCONS); - if ($res === ZIPARCHIVE::ER_NOZIP || $res === ZIPARCHIVE::ER_INCONS) { - throw new BadRequestHttpException('No valid zip archive given'); + $res = $zip->open($filename, ZipArchive::CHECKCONS); + if ($res === ZipArchive::ER_NOZIP) { + throw new BadRequestHttpException('No valid ZIP archive given.'); + } elseif ($res === ZipArchive::ER_INCONS) { + throw new BadRequestHttpException( + 'ZIP archive is inconsistent; this can happen when using built-in graphical ZIP tools on Mac OS or Ubuntu,' + . ' use the command line zip tool instead, e.g.: zip -r ../problemarchive.zip *'); } elseif ($res === ZIPARCHIVE::ER_MEMORY) { - throw new ServiceUnavailableHttpException(null, 'Not enough memory to extract zip archive'); + throw new ServiceUnavailableHttpException(null, 'Not enough memory to extract ZIP archive.'); } elseif ($res !== true) { throw new ServiceUnavailableHttpException(null, - 'Unknown error while extracting zip archive: ' . print_r($res, true)); + 'Unknown error while extracting ZIP archive: ' . print_r($res, true)); } return $zip; @@ -1165,111 +1166,11 @@ public function maybeCreateJudgeTasks(Judging $judging, int $priority = JudgeTas if ($submission->isImportError()) { return; } - - $evalOnDemand = false; - // We have 2 cases, the problem picks the global value or the value is set. - if ($problem->determineOnDemand($this->config->get('lazy_eval_results'))) { - $evalOnDemand = true; - - // Special case, we're shadow and someone submits on our side in that case - // we're not super lazy. - if ($this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL - && $submission->getExternalid() === null) { - $evalOnDemand = false; - } - if ($manualRequest) { - // When explicitly requested, judge the submission. - $evalOnDemand = false; - } - } - if (!$problem->getAllowJudge() || !$language->getAllowJudge() || $evalOnDemand) { + if (!$this->allowJudge($problem, $submission, $language, $manualRequest)) { return; } - // We use a mass insert query, since that is way faster than doing a separate insert for each testcase. - // We first insert judgetasks, then select their ID's and finally insert the judging runs. - - // Step 1: Create the template for the judgetasks. - $compileExecutable = $submission->getLanguage()->getCompileExecutable()->getImmutableExecutable(); - $judgetaskInsertParams = [ - ':type' => JudgeTaskType::JUDGING_RUN, - ':submitid' => $submission->getSubmitid(), - ':priority' => $priority, - ':jobid' => $judging->getJudgingid(), - ':uuid' => $judging->getUuid(), - ':compile_script_id' => $compileExecutable->getImmutableExecId(), - ':compare_script_id' => $this->getImmutableCompareExecutable($problem)->getImmutableExecId(), - ':run_script_id' => $this->getImmutableRunExecutable($problem)->getImmutableExecId(), - ':compile_config' => $this->getCompileConfig($submission), - ':run_config' => $this->getRunConfig($problem, $submission), - ':compare_config' => $this->getCompareConfig($problem), - ]; - - $judgetaskDefaultParamNames = array_keys($judgetaskInsertParams); - - // Step 2: Create and insert the judgetasks. - - $testcases = $problem->getProblem()->getTestcases(); - if (count($testcases) < 1) { - throw new BadRequestHttpException("No testcases set for problem {$problem->getProbid()}"); - } - $judgetaskInsertParts = []; - /** @var Testcase $testcase */ - foreach ($testcases as $testcase) { - $judgetaskInsertParts[] = sprintf( - '(%s, :testcase_id%d, :testcase_hash%d)', - implode(', ', $judgetaskDefaultParamNames), - $testcase->getTestcaseid(), - $testcase->getTestcaseid() - ); - $judgetaskInsertParams[':testcase_id' . $testcase->getTestcaseid()] = $testcase->getTestcaseid(); - $judgetaskInsertParams[':testcase_hash' . $testcase->getTestcaseid()] = $testcase->getMd5sumInput() . '_' . $testcase->getMd5sumOutput(); - } - $judgetaskColumns = array_map(fn(string $column) => substr($column, 1), $judgetaskDefaultParamNames); - $judgetaskInsertQuery = sprintf( - 'INSERT INTO judgetask (%s, testcase_id, testcase_hash) VALUES %s', - implode(', ', $judgetaskColumns), - implode(', ', $judgetaskInsertParts) - ); - - $judgetaskInsertParamsWithoutColon = []; - foreach ($judgetaskInsertParams as $key => $param) { - $key = str_replace(':', '', $key); - $judgetaskInsertParamsWithoutColon[$key] = $param; - } - - $this->em->getConnection()->executeQuery($judgetaskInsertQuery, $judgetaskInsertParamsWithoutColon); - - // Step 3: Fetch the judgetasks ID's per testcase. - $judgetaskData = $this->em->getConnection()->executeQuery( - 'SELECT judgetaskid, testcase_id FROM judgetask WHERE jobid = :jobid ORDER BY judgetaskid', - ['jobid' => $judging->getJudgingid()] - )->fetchAllAssociative(); - - // Step 4: Create and insert the corresponding judging runs. - $judgingRunInsertParams = [':judgingid' => $judging->getJudgingid()]; - $judgingRunInsertParts = []; - foreach ($judgetaskData as $judgetaskItem) { - $judgingRunInsertParts[] = sprintf( - '(:judgingid, :testcaseid%d, :judgetaskid%d)', - $judgetaskItem['judgetaskid'], - $judgetaskItem['judgetaskid'] - ); - $judgingRunInsertParams[':testcaseid' . $judgetaskItem['judgetaskid']] = $judgetaskItem['testcase_id']; - $judgingRunInsertParams[':judgetaskid' . $judgetaskItem['judgetaskid']] = $judgetaskItem['judgetaskid']; - } - $judgingRunInsertQuery = sprintf( - 'INSERT INTO judging_run (judgingid, testcaseid, judgetaskid) VALUES %s', - implode(', ', $judgingRunInsertParts) - ); - - $judgingRunInsertParamsWithoutColon = []; - foreach ($judgingRunInsertParams as $key => $param) { - $key = str_replace(':', '', $key); - $judgingRunInsertParamsWithoutColon[$key] = $param; - } - - $this->em->getConnection()->executeQuery($judgingRunInsertQuery, $judgingRunInsertParamsWithoutColon); + $this->actuallyCreateJudgetasks($priority, $judging); $team = $submission->getTeam(); $result = $this->em->createQueryBuilder() @@ -1301,14 +1202,17 @@ public function maybeCreateJudgeTasks(Judging $judging, int $priority = JudgeTas // - the new submission would get X+5+60 (since there's only one of their submissions still to be worked on), // but we want to judge submissions of this team in order, so we take the current max (X+120) and add 1. $teamPriority = (int)(max($result['max']+1, $submission->getSubmittime() + 60*$result['count'])); - $queueTask = new QueueTask(); - $queueTask->setJudging($judging) - ->setPriority($priority) - ->setTeam($team) - ->setTeamPriority($teamPriority) - ->setStartTime(null); - $this->em->persist($queueTask); - $this->em->flush(); + // Use a direct query to speed things up + $this->em->getConnection()->executeQuery( + 'INSERT INTO queuetask (judgingid, priority, teamid, teampriority, starttime) + VALUES (:judgingid, :priority, :teamid, :teampriority, null)', + [ + 'judgingid' => $judging->getJudgingid(), + 'priority' => $priority, + 'teamid' => $team->getTeamid(), + 'teampriority' => $teamPriority, + ] + ); } public function getImmutableCompareExecutable(ContestProblem $problem): ImmutableExecutable @@ -1404,7 +1308,7 @@ public function getAssetFiles(string $path): array * @param bool $fullPath If true, get the full path. If false, get the webserver relative path * @param string|null $forceExtension If set, also return the asset path if it does not exist currently and use the given extension */ - public function assetPath(string $name, string $type, bool $fullPath = false, ?string $forceExtension = null): ?string + public function assetPath(?string $name, string $type, bool $fullPath = false, ?string $forceExtension = null): ?string { $prefix = $fullPath ? ($this->getDomjudgeWebappDir() . '/public/') : ''; switch ($type) { @@ -1449,20 +1353,20 @@ public function globalBannerAssetPath(): ?string /** * Get the full asset path for the given entity and property. */ - public function fullAssetPath(AssetEntityInterface $entity, string $property, bool $useExternalid, ?string $forceExtension = null): ?string + public function fullAssetPath(AssetEntityInterface $entity, string $property, ?string $forceExtension = null): ?string { if ($entity instanceof Team && $property == 'photo') { - return $this->assetPath($useExternalid ? $entity->getExternalid() : (string)$entity->getTeamid(), 'team', true, $forceExtension); + return $this->assetPath($entity->getExternalid(), 'team', true, $forceExtension); } elseif ($entity instanceof TeamAffiliation && $property == 'logo') { - return $this->assetPath($useExternalid ? $entity->getExternalid() : (string)$entity->getAffilid(), 'affiliation', true, $forceExtension); + return $this->assetPath($entity->getExternalid(), 'affiliation', true, $forceExtension); } elseif ($entity instanceof Contest && $property == 'banner') { - return $this->assetPath($useExternalid ? $entity->getExternalid() : (string)$entity->getCid(), 'contest', true, $forceExtension); + return $this->assetPath($entity->getExternalid(), 'contest', true, $forceExtension); } return null; } - public function loadTeam(string $idField, string $teamId, Contest $contest): Team + public function loadTeam(string $teamId, Contest $contest): Team { $queryBuilder = $this->em->createQueryBuilder() ->from(Team::class, 't') @@ -1470,7 +1374,7 @@ public function loadTeam(string $idField, string $teamId, Contest $contest): Tea ->leftJoin('t.category', 'tc') ->leftJoin('t.contests', 'c') ->leftJoin('tc.contests', 'cc') - ->andWhere(sprintf('t.%s = :team', $idField)) + ->andWhere('t.externalid = :team') ->andWhere('t.enabled = 1') ->setParameter('team', $teamId); @@ -1647,4 +1551,122 @@ public function getScoreboardZip( return Utils::streamZipFile($tempFilename, 'contest.zip'); } + + private function allowJudge(ContestProblem $problem, Submission $submission, Language $language, bool $manualRequest): bool + { + if (!$problem->getAllowJudge() || !$language->getAllowJudge()) { + return false; + } + $evalOnDemand = false; + // We have 2 cases, the problem picks the global value or the value is set. + if ($problem->determineOnDemand($this->config->get('lazy_eval_results'))) { + $evalOnDemand = true; + + // Special case, we're shadow and someone submits on our side in that case + // we're not super lazy. + if ($this->shadowMode() && $submission->getExternalid() === ('dj-' . $submission->getSubmitid())) { + $evalOnDemand = false; + } + if ($manualRequest) { + // When explicitly requested, judge the submission. + $evalOnDemand = false; + } + } + return !$evalOnDemand; + } + + private function actuallyCreateJudgetasks(int $priority, Judging $judging): void + { + $submission = $judging->getSubmission(); + $problem = $submission->getContestProblem(); + // We use a mass insert query, since that is way faster than doing a separate insert for each testcase. + // We first insert judgetasks, then select their ID's and finally insert the judging runs. + + // Step 1: Create the template for the judgetasks. + $compileExecutable = $submission->getLanguage()->getCompileExecutable()->getImmutableExecutable(); + $judgetaskInsertParams = [ + ':type' => JudgeTaskType::JUDGING_RUN, + ':submitid' => $submission->getSubmitid(), + ':priority' => $priority, + ':jobid' => $judging->getJudgingid(), + ':uuid' => $judging->getUuid(), + ':compile_script_id' => $compileExecutable->getImmutableExecId(), + ':compare_script_id' => $this->getImmutableCompareExecutable($problem)->getImmutableExecId(), + ':run_script_id' => $this->getImmutableRunExecutable($problem)->getImmutableExecId(), + ':compile_config' => $this->getCompileConfig($submission), + ':run_config' => $this->getRunConfig($problem, $submission), + ':compare_config' => $this->getCompareConfig($problem), + ]; + + $judgetaskDefaultParamNames = array_keys($judgetaskInsertParams); + + // Step 2: Create and insert the judgetasks. + + $testcases = $problem->getProblem()->getTestcases(); + if (count($testcases) < 1) { + throw new BadRequestHttpException("No testcases set for problem {$problem->getProbid()}"); + } + $judgetaskInsertParts = []; + /** @var Testcase $testcase */ + foreach ($testcases as $testcase) { + $judgetaskInsertParts[] = sprintf( + '(%s, :testcase_id%d, :testcase_hash%d)', + implode(', ', $judgetaskDefaultParamNames), + $testcase->getTestcaseid(), + $testcase->getTestcaseid() + ); + $judgetaskInsertParams[':testcase_id' . $testcase->getTestcaseid()] = $testcase->getTestcaseid(); + $judgetaskInsertParams[':testcase_hash' . $testcase->getTestcaseid()] = $testcase->getMd5sumInput() . '_' . $testcase->getMd5sumOutput(); + } + $judgetaskColumns = array_map(fn(string $column) => substr($column, 1), $judgetaskDefaultParamNames); + $judgetaskInsertQuery = sprintf( + 'INSERT INTO judgetask (%s, testcase_id, testcase_hash) VALUES %s', + implode(', ', $judgetaskColumns), + implode(', ', $judgetaskInsertParts) + ); + + $judgetaskInsertParamsWithoutColon = []; + foreach ($judgetaskInsertParams as $key => $param) { + $key = str_replace(':', '', $key); + $judgetaskInsertParamsWithoutColon[$key] = $param; + } + + $this->em->getConnection()->executeQuery($judgetaskInsertQuery, $judgetaskInsertParamsWithoutColon); + + // Step 3: Fetch the judgetasks ID's per testcase. + $judgetaskData = $this->em->getConnection()->executeQuery( + 'SELECT judgetaskid, testcase_id FROM judgetask WHERE jobid = :jobid ORDER BY judgetaskid', + ['jobid' => $judging->getJudgingid()] + )->fetchAllAssociative(); + + // Step 4: Create and insert the corresponding judging runs. + $judgingRunInsertParams = [':judgingid' => $judging->getJudgingid()]; + $judgingRunInsertParts = []; + foreach ($judgetaskData as $judgetaskItem) { + $judgingRunInsertParts[] = sprintf( + '(:judgingid, :testcaseid%d, :judgetaskid%d)', + $judgetaskItem['judgetaskid'], + $judgetaskItem['judgetaskid'] + ); + $judgingRunInsertParams[':testcaseid' . $judgetaskItem['judgetaskid']] = $judgetaskItem['testcase_id']; + $judgingRunInsertParams[':judgetaskid' . $judgetaskItem['judgetaskid']] = $judgetaskItem['judgetaskid']; + } + $judgingRunInsertQuery = sprintf( + 'INSERT INTO judging_run (judgingid, testcaseid, judgetaskid) VALUES %s', + implode(', ', $judgingRunInsertParts) + ); + + $judgingRunInsertParamsWithoutColon = []; + foreach ($judgingRunInsertParams as $key => $param) { + $key = str_replace(':', '', $key); + $judgingRunInsertParamsWithoutColon[$key] = $param; + } + + $this->em->getConnection()->executeQuery($judgingRunInsertQuery, $judgingRunInsertParamsWithoutColon); + } + + public function shadowMode(): bool + { + return (bool)$this->config->get('shadow_mode'); + } } diff --git a/webapp/src/Service/EventLogService.php b/webapp/src/Service/EventLogService.php index 9134c33d10..b2f053858e 100644 --- a/webapp/src/Service/EventLogService.php +++ b/webapp/src/Service/EventLogService.php @@ -7,6 +7,7 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Event; +use App\Entity\HasExternalIdInterface; use App\Entity\Judging; use App\Entity\JudgingRun; use App\Entity\Submission; @@ -30,110 +31,66 @@ class EventLogService { // Keys used in below config: - final public const KEY_TYPE = 'type'; - final public const KEY_URL = 'url'; final public const KEY_ENTITY = 'entity'; final public const KEY_TABLES = 'tables'; - final public const KEY_USE_EXTERNAL_ID = 'use-external-id'; - final public const KEY_ALWAYS_USE_EXTERNAL_ID = 'always-use-external-id'; - final public const KEY_SKIP_IN_EVENT_FEED = 'skip-in-event-feed'; - // Types of endpoints: - final public const TYPE_CONFIGURATION = 'configuration'; - final public const TYPE_LIVE = 'live'; - final public const TYPE_AGGREGATE = 'aggregate'; // Allowed actions: final public const ACTION_CREATE = 'create'; final public const ACTION_UPDATE = 'update'; final public const ACTION_DELETE = 'delete'; - // TODO: Add a way to specify when to use external ID using some (DB) - // config instead of hardcoding it here. Also relates to - // AbstractRestController::getIdField. /** @var mixed[] */ public array $apiEndpoints = [ - 'contests' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, - self::KEY_URL => '', - self::KEY_USE_EXTERNAL_ID => true, - ], + 'contests' => [], 'judgement-types' => [ // hardcoded in $VERDICTS and the API - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_ENTITY => null, self::KEY_TABLES => [], ], - 'languages' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, - self::KEY_USE_EXTERNAL_ID => true, - self::KEY_ALWAYS_USE_EXTERNAL_ID => true, - ], + 'languages' => [], 'problems' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_TABLES => ['problem', 'contestproblem'], - self::KEY_USE_EXTERNAL_ID => true, ], 'groups' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_ENTITY => TeamCategory::class, self::KEY_TABLES => ['team_category'], - self::KEY_USE_EXTERNAL_ID => true, ], 'organizations' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_ENTITY => TeamAffiliation::class, self::KEY_TABLES => ['team_affiliation'], - self::KEY_USE_EXTERNAL_ID => true, ], 'teams' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_TABLES => ['team', 'contestteam'], - self::KEY_USE_EXTERNAL_ID => true, ], 'state' => [ - self::KEY_TYPE => self::TYPE_AGGREGATE, self::KEY_ENTITY => null, self::KEY_TABLES => [], ], - 'submissions' => [ - self::KEY_TYPE => self::TYPE_LIVE, - self::KEY_USE_EXTERNAL_ID => true, - ], + 'submissions' => [], 'judgements' => [ - self::KEY_TYPE => self::TYPE_LIVE, self::KEY_ENTITY => Judging::class, self::KEY_TABLES => ['judging'], ], 'runs' => [ - self::KEY_TYPE => self::TYPE_LIVE, self::KEY_ENTITY => JudgingRun::class, self::KEY_TABLES => ['judging_run'], ], - 'clarifications' => [ - self::KEY_TYPE => self::TYPE_LIVE, - self::KEY_USE_EXTERNAL_ID => true, - ], + 'clarifications' => [], 'awards' => [ - self::KEY_TYPE => self::TYPE_AGGREGATE, self::KEY_ENTITY => null, self::KEY_TABLES => [], ], 'scoreboard' => [ - self::KEY_TYPE => self::TYPE_AGGREGATE, self::KEY_ENTITY => null, self::KEY_TABLES => [], ], 'event-feed' => [ - self::KEY_TYPE => self::TYPE_AGGREGATE, self::KEY_ENTITY => null, self::KEY_TABLES => ['event'], ], 'accounts' => [ - self::KEY_TYPE => self::TYPE_CONFIGURATION, self::KEY_ENTITY => User::class, self::KEY_TABLES => ['user'], - self::KEY_USE_EXTERNAL_ID => true, - self::KEY_SKIP_IN_EVENT_FEED => true, ], ]; @@ -153,9 +110,6 @@ public function __construct( protected readonly LoggerInterface $logger ) { foreach ($this->apiEndpoints as $endpoint => $data) { - if (!array_key_exists(self::KEY_URL, $data)) { - $this->apiEndpoints[$endpoint][self::KEY_URL] = '/' . $endpoint; - } if (!array_key_exists(self::KEY_ENTITY, $data)) { // Determine default controller $inflector = InflectorFactory::create()->build(); @@ -265,12 +219,6 @@ public function log( ); return; } - if ($endpoint[self::KEY_URL] === null) { - $this->logger->warning( - "EventLogService::log: no endpoint for '%s', ignoring", [ $type ] - ); - return; - } // Look up external/API ID from various sources. if ($ids === null) { @@ -355,12 +303,12 @@ public function log( if ($action === self::ACTION_DELETE) { $json = array_values(array_map(fn($id) => ['id' => (string)$id], $ids)); } elseif ($json === null) { - $url = $endpoint[self::KEY_URL]; + $url = $type === 'contests' ? '' : ('/' . $type); // Temporary fix for single/multi contest API: if (isset($contestId)) { - $externalContestIds = $this->getExternalIds('contests', [$contestId]); - $url = '/contests/' . reset($externalContestIds) . $url; + $externalContestId = $this->em->getRepository(Contest::class)->find($contestId)->getExternalid(); + $url = '/contests/' . $externalContestId . $url; } if (in_array($type, ['contests', 'state'])) { @@ -470,12 +418,14 @@ public function addMissingStateEvents(Contest $contest): void // Because some states can happen in multiple different orders, we need to check per // field to see if we have a state event where that field matches the current value. + // If the contest start is disabled, all fields should be null. + // Note that for `started` the check already happens within the method, but we can better be explicit here. $states = [ - 'started' => $contest->getStarttime(), - 'ended' => $contest->getEndtime(), - 'frozen' => $contest->getFreezetime(), - 'thawed' => $contest->getUnfreezetime(), - 'finalized' => $contest->getFinalizetime(), + 'started' => $contest->getStarttimeEnabled() ? $contest->getStarttime() : null, + 'ended' => $contest->getStarttimeEnabled() ? $contest->getEndtime() : null, + 'frozen' => $contest->getStarttimeEnabled() ? $contest->getFreezetime() : null, + 'thawed' => $contest->getStarttimeEnabled() ? $contest->getUnfreezetime() : null, + 'finalized' => $contest->getStarttimeEnabled() ? $contest->getFinalizetime() : null, ]; // Because we have the events in order now, we can keep 'growing' the data to insert, @@ -533,7 +483,7 @@ public function addMissingStateEvents(Contest $contest): void if ($field === 'finalized') { // Insert all awards events. - $url = sprintf('/contests/%s/awards', $contest->getApiId($this)); + $url = sprintf('/contests/%s/awards', $contest->getExternalid()); $awards = []; $this->dj->withAllRoles(function () use ($url, &$awards) { $awards = $this->dj->internalApiRequest($url); @@ -658,19 +608,24 @@ protected function insertEvent( */ public function initStaticEvents(Contest $contest): void { + $staticEventTypes = [ + 'contests', + 'judgement-types', + 'languages', + 'problems', + 'groups', + 'organizations', + 'teams', + ]; // Loop over all configuration endpoints with an URL and check if we have all data. foreach ($this->apiEndpoints as $endpoint => $endpointData) { - if ($endpointData[static::KEY_SKIP_IN_EVENT_FEED] ?? false) { - continue; - } - if ($endpointData[EventLogService::KEY_TYPE] === EventLogService::TYPE_CONFIGURATION && - isset($endpointData[EventLogService::KEY_URL])) { - $contestId = $contest->getApiId($this); + if (in_array($endpoint, $staticEventTypes, true)) { + $contestId = $contest->getExternalid(); // Do an internal API request to the overview URL // of the endpoint to get current data. - $url = sprintf('/contests/%s%s', $contestId, - $endpointData[EventLogService::KEY_URL]); + $urlPart = $endpoint === 'contests' ? '' : ('/' . $endpoint); + $url = sprintf('/contests/%s%s', $contestId, $urlPart); $this->dj->withAllRoles(function () use ($url, &$data) { $data = $this->dj->internalApiRequest($url); }); @@ -737,7 +692,7 @@ protected function hasAllDependentObjectEvents(Contest $contest, string $type, a { // Build up the referenced data to check for. $toCheck = [ - 'contests' => $contest->getApiId($this), + 'contests' => $contest->getExternalid(), ]; switch ($type) { case 'teams': @@ -844,9 +799,6 @@ protected function getExternalIds(string $type, array $ids): array } $endpointData = $this->apiEndpoints[$type]; - if (!isset($endpointData[self::KEY_USE_EXTERNAL_ID])) { - return $ids; - } /** @var class-string $entity */ $entity = $endpointData[self::KEY_ENTITY]; @@ -854,25 +806,7 @@ protected function getExternalIds(string $type, array $ids): array throw new BadMethodCallException(sprintf('No entity defined for type \'%s\'', $type)); } - // Special case for submissions and clarifications: they can have an external ID even if when running in - // full local mode, because one can use the API to upload one with an external ID. - $externalIdAlwaysAllowed = [ - Submission::class => 's.submitid', - Clarification::class => 'clar.clarid', - ]; - if (isset($externalIdAlwaysAllowed[$entity])) { - $fullField = $externalIdAlwaysAllowed[$entity]; - [$table, $field] = explode('.', $fullField); - return array_map(fn(array $item) => $item['externalid'] ?? $item[$field], $this->em->createQueryBuilder() - ->from($entity, $table) - ->select($fullField, sprintf('%s.externalid', $table)) - ->andWhere(sprintf('%s IN (:ids)', $fullField)) - ->setParameter('ids', $ids) - ->getQuery() - ->getScalarResult()); - } - - if (!$this->externalIdFieldForEntity($entity)) { + if (!is_a($entity, HasExternalIdInterface::class, true)) { return $ids; } @@ -896,80 +830,6 @@ protected function getExternalIds(string $type, array $ids): array ); } - /** - * Get the external ID field for a given entity type. Will return null if - * no external ID field should be used. - * @param object|string $entity - */ - public function externalIdFieldForEntity($entity): ?string - { - // Allow passing in a class instance: convert it to its class type. - if (is_object($entity)) { - $entity = $entity::class; - } - // Special case: strip of Doctrine proxies. - if (str_starts_with($entity, 'Proxies\\__CG__\\')) { - $entity = substr($entity, strlen('Proxies\\__CG__\\')); - } - - if (!isset($this->entityToEndpoint[$entity])) { - throw new BadMethodCallException(sprintf('Entity \'%s\' does not have a corresponding endpoint', - $entity)); - } - - $endpointData = $this->apiEndpoints[$this->entityToEndpoint[$entity]]; - - if (!isset($endpointData[self::KEY_USE_EXTERNAL_ID])) { - return null; - } - - $useExternalId = false; - if ($endpointData[self::KEY_ALWAYS_USE_EXTERNAL_ID] ?? false) { - $useExternalId = true; - } else { - $dataSource = $this->config->get('data_source'); - - if ($dataSource !== DOMJudgeService::DATA_SOURCE_LOCAL) { - $endpointType = $endpointData[self::KEY_TYPE]; - if ($endpointType === self::TYPE_CONFIGURATION && - in_array($dataSource, [ - DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL, - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL - ])) { - $useExternalId = true; - } elseif ($endpointType === self::TYPE_LIVE && - $dataSource === DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL) { - $useExternalId = true; - } - } - } - - if ($useExternalId) { - return 'externalid'; - } else { - return null; - } - } - - /** - * Get the API ID field for a given entity type. - * @param object|string $entity - */ - public function apiIdFieldForEntity($entity): string - { - if ($field = $this->externalIdFieldForEntity($entity)) { - return $field; - } - /** @var class-string $class */ - $class = is_object($entity) ? $entity::class : $entity; - $metadata = $this->em->getClassMetadata($class); - try { - return $metadata->getSingleIdentifierFieldName(); - } catch (MappingException) { - throw new BadMethodCallException("Entity '$class' has a composite primary key"); - } - } - /** * Get the endpoint to use for the given entity. * @param object|string $entity diff --git a/webapp/src/Service/ExternalContestSourceService.php b/webapp/src/Service/ExternalContestSourceService.php index aab49ee951..54e2d96d7c 100644 --- a/webapp/src/Service/ExternalContestSourceService.php +++ b/webapp/src/Service/ExternalContestSourceService.php @@ -407,10 +407,13 @@ protected function importFromCcsApi(array $eventsToSkip, ?callable $progressRepo $this->setLastEvent($this->getLastReadEventId()); } } catch (TransportException $e) { - $this->logger->error( - 'Received error while reading event feed: %s', - [$e->getMessage()] - ); + if (!str_starts_with($e->getMessage(), 'OpenSSL SSL_read: error:0A000126')) { + // Ignore error of not fully compliant TLS implementation on server-side + $this->logger->error( + 'Received error while reading event feed: %s', + [$e->getMessage()] + ); + } } } @@ -620,7 +623,7 @@ public function importEvent(Event $event, array $eventsToSkip): void // Note the @vars here are to make PHPStan understand the correct types. $method = match ($event->type) { - EventType::AWARDS, EventType::TEAM_MEMBERS, EventType::ACCOUNTS, EventType::PERSONS => $this->ignoreEvent(...), + EventType::ACCOUNTS, EventType::AWARDS, EventType::MAP_INFO, EventType::PERSONS, EventType::TEAM_MEMBERS => $this->ignoreEvent(...), EventType::STATE => $this->validateState(...), EventType::CONTESTS => $this->validateAndUpdateContest(...), EventType::JUDGEMENT_TYPES => $this->importJudgementType(...), @@ -748,8 +751,8 @@ protected function validateAndUpdateContest(Event $event, EventData $data): void $toCheck = [ 'start_time_enabled' => true, 'start_time_string' => $startTime->format('Y-m-d H:i:s ') . $timezoneToUse, - 'end_time' => $contest->getAbsoluteTime($fullDuration), - 'freeze_time' => $contest->getAbsoluteTime($fullFreeze), + 'end_time_string' => preg_replace('/\.000$/', '', $fullDuration), + 'freeze_time_string' => preg_replace('/\.000$/', '', $fullFreeze), ]; } else { $toCheck = [ diff --git a/webapp/src/Service/ImportExportService.php b/webapp/src/Service/ImportExportService.php index e75a7142f7..b80dfc52cf 100644 --- a/webapp/src/Service/ImportExportService.php +++ b/webapp/src/Service/ImportExportService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\DataTransferObject\ResultRow; use App\Entity\Configuration; use App\Entity\Contest; use App\Entity\ContestProblem; @@ -63,6 +64,14 @@ public function getContestYamlData(Contest $contest, bool $includeProblems = tru if ($warnMsg = $contest->getWarningMessage()) { $data['warning_message'] = $warnMsg; } + + foreach (['gold', 'silver', 'bronze'] as $medal) { + $medalCount = $contest->{'get' . ucfirst($medal) . 'Medals'}(); + if ($medalCount) { + $data['medals'][$medal] = $medalCount; + } + } + if ($contest->getFreezetime() !== null) { $data['scoreboard_freeze_time'] = Utils::absTime($contest->getFreezetime(), true); $data['scoreboard_freeze_duration'] = Utils::relTime( @@ -229,6 +238,13 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, st ->setMedalsEnabled(true) ->addMedalCategory($visibleCategory); } + + foreach (['gold', 'silver', 'bronze'] as $medal) { + if (isset($data['medals'][$medal])) { + $setter = 'set' . ucfirst($medal) . 'Medals'; + $contest->$setter($data['medals'][$medal]); + } + } } /** @var string|null $freezeDuration */ @@ -297,7 +313,7 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, st $this->importProblemsData($contest, $data['problems']); } - $cid = $contest->getApiId($this->eventLogService); + $cid = $contest->getExternalid(); $this->em->flush(); return true; @@ -309,8 +325,9 @@ public function importContestData(mixed $data, ?string &$errorMessage = null, st * problems?: array{name?: string, short-name?: string, id?: string, label?: string, * letter?: string, label?: string, letter?: string}} $problems * @param string[]|null $ids + * @param array $messages */ - public function importProblemsData(Contest $contest, array $problems, array &$ids = null): bool + public function importProblemsData(Contest $contest, array $problems, array &$ids = null, ?array &$messages = []): bool { // For problemset.yaml the root key is called `problems`, so handle that case // TODO: Move this check away to make the $problems array shape easier @@ -329,8 +346,19 @@ public function importProblemsData(Contest $contest, array $problems, array &$id ->setTimelimit($problemData['time_limit'] ?? 10) ->setExternalid($problemData['id'] ?? $problemData['short-name'] ?? $problemLabel ?? null); - $this->em->persist($problem); - $this->em->flush(); + $errors = $this->validator->validate($problem); + $hasProblemErrors = $errors->count(); + if ($hasProblemErrors) { + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages['danger'][] = sprintf( + 'Error: problems.%s.%s: %s', + $problem->getExternalid(), + $error->getPropertyPath(), + $error->getMessage() + ); + } + } $contestProblem = new ContestProblem(); $contestProblem @@ -339,14 +367,34 @@ public function importProblemsData(Contest $contest, array $problems, array &$id // We need to set both the entities and the IDs because of the composite primary key. ->setProblem($problem) ->setContest($contest); + + $errors = $this->validator->validate($contestProblem); + $hasContestProblemErrors = $errors->count(); + if ($hasContestProblemErrors) { + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages['danger'][] = sprintf( + 'Error: problems.%s.contestproblem.%s: %s', + $problem->getExternalid(), + $error->getPropertyPath(), + $error->getMessage() + ); + } + } + + if ($hasProblemErrors || $hasContestProblemErrors) { + return false; + } + + $this->em->persist($problem); $this->em->persist($contestProblem); + $this->em->flush(); - $ids[] = $problem->getApiId($this->eventLogService); + $ids[] = $problem->getExternalid(); } $this->em->flush(); - // For now this method will never fail so always return true. return true; } @@ -367,7 +415,7 @@ public function getGroupData(): array $data = []; foreach ($categories as $category) { - $data[] = [$category->getApiId($this->eventLogService), $category->getName()]; + $data[] = [$category->getExternalid(), $category->getName()]; } return $data; @@ -392,9 +440,9 @@ public function getTeamData(): array $data = []; foreach ($teams as $team) { $data[] = [ - $team->getApiId($this->eventLogService), + $team->getExternalid(), $team->getIcpcId(), - $team->getCategory()->getApiId($this->eventLogService), + $team->getCategory()->getExternalid(), $team->getEffectiveName(), $team->getAffiliation() ? $team->getAffiliation()->getName() : '', $team->getAffiliation() ? $team->getAffiliation()->getShortname() : '', @@ -407,21 +455,13 @@ public function getTeamData(): array } /** - * Get results data for the given sortorder. - * - * We'll here assume that the requested file will be of the current contest, - * as all our scoreboard interfaces do: - * 0 ICPC ID 24314 string - * 1 Rank in contest 1 integer|'' - * 2 Award Gold Medal string - * 3 Number of problems the team has solved 4 integer - * 4 Total Time 534 integer - * 5 Time of the last submission 233 integer - * 6 Group Winner North American string - * @return array + * @return ResultRow[] */ - public function getResultsData(int $sortOrder, bool $full = false): array - { + public function getResultsData( + int $sortOrder, + bool $individuallyRanked = false, + bool $honors = true, + ): array { $contest = $this->dj->getCurrentContest(); if ($contest === null) { throw new BadRequestHttpException('No current contest'); @@ -465,9 +505,14 @@ public function getResultsData(int $sortOrder, bool $full = false): array } } - $ranks = []; - $groupWinners = []; - $data = []; + $ranks = []; + $groupWinners = []; + $data = []; + $lowestMedalPoints = 0; + + // For every team that we skip because it is not in a medal category, we need to include one + // additional rank. So keep track of the number of skipped teams + $skippedTeams = 0; foreach ($scoreboard->getScores() as $teamScore) { if ($teamScore->team->getCategory()->getSortorder() !== $sortOrder) { @@ -481,72 +526,102 @@ public function getResultsData(int $sortOrder, bool $full = false): array $rank = $teamScore->rank; $numPoints = $teamScore->numPoints; - if ($rank <= $contest->getGoldMedals()) { + $skip = false; + + if (!$contest->getMedalCategories()->contains($teamScore->team->getCategory())) { + $skip = true; + $skippedTeams++; + } + + if ($numPoints === 0) { + // Teams with 0 points won't get a medal, a rank or an honor. + // They will always get an honorable mention. + $data[] = new ResultRow( + $teamScore->team->getIcpcId(), + null, + 'Honorable', + $teamScore->numPoints, + $teamScore->totalTime, + $maxTime, + null + ); + continue; + } + + if (!$skip && $rank - $skippedTeams <= $contest->getGoldMedals()) { $awardString = 'Gold Medal'; - } elseif ($rank <= $contest->getGoldMedals() + $contest->getSilverMedals()) { + $lowestMedalPoints = $teamScore->numPoints; + } elseif (!$skip && $rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals()) { $awardString = 'Silver Medal'; - } elseif ($rank <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $contest->getB()) { + $lowestMedalPoints = $teamScore->numPoints; + } elseif (!$skip && $rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $contest->getB()) { $awardString = 'Bronze Medal'; + $lowestMedalPoints = $teamScore->numPoints; } elseif ($numPoints >= $median) { // Teams with equally solved number of problems get the same rank unless $full is true. - if (!$full) { + if (!$individuallyRanked) { if (!isset($ranks[$numPoints])) { $ranks[$numPoints] = $rank; } $rank = $ranks[$numPoints]; } - $awardString = 'Ranked'; + if ($honors) { + if ($numPoints === $lowestMedalPoints + || $rank - $skippedTeams <= $contest->getGoldMedals() + $contest->getSilverMedals() + $contest->getBronzeMedals() + $contest->getB()) { + // Some teams out of the medal categories but ranked higher than bronze medallists may get more points. + $awardString = 'Highest Honors'; + } elseif ($numPoints === $lowestMedalPoints - 1) { + $awardString = 'High Honors'; + } else { + $awardString = 'Honors'; + } + } else { + $awardString = 'Ranked'; + } } else { $awardString = 'Honorable'; - $rank = ''; + $rank = null; } - $groupWinner = ""; $categoryId = $teamScore->team->getCategory()->getCategoryid(); - if (!isset($groupWinners[$categoryId])) { + if (isset($groupWinners[$categoryId])) { + $groupWinner = null; + } else { $groupWinners[$categoryId] = true; $groupWinner = $teamScore->team->getCategory()->getName(); } - $data[] = [ + $data[] = new ResultRow( $teamScore->team->getIcpcId(), $rank, $awardString, $teamScore->numPoints, $teamScore->totalTime, $maxTime, - $groupWinner - ]; + $groupWinner, + ); } // Sort by rank/name. - uasort($data, function ($a, $b) use ($teams) { - if ($a[1] != $b[1]) { + uasort($data, function (ResultRow $a, ResultRow $b) use ($teams) { + if ($a->rank !== $b->rank) { // Honorable mention has no rank. - if ($a[1] === '') { + if ($a->rank === null) { return 1; - } elseif ($b[1] === '') { + } elseif ($b->rank === null) { return -11; } - return $a[1] - $b[1]; - } - $teamA = $teams[$a[0]] ?? null; - $teamB = $teams[$b[0]] ?? null; - if ($teamA) { - $nameA = $teamA->getEffectiveName(); - } else { - $nameA = ''; - } - if ($teamB) { - $nameB = $teamB->getEffectiveName(); - } else { - $nameB = ''; + return $a->rank <=> $b->rank; } + $teamA = $teams[$a->teamId] ?? null; + $teamB = $teams[$b->teamId] ?? null; + $nameA = $teamA?->getEffectiveName(); + $nameB = $teamB?->getEffectiveName(); $collator = new Collator('en'); return $collator->compare($nameA, $nameB); }); - return $data; + return array_values($data); } /** @@ -644,7 +719,7 @@ protected function importGroupsTsv(array $content, ?string &$message = null): in /** * Import groups JSON * - * @param array $data * @param TeamCategory[]|null $saved The saved groups */ @@ -656,7 +731,7 @@ public function importGroupsJson(array $data, ?string &$message = null, ?array & $groupData[] = [ 'categoryid' => @$group['id'], 'icpc_id' => @$group['icpc_id'], - 'name' => @$group['name'], + 'name' => $group['name'] ?? '', 'visible' => !($group['hidden'] ?? false), 'sortorder' => @$group['sortorder'], 'color' => @$group['color'], @@ -664,7 +739,7 @@ public function importGroupsJson(array $data, ?string &$message = null, ?array & ]; } - return $this->importGroupData($groupData, $saved); + return $this->importGroupData($groupData, $saved, $message); } /** @@ -676,20 +751,24 @@ public function importGroupsJson(array $data, ?string &$message = null, ?array & * * @throws NonUniqueResultException */ - protected function importGroupData(array $groupData, ?array &$saved = null): int - { + protected function importGroupData( + array $groupData, + ?array &$saved = null, + ?string &$message = null + ): int { // We want to overwrite the ID so change the ID generator. $createdCategories = []; $updatedCategories = []; + $allCategories = []; + $anyErrors = []; - foreach ($groupData as $groupItem) { + foreach ($groupData as $index => $groupItem) { if (empty($groupItem['categoryid'])) { $categoryId = null; $teamCategory = null; } else { $categoryId = $groupItem['categoryid']; - $field = $this->eventLogService->apiIdFieldForEntity(TeamCategory::class); - $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy([$field => $categoryId]); + $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy(['externalid' => $categoryId]); } $added = false; if (!$teamCategory) { @@ -697,7 +776,6 @@ protected function importGroupData(array $groupData, ?array &$saved = null): int if ($categoryId !== null) { $teamCategory->setExternalid($categoryId); } - $this->em->persist($teamCategory); $added = true; } $teamCategory @@ -707,19 +785,41 @@ protected function importGroupData(array $groupData, ?array &$saved = null): int ->setColor($groupItem['color'] ?? null) ->setIcpcid($groupItem['icpc_id'] ?? null); $teamCategory->setAllowSelfRegistration($groupItem['allow_self_registration']); - $this->em->flush(); - $this->dj->auditlog('team_category', $teamCategory->getCategoryid(), 'replaced', - 'imported from tsv / json'); - if ($added) { - $createdCategories[] = $teamCategory->getCategoryid(); + + $errors = $this->validator->validate($teamCategory); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Group at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; } else { - $updatedCategories[] = $teamCategory->getCategoryid(); - } - if ($saved !== null) { - $saved[] = $teamCategory; + $allCategories[] = $teamCategory; + if ($added) { + $createdCategories[] = $teamCategory->getCategoryid(); + } else { + $updatedCategories[] = $teamCategory->getCategoryid(); + } + if ($saved !== null) { + $saved[] = $teamCategory; + } } } + if ($anyErrors) { + return 0; + } + + foreach ($allCategories as $category) { + $this->em->persist($category); + $this->em->flush(); + $this->dj->auditlog('team_category', $category->getCategoryid(), 'replaced', + 'imported from tsv / json'); + } + if ($contest = $this->dj->getCurrentContest()) { if (!empty($createdCategories)) { $this->eventLogService->log('team_category', $createdCategories, 'create', $contest->getCid(), null, null, false); @@ -753,7 +853,7 @@ public function importOrganizationsJson(array $data, ?string &$message = null, ? ]; } - return $this->importOrganizationData($organizationData, $saved); + return $this->importOrganizationData($organizationData, $saved, $message); } /** @@ -764,11 +864,16 @@ public function importOrganizationsJson(array $data, ?string &$message = null, ? * * @throws NonUniqueResultException */ - protected function importOrganizationData(array $organizationData, ?array &$saved = null): int - { + protected function importOrganizationData( + array $organizationData, + ?array &$saved = null, + ?string &$message = null, + ): int { $createdOrganizations = []; $updatedOrganizations = []; - foreach ($organizationData as $organizationItem) { + $allOrganizations = []; + $anyErrors = false; + foreach ($organizationData as $index => $organizationItem) { $externalId = $organizationItem['externalid']; $teamAffiliation = null; $added = false; @@ -778,7 +883,6 @@ protected function importOrganizationData(array $organizationData, ?array &$save if (!$teamAffiliation) { $teamAffiliation = new TeamAffiliation(); $teamAffiliation->setExternalid($externalId); - $this->em->persist($teamAffiliation); $added = true; } if (!isset($organizationItem['shortname'])) { @@ -789,19 +893,40 @@ protected function importOrganizationData(array $organizationData, ?array &$save ->setName($organizationItem['name']) ->setCountry($organizationItem['country']) ->setIcpcid($organizationItem['icpc_id'] ?? null); - $this->em->flush(); - if ($added) { - $createdOrganizations[] = $teamAffiliation->getAffilid(); + $errors = $this->validator->validate($teamAffiliation); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Organization at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; } else { - $updatedOrganizations[] = $teamAffiliation->getAffilid(); - } - $this->dj->auditlog('team_affiliation', $teamAffiliation->getAffilid(), 'replaced', - 'imported from tsv / json'); - if ($saved !== null) { - $saved[] = $teamAffiliation; + $allOrganizations[] = $teamAffiliation; + if ($added) { + $createdOrganizations[] = $teamAffiliation->getAffilid(); + } else { + $updatedOrganizations[] = $teamAffiliation->getAffilid(); + } + if ($saved !== null) { + $saved[] = $teamAffiliation; + } } } + if ($anyErrors) { + return 0; + } + + foreach ($allOrganizations as $organization) { + $this->em->persist($organization); + $this->em->flush(); + $this->dj->auditlog('team_affiliation', $organization->getAffilid(), 'replaced', + 'imported from tsv / json'); + } + if ($contest = $this->dj->getCurrentContest()) { if (!empty($createdOrganizations)) { $this->eventLogService->log('team_affiliation', $createdOrganizations, 'create', $contest->getCid(), null, null, false); @@ -887,7 +1012,7 @@ public function importTeamsJson(array $data, ?string &$message = null, ?array &$ 'label' => $team['label'] ?? null, 'categoryid' => $team['group_ids'][0] ?? null, 'name' => $team['name'] ?? '', - 'display_name' => $team['display_name'] ?? '', + 'display_name' => $team['display_name'] ?? null, 'publicdescription' => $team['public_description'] ?? $team['members'] ?? '', 'location' => $team['location']['description'] ?? null, ], @@ -937,13 +1062,6 @@ public function importAccountsJson(array $data, ?string &$message = null, ?array $type = $account['type']; $username = $account['username']; - $icpcRegexChars = "[a-zA-Z0-9@._-]"; - $icpcRegex = "/^" . $icpcRegexChars . "+$/"; - if (!preg_match($icpcRegex, $username)) { - $message = sprintf('Username "%s" should be non empty and only contain: %s', $username, $icpcRegexChars); - return -1; - } - // Special case for the World Finals, if the username is CDS we limit the access. // The user can see what every admin can see, but can not log in via the UI. if (isset($account['username']) && $account['username'] === 'cds') { @@ -986,7 +1104,7 @@ public function importAccountsJson(array $data, ?string &$message = null, ?array ]; } - return $this->importAccountData($accountData, $saved); + return $this->importAccountData($accountData, $saved, $message); } /** @@ -1004,9 +1122,12 @@ public function importAccountsJson(array $data, ?string &$message = null, ?array protected function importTeamData(array $teamData, ?string &$message, ?array &$saved = null): int { $createdAffiliations = []; + $createdCategories = []; $createdTeams = []; $updatedTeams = []; - foreach ($teamData as $teamItem) { + $allTeams = []; + $anyErrors = false; + foreach ($teamData as $index => $teamItem) { // It is legitimate that a team has no affiliation. Do not add it then. $teamAffiliation = null; $teamCategory = null; @@ -1020,11 +1141,19 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $propertyAccessor->setValue($teamAffiliation, $field, $value); } - $this->em->persist($teamAffiliation); - $this->em->flush(); - $createdAffiliations[] = $teamAffiliation->getAffilid(); - $this->dj->auditlog('team_affiliation', $teamAffiliation->getAffilid(), - 'added', 'imported from tsv'); + $errors = $this->validator->validate($teamAffiliation); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Organization for team at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $createdAffiliations[] = $teamAffiliation; + } } } elseif (!empty($teamItem['team_affiliation']['externalid'])) { $teamAffiliation = $this->em->getRepository(TeamAffiliation::class)->findOneBy(['externalid' => $teamItem['team_affiliation']['externalid']]); @@ -1034,26 +1163,46 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s ->setExternalid($teamItem['team_affiliation']['externalid']) ->setName($teamItem['team_affiliation']['externalid'] . ' - auto-create during import') ->setShortname($teamItem['team_affiliation']['externalid'] . ' - auto-create during import'); - $this->em->persist($teamAffiliation); - $this->dj->auditlog('team_affiliation', - $teamAffiliation->getAffilid(), - 'added', 'imported from tsv / json'); + + $errors = $this->validator->validate($teamAffiliation); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Organization for team at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $createdAffiliations[] = $teamAffiliation; + } } } $teamItem['team']['affiliation'] = $teamAffiliation; unset($teamItem['team']['affilid']); if (!empty($teamItem['team']['categoryid'])) { - $field = $this->eventLogService->apiIdFieldForEntity(TeamCategory::class); - $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy([$field => $teamItem['team']['categoryid']]); + $teamCategory = $this->em->getRepository(TeamCategory::class)->findOneBy(['externalid' => $teamItem['team']['categoryid']]); if (!$teamCategory) { $teamCategory = new TeamCategory(); $teamCategory ->setExternalid($teamItem['team']['categoryid']) ->setName($teamItem['team']['categoryid'] . ' - auto-create during import'); - $this->em->persist($teamCategory); - $this->dj->auditlog('team_category', $teamCategory->getCategoryid(), - 'added', 'imported from tsv'); + + $errors = $this->validator->validate($teamCategory); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Group for team at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $createdCategories[] = $teamCategory; + } } } $teamItem['team']['category'] = $teamCategory; @@ -1063,8 +1212,7 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s if (empty($teamItem['team']['teamid'])) { $team = null; } else { - $field = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $team = $this->em->getRepository(Team::class)->findOneBy([$field => $teamItem['team']['teamid']]); + $team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamItem['team']['teamid']]); } if (!$team) { $team = new Team(); @@ -1083,11 +1231,6 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s return -1; } - if (!$teamItem['team']['name']) { - $message = 'Name for team required'; - return -1; - } - $team->setExternalid($teamItem['team']['teamid']); unset($teamItem['team']['teamid']); @@ -1096,10 +1239,19 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $propertyAccessor->setValue($team, $field, $value); } - if ($added) { - $this->em->persist($team); + $errors = $this->validator->validate($team); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Team at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $allTeams[] = $team; } - $this->em->flush(); if ($added) { $createdTeams[] = $team->getTeamid(); @@ -1107,15 +1259,40 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s $updatedTeams[] = $team->getTeamid(); } - $this->dj->auditlog('team', $team->getTeamid(), 'replaced', 'imported from tsv'); if ($saved !== null) { $saved[] = $team; } } + if ($anyErrors) { + return 0; + } + + foreach ($createdAffiliations as $affiliation) { + $this->em->persist($affiliation); + $this->em->flush(); + $this->dj->auditlog('team_affiliation', + $affiliation->getAffilid(), + 'added', 'imported from tsv / json'); + } + + foreach ($createdCategories as $category) { + $this->em->persist($category); + $this->em->flush(); + $this->dj->auditlog('team_category', $category->getCategoryid(), + 'added', 'imported from tsv'); + } + + foreach ($allTeams as $team) { + $this->em->persist($team); + $this->em->flush(); + $this->dj->auditlog('team', $team->getTeamid(), 'replaced', 'imported from tsv'); + } + if ($contest = $this->dj->getCurrentContest()) { if (!empty($createdAffiliations)) { - $this->eventLogService->log('team_affiliation', $createdAffiliations, + $affiliationIds = array_map(fn (TeamAffiliation $affiliation) => $affiliation->getAffilid(), $createdAffiliations); + $this->eventLogService->log('team_affiliation', $affiliationIds, 'create', $contest->getCid()); } if (!empty($createdTeams)) { @@ -1141,10 +1318,15 @@ protected function importTeamData(array $teamData, ?string &$message, ?array &$s * publicdescription?: string}}> $accountData * @throws NonUniqueResultException */ - protected function importAccountData(array $accountData, ?array &$saved = null): int - { + protected function importAccountData( + array $accountData, + ?array &$saved = null, + ?string &$message = null + ): int { $newTeams = []; - foreach ($accountData as $accountItem) { + $anyErrors = false; + $allUsers = []; + foreach ($accountData as $index => $accountItem) { if (!empty($accountItem['team'])) { $team = $this->em->getRepository(Team::class)->findOneBy([ 'name' => $accountItem['team']['name'], @@ -1157,28 +1339,33 @@ protected function importAccountData(array $accountData, ?array &$saved = null): ->setCategory($accountItem['team']['category']) ->setExternalid($accountItem['team']['externalid']) ->setPublicDescription($accountItem['team']['publicdescription'] ?? null); - $this->em->persist($team); $action = EventLogService::ACTION_CREATE; } else { $action = EventLogService::ACTION_UPDATE; } - $this->em->flush(); - $newTeams[] = [ - 'team' => $team, - 'action' => $action, - ]; - $this->dj->auditlog('team', $team->getTeamid(), 'replaced', - 'imported from tsv, autocreated for judge'); + $errors = $this->validator->validate($team); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } + + $message .= sprintf("Team for user at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $newTeams[] = [ + 'team' => $team, + 'action' => $action, + ]; + } $accountItem['user']['team'] = $team; unset($accountItem['user']['teamid']); } $user = $this->em->getRepository(User::class)->findOneBy(['username' => $accountItem['user']['username']]); if (!$user) { - $user = new User(); - $added = true; - } else { - $added = false; + $user = new User(); } if (array_key_exists('teamid', $accountItem['user'])) { @@ -1186,12 +1373,11 @@ protected function importAccountData(array $accountData, ?array &$saved = null): unset($accountItem['user']['teamid']); $team = null; if ($teamId !== null) { - $field = $this->eventLogService->apiIdFieldForEntity(Team::class); - $team = $this->em->getRepository(Team::class)->findOneBy([$field => $teamId]); + $team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamId]); if (!$team) { $team = new Team(); $team - ->setExternalid($teamId) + ->setExternalid((string)$teamId) ->setName($teamId . ' - auto-create during import'); $this->em->persist($team); $this->dj->auditlog('team', $team->getTeamid(), @@ -1206,15 +1392,41 @@ protected function importAccountData(array $accountData, ?array &$saved = null): $propertyAccessor->setValue($user, $field, $value); } - if ($added) { - $this->em->persist($user); - } - $this->em->flush(); + $errors = $this->validator->validate($user); + if ($errors->count()) { + $messages = []; + /** @var ConstraintViolationInterface $error */ + foreach ($errors as $error) { + $messages[] = sprintf('%s: %s', $error->getPropertyPath(), $error->getMessage()); + } - if ($saved !== null) { - $saved[] = $user; + $message .= sprintf("User at index %d has errors:\n\n%s\n", $index, implode("\n", $messages)); + $anyErrors = true; + } else { + $allUsers[] = $user; + + if ($saved !== null) { + $saved[] = $user; + } } + } + + if ($anyErrors) { + return 0; + } + foreach ($allUsers as $user) { + $this->em->persist($user); + } + + foreach ($newTeams as $newTeam) { + $team = $newTeam['team']; + $this->em->persist($team); + } + + $this->em->flush(); + + foreach ($allUsers as $user) { $this->dj->auditlog('user', $user->getUserid(), 'replaced', 'imported from tsv'); } @@ -1222,6 +1434,8 @@ protected function importAccountData(array $accountData, ?array &$saved = null): foreach ($newTeams as $newTeam) { $team = $newTeam['team']; $action = $newTeam['action']; + $this->dj->auditlog('team', $team->getTeamid(), 'replaced', + 'imported from tsv, autocreated for judge'); $this->eventLogService->log('team', $team->getTeamid(), $action, $contest->getCid()); } } @@ -1288,8 +1502,7 @@ protected function importAccountsTsv(array $content, ?string &$message = null): $line[2]); return -1; } - $field = $this->eventLogService->externalIdFieldForEntity(Team::class) ?? 'teamid'; - $team = $this->em->getRepository(Team::class)->findOneBy([$field => $teamId]); + $team = $this->em->getRepository(Team::class)->findOneBy(['externalid' => $teamId]); if ($team === null) { $message = sprintf('Unknown team id %s on line %d', $teamId, $lineNr); return -1; diff --git a/webapp/src/Service/ImportProblemService.php b/webapp/src/Service/ImportProblemService.php index d2011cb47f..8b58db4f46 100644 --- a/webapp/src/Service/ImportProblemService.php +++ b/webapp/src/Service/ImportProblemService.php @@ -265,10 +265,6 @@ public function importZippedProblem( $yamlData = Yaml::parse($problemYaml); if (!empty($yamlData)) { - if (isset($yamlData['uuid']) && $contestProblem !== null) { - $contestProblem->setShortname($yamlData['uuid']); - } - $yamlProblemProperties = []; if (isset($yamlData['name'])) { if (is_array($yamlData['name'])) { @@ -870,7 +866,7 @@ public function importProblemFromRequest(Request $request, ?int $contestId = nul $problem = $this->em->createQueryBuilder() ->from(Problem::class, 'p') ->select('p') - ->andWhere(sprintf('p.%s = :id', $this->eventLogService->externalIdFieldForEntity(Problem::class) ?? 'probid')) + ->andWhere('p.externalid = :id') ->setParameter('id', $probId) ->getQuery() ->getOneOrNullResult(); @@ -890,7 +886,7 @@ public function importProblemFromRequest(Request $request, ?int $contestId = nul $allMessages = array_merge($allMessages, $messages); if ($newProblem) { $this->dj->auditlog('problem', $newProblem->getProbid(), 'upload zip', $clientName); - $probId = $newProblem->getApiId($this->eventLogService); + $probId = $newProblem->getExternalid(); } else { $errors = array_merge($errors, $messages); } diff --git a/webapp/src/Service/RejudgingService.php b/webapp/src/Service/RejudgingService.php index 092d7ac4cf..280c3b7a21 100644 --- a/webapp/src/Service/RejudgingService.php +++ b/webapp/src/Service/RejudgingService.php @@ -16,6 +16,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; +use Ramsey\Uuid\Uuid; class RejudgingService { @@ -82,50 +83,79 @@ public function createRejudging( $index = 0; $first = true; foreach ($judgings as $judging) { + $submission = $judging->getSubmission(); + $contestProblem = $submission->getContestProblem(); + $language = $submission->getLanguage(); + $index++; - if ($judging->getSubmission()->getRejudging() !== null) { - // The submission is already part of another rejudging, record and skip it. + if ( + // Record and skip submission/judging if it is already part of another judging or is not allowed + // to be judged. + $submission->getRejudging() !== null + || !$contestProblem->getAllowJudge() + || !$language->getAllowJudge() + ) { $skipped[] = $judging; continue; } - $this->em->wrapInTransaction(function () use ( - $priority, - $singleJudging, - $judging, - $rejudging - ) { - $this->em->getConnection()->executeStatement( - 'UPDATE submission SET rejudgingid = :rejudgingid WHERE submitid = :submitid AND rejudgingid IS NULL', - [ - 'rejudgingid' => $rejudging->getRejudgingid(), - 'submitid' => $judging->getSubmissionId(), - ] - ); - - if ($singleJudging) { - $teamid = $judging->getSubmission()->getTeamId(); - if ($teamid) { - $this->em->getConnection()->executeStatement( - 'UPDATE team SET judging_last_started = null WHERE teamid = :teamid', - [ 'teamid' => $teamid ] - ); - } - } - // Give back judging, create a new one. - $newJudging = new Judging(); - $newJudging - ->setContest($judging->getContest()) - ->setValid(false) - ->setSubmission($judging->getSubmission()) - ->setOriginalJudging($judging) - ->setRejudging($rejudging); - $this->em->persist($newJudging); - $this->em->flush(); + // $this->>em->wrapInTransaction flushes the entity manager, which is pretty slow. + // So use the direct connection transaction API here. + $this->em->getConnection()->beginTransaction(); + + $this->em->getConnection()->executeStatement( + 'UPDATE submission SET rejudgingid = :rejudgingid WHERE submitid = :submitid AND rejudgingid IS NULL', + [ + 'rejudgingid' => $rejudging->getRejudgingid(), + 'submitid' => $judging->getSubmissionId(), + ] + ); + + if ($singleJudging) { + $teamid = $judging->getSubmission()->getTeamId(); + if ($teamid) { + $this->em->getConnection()->executeStatement( + 'UPDATE team SET judging_last_started = null WHERE teamid = :teamid', + [ 'teamid' => $teamid ] + ); + } + } - $this->dj->maybeCreateJudgeTasks($newJudging, $priority); - }); + // Give back judging, create a new one. + // Use a direct query to speed things up. + $this->em->getConnection()->executeStatement( + 'INSERT INTO judging (cid, valid, submitid, prevjudgingid, rejudgingid, uuid) + VALUES (:cid, 0, :submitid, :prevjudgingid, :rejudgingid, :uuid)', + [ + 'cid' => $judging->getContest()->getCid(), + 'submitid' => $judging->getSubmissionId(), + 'prevjudgingid' => $judging->getJudgingId(), + 'rejudgingid' => $rejudging->getRejudgingid(), + 'uuid' => Uuid::uuid4()->toString(), + ] + ); + $newJudgingId = $this->em->getConnection()->lastInsertId(); + $newJudging = $this->em->getRepository(Judging::class) + ->createQueryBuilder('j') + ->join('j.submission', 's') + ->join('s.contest_problem', 'cp') + ->join('s.language', 'l') + ->join('l.compile_executable', 'e') + ->join('cp.problem', 'p') + ->leftJoin('p.compare_executable', 'ce') + ->leftJoin('ce.immutableExecutable', 'ice') + ->leftJoin('p.run_executable', 're') + ->leftJoin('re.immutableExecutable', 'ire') + ->select('j', 's', 'cp', 'l', 'e', 'p', 'ce', 'ice', 're', 'ire') + ->andWhere('j.judgingid = :judgingid') + ->setParameter('judgingid', $newJudgingId) + ->getQuery() + ->getSingleResult(); + + $this->dj->maybeCreateJudgeTasks($newJudging, $priority); + + $this->em->getConnection()->commit(); if (!$first) { $log .= ', '; @@ -380,31 +410,27 @@ public function finishRejudging(Rejudging $rejudging, string $action, ?callable public function calculateTodo(Rejudging $rejudging): array { // Make sure we have the most recent data. This is necessary to - // guarantee that repeated rejugdings are scheduled correctly. + // guarantee that repeated rejudgings are scheduled correctly. $this->em->flush(); - $todo = $this->em->createQueryBuilder() - ->from(Submission::class, 's') - ->select('COUNT(s)') - ->andWhere('s.rejudging = :rejudging') - ->setParameter('rejudging', $rejudging) - ->getQuery() - ->getSingleScalarResult(); - - $done = $this->em->createQueryBuilder() + $queryBuilder = $this->em->createQueryBuilder() ->from(Judging::class, 'j') ->select('COUNT(j)') ->andWhere('j.rejudging = :rejudging') + ->setParameter('rejudging', $rejudging); + + $clonedQueryBuilder = clone $queryBuilder; + + $todo = $queryBuilder + ->andWhere('j.endtime IS NULL') + ->getQuery() + ->getSingleScalarResult(); + + $done = $clonedQueryBuilder ->andWhere('j.endtime IS NOT NULL') - // This is necessary for rejudgings which apply automatically. - // We remove the association of the submission with the rejudging, - // but not the one of the judging with the rejudging for accounting reasons. - ->andWhere('j.valid = 0') - ->setParameter('rejudging', $rejudging) ->getQuery() ->getSingleScalarResult(); - $todo -= $done; return ['todo' => $todo, 'done' => $done]; } } diff --git a/webapp/src/Service/ScoreboardService.php b/webapp/src/Service/ScoreboardService.php index 4bad3a1db8..e6f2ea208b 100644 --- a/webapp/src/Service/ScoreboardService.php +++ b/webapp/src/Service/ScoreboardService.php @@ -41,7 +41,6 @@ public function __construct( protected readonly DOMJudgeService $dj, protected readonly ConfigurationService $config, protected readonly LoggerInterface $logger, - protected readonly EventLogService $eventLogService ) {} /** @@ -58,12 +57,13 @@ public function getScoreboard( Contest $contest, bool $jury = false, ?Filter $filter = null, - bool $visibleOnly = false + bool $visibleOnly = false, + bool $forceUnfrozen = false, ): ?Scoreboard { $freezeData = new FreezeData($contest); // Don't leak information before start of contest. - if (!$freezeData->started() && !$jury) { + if (!$freezeData->started() && !$jury && !$forceUnfrozen) { return null; } @@ -74,9 +74,9 @@ public function getScoreboard( return new Scoreboard( $contest, $teams, $categories, $problems, - $scoreCache, $freezeData, $jury, + $scoreCache, $freezeData, $jury || $forceUnfrozen, (int)$this->config->get('penalty_time'), - (bool)$this->config->get('score_in_seconds') + (bool)$this->config->get('score_in_seconds'), ); } @@ -280,7 +280,7 @@ public function calculateScoreRow( } // Determine whether we will use external judgements instead of judgings. - $useExternalJudgements = $this->config->get('data_source') == DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL; + $useExternalJudgements = $this->dj->shadowMode(); // Note the clause 's.submittime < c.endtime': this is used to // filter out TOO-LATE submissions from pending, but it also means @@ -777,7 +777,7 @@ public function getGroupedAffiliations(Contest $contest): array foreach ($category->getTeams() as $team) { if ($teamaffil = $team->getAffiliation()) { $affiliations[$teamaffil->getName()] = [ - 'id' => $teamaffil->getApiId($this->eventLogService), + 'id' => $teamaffil->getExternalid(), 'name' => $teamaffil->getName(), ]; } @@ -891,7 +891,7 @@ public function getScoreboardTwigData( ], 'static' => $static, ]; - if ($static && $contest && $contest->getFreezeData()->showFinal()) { + if ($static && $contest && ($forceUnfrozen || $contest->getFreezeData()->showFinal())) { unset($data['refresh']); $data['refreshstop'] = true; } @@ -903,14 +903,24 @@ public function getScoreboardTwigData( $scoreFilter = null; } if ($scoreboard === null) { - $scoreboard = $this->getScoreboard($contest, $jury, $scoreFilter); + $scoreboard = $this->getScoreboard( + contest: $contest, + jury: $jury, + filter: $scoreFilter, + forceUnfrozen: $forceUnfrozen + ); } if ($forceUnfrozen) { $scoreboard->getFreezeData() ->setForceValue(FreezeData::KEY_SHOW_FROZEN, false) ->setForceValue(FreezeData::KEY_SHOW_FINAL, true) + ->setForceValue(FreezeData::KEY_SHOW_FINAL_JURY, true) ->setForceValue(FreezeData::KEY_FINALIZED, true); + + if (!$contest->getFinalizetime()) { + $contest->setFinalizetime(Utils::now()); + } } $data['contest'] = $contest; diff --git a/webapp/src/Service/StatisticsService.php b/webapp/src/Service/StatisticsService.php index 76a52a5457..15ff41ce32 100644 --- a/webapp/src/Service/StatisticsService.php +++ b/webapp/src/Service/StatisticsService.php @@ -5,6 +5,7 @@ use App\Entity\Contest; use App\Entity\ContestProblem; use App\Entity\Judging; +use App\Entity\Language; use App\Entity\Problem; use App\Entity\Submission; use App\Entity\Team; @@ -58,6 +59,7 @@ public function getTeams(Contest $contest, string $filter): array ->join('t.category', 'tc') ->leftJoin('t.affiliation', 'a') ->join('t.submissions', 'ts') + ->join('ts.language', 'l') ->join('ts.judgings', 'j') ->andWhere('j.valid = true') ->join('ts.language', 'lang') @@ -72,6 +74,7 @@ public function getTeams(Contest $contest, string $filter): array ->join('t.category', 'tc') ->leftJoin('tc.contests', 'cc') ->join('t.submissions', 'ts') + ->join('ts.language', 'l') ->join('ts.judgings', 'j') ->andWhere('j.valid = true') ->join('ts.language', 'lang') @@ -514,6 +517,118 @@ public function getGroupedProblemsStats( return $stats; } + /** + * @return array{ + * contest: Contest, + * problems: ContestProblem[], + * filters: array, + * view: string, + * languages: array, + * team_count: int, + * solved: int, + * not_solved: int, + * total: int, + * problems_solved: array, + * problems_solved_count: int, + * problems_attempted: array, + * problems_attempted_count: int, + * }> + * } + */ + public function getLanguagesStats(Contest $contest, string $view): array + { + /** @var Language[] $languages */ + $languages = $this->em->getRepository(Language::class) + ->createQueryBuilder('l') + ->andWhere('l.allowSubmit = 1') + ->orderBy('l.name') + ->getQuery() + ->getResult(); + + $languageStats = []; + + foreach ($languages as $language) { + $languageStats[$language->getLangid()] = [ + 'name' => $language->getName(), + 'teams' => [], + 'team_count' => 0, + 'solved' => 0, + 'not_solved' => 0, + 'total' => 0, + 'problems_solved' => [], + 'problems_solved_count' => 0, + 'problems_attempted' => [], + 'problems_attempted_count' => 0, + ]; + } + + $teams = $this->getTeams($contest, $view); + foreach ($teams as $team) { + foreach ($team->getSubmissions() as $s) { + if ($s->getContest() != $contest) { + continue; + } + if ($s->getSubmitTime() > $contest->getEndTime()) { + continue; + } + if ($s->getSubmitTime() < $contest->getStartTime()) { + continue; + } + if ($s->getSubmittime() > $contest->getFreezetime()) { + continue; + } + + $language = $s->getLanguage(); + + if (!isset($languageStats[$language->getLangid()]['teams'][$team->getTeamid()])) { + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()] = [ + 'team' => $team, + 'solved' => 0, + 'total' => 0, + ]; + } + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()]['total']++; + $languageStats[$language->getLangid()]['total']++; + if ($s->getResult() === 'correct') { + $languageStats[$language->getLangid()]['solved']++; + $languageStats[$language->getLangid()]['teams'][$team->getTeamid()]['solved']++; + $languageStats[$language->getLangid()]['problems_solved'][$s->getProblem()->getProbId()] = $s->getContestProblem(); + } else { + $languageStats[$language->getLangid()]['not_solved']++; + } + $languageStats[$language->getLangid()]['problems_attempted'][$s->getProblem()->getProbId()] = $s->getContestProblem(); + } + } + + foreach ($languageStats as &$languageStat) { + usort($languageStat['teams'], static function (array $a, array $b): int { + if ($a['solved'] === $b['solved']) { + return $b['total'] <=> $a['total']; + } + + return $b['solved'] <=> $a['solved']; + }); + $languageStat['team_count'] = count($languageStat['teams']); + $languageStat['problems_solved_count'] = count($languageStat['problems_solved']); + $languageStat['problems_attempted_count'] = count($languageStat['problems_attempted']); + } + unset($languageStat); + + return [ + 'contest' => $contest, + 'problems' => $this->getContestProblems($contest), + 'filters' => StatisticsService::FILTERS, + 'view' => $view, + 'languages' => $languageStats, + ]; + } + /** * Apply the filter to the given query builder. */ diff --git a/webapp/src/Service/SubmissionService.php b/webapp/src/Service/SubmissionService.php index 43453b3e1a..bb7ff306d4 100644 --- a/webapp/src/Service/SubmissionService.php +++ b/webapp/src/Service/SubmissionService.php @@ -235,8 +235,7 @@ public function getSubmissionList( } } - if ($this->config->get('data_source') == - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL) { + if ($this->dj->shadowMode()) { // When we are shadow, also load the external results $queryBuilder ->leftJoin('s.external_judgements', 'ej', Join::WITH, 'ej.valid = 1') @@ -556,7 +555,9 @@ public function submitSolution( // First look up any expected results in all submission files to minimize the // SQL transaction time below. - if ($this->dj->checkrole('jury')) { + // Only do this for problem import submissions, as we do not want this for re-submitted submissions nor + // submissions that come through the API, e.g. when doing a replay of an old contest. + if ($this->dj->checkrole('jury') && $source == 'problem import') { $results = null; foreach ($files as $file) { $fileResult = self::getExpectedResults(file_get_contents($file->getRealPath()), @@ -613,6 +614,7 @@ public function submitSolution( $judging ->setContest($contest) ->setSubmission($submission); + $submission->addJudging($judging); if ($juryMember !== null) { $judging->setJuryMember($juryMember); } diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php index 9ddbfaddb4..d61ccedcce 100644 --- a/webapp/src/Twig/TwigExtension.php +++ b/webapp/src/Twig/TwigExtension.php @@ -8,6 +8,7 @@ use App\Entity\ExternalJudgement; use App\Entity\ExternalRun; use App\Entity\ExternalSourceWarning; +use App\Entity\HasExternalIdInterface; use App\Entity\Judging; use App\Entity\JudgingRun; use App\Entity\Language; @@ -22,6 +23,7 @@ use App\Service\DOMJudgeService; use App\Service\EventLogService; use App\Service\SubmissionService; +use App\Utils\Scoreboard\ScoreboardMatrixItem; use App\Utils\Utils; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\EntityManagerInterface; @@ -59,9 +61,9 @@ public function getFunctions(): array return [ new TwigFunction('button', $this->button(...), ['is_safe' => ['html']]), new TwigFunction('calculatePenaltyTime', $this->calculatePenaltyTime(...)), - new TwigFunction('showExternalId', $this->showExternalId(...)), new TwigFunction('customAssetFiles', $this->customAssetFiles(...)), new TwigFunction('globalBannerAssetPath', $this->dj->globalBannerAssetPath(...)), + new TwigFunction('shadowMode', $this->shadowMode(...)), ]; } @@ -108,7 +110,8 @@ 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('problemBadgeMaybe', $this->problemBadgeMaybe(...), ['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']]), @@ -153,7 +156,7 @@ public function getGlobals(): array ), 'show_shadow_differences' => $this->tokenStorage->getToken() && $this->authorizationChecker->isGranted('ROLE_ADMIN') && - $this->config->get('data_source') === DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL, + $this->dj->shadowMode(), 'doc_links' => $this->dj->getDocLinks(), 'allow_registration' => $selfRegistrationCategoriesCount !== 0, 'enable_ranking' => $this->config->get('enable_ranking'), @@ -215,26 +218,31 @@ public function printtime(string|float|null $datetime, ?string $format = null, ? } } - public function printHumanTimeDiff(float|null $datetime): string + public function printHumanTimeDiff(float|null $startTime = null, float|null $endTime = null): string { - if ($datetime === null) { + if ($startTime === null) { return ''; } - $diff = Utils::now() - $datetime; + $suffix = ''; + if ($endTime === null) { + $suffix = ' ago'; + $endTime = Utils::now(); + } + $diff = $endTime - $startTime; if ($diff < 120) { - return (int)($diff) . ' seconds ago'; + return (int)($diff) . ' seconds' . $suffix; } $diff /= 60; if ($diff < 120) { - return (int)($diff) . ' minutes ago'; + return (int)($diff) . ' minutes' . $suffix; } $diff /= 60; if ($diff < 48) { - return (int)($diff) . ' hours ago'; + return (int)($diff) . ' hours' . $suffix; } $diff /= 24; - return (int)($diff) . ' days ago'; + return (int)($diff) . ' days' . $suffix; } /** @@ -369,7 +377,7 @@ public function testcaseResults(Submission $submission, ?bool $showExternal = fa $externalJudgementId = $externalJudgement?->getExtjudgementid(); $probId = $submission->getProblem()->getProbid(); $testcases = $this->em->getConnection()->fetchAllAssociative( - 'SELECT er.result as runresult, t.ranknumber, t.description + 'SELECT er.result as runresult, t.ranknumber, t.description, t.sample FROM testcase t LEFT JOIN external_run er ON (er.testcaseid = t.testcaseid AND er.extjudgementid = :extjudgementid) @@ -383,7 +391,7 @@ public function testcaseResults(Submission $submission, ?bool $showExternal = fa $judgingId = $judging ? $judging->getJudgingid() : null; $probId = $submission->getProblem()->getProbid(); $testcases = $this->em->getConnection()->fetchAllAssociative( - 'SELECT r.runresult, jh.hostname, jt.valid, t.ranknumber, t.description + 'SELECT r.runresult, jh.hostname, jt.valid, t.ranknumber, t.description, t.sample FROM testcase t LEFT JOIN judging_run r ON (r.testcaseid = t.testcaseid AND r.judgingid = :judgingid) @@ -396,7 +404,12 @@ public function testcaseResults(Submission $submission, ?bool $showExternal = fa } $results = ''; + $lastTypeSample = true; foreach ($testcases as $key => $testcase) { + if ($testcase['sample'] != $lastTypeSample) { + $results .= ' | '; + $lastTypeSample = $testcase['sample']; + } $class = $submissionDone ? 'secondary' : 'primary'; $text = '?'; @@ -575,16 +588,9 @@ public function externalCcsUrl(Submission $submission): ?string { $extCcsUrl = $this->config->get('external_ccs_submission_url'); if (!empty($extCcsUrl)) { - $dataSource = $this->config->get('data_source'); - if ($dataSource == 2 && $submission->getExternalid()) { - return str_replace(['[contest]', '[id]'], - [$submission->getContest()->getExternalid(), $submission->getExternalid()], - $extCcsUrl); - } elseif ($dataSource == 1) { - return str_replace(['[contest]', '[id]'], - [$submission->getContest()->getExternalid(), $submission->getSubmitid()], - $extCcsUrl); - } + return str_replace(['[contest]', '[id]'], + [$submission->getContest()->getExternalid(), $submission->getExternalid()], + $extCcsUrl); } return null; @@ -662,6 +668,7 @@ private function getCommonPrefix(array $strings): string */ public function printHosts(array $hostnames): string { + $hostnames = array_values($hostnames); if (empty($hostnames)) { return ""; } @@ -1002,12 +1009,9 @@ public function descriptionExpand(?string $description = null): string } } - /** - * @param object|string $entity - */ - public function showExternalId($entity): bool + public function shadowMode(): bool { - return $this->eventLogService->externalIdFieldForEntity($entity) !== null; + return $this->dj->shadowMode(); } public function wrapUnquoted(string $text, int $width = 75, string $quote = '>'): string @@ -1021,8 +1025,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; } @@ -1059,9 +1063,12 @@ public function fileTypeIcon(string $type): string return 'fas fa-file-' . $iconName; } - public function problemBadge(ContestProblem $problem): string + public function problemBadge(ContestProblem $problem, bool $grayedOut = false): string { $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); + if ($grayedOut) { + $rgb = 'whitesmoke'; + } $background = Utils::parseHexColor($rgb); // Pick a border that's a bit darker. @@ -1073,8 +1080,12 @@ public function problemBadge(ContestProblem $problem): string // Pick the foreground text color based on the background color. $foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff'; + if ($grayedOut) { + $foreground = 'silver'; + $border = 'linen'; + } return sprintf( - '%s', + '%s', $rgb, $border, $foreground, @@ -1082,14 +1093,50 @@ public function problemBadge(ContestProblem $problem): string ); } - public function problemBadgeForProblemAndContest(Problem $problem, ?Contest $contest): string + public function problemBadgeMaybe(ContestProblem $problem, ScoreboardMatrixItem $matrixItem): string { - foreach ($problem->getContestProblems() as $contestProblem) { - if ($contestProblem->getContest() === $contest) { - return $this->problemBadge($contestProblem); + $rgb = Utils::convertToHex($problem->getColor() ?? '#ffffff'); + if (!$matrixItem->isCorrect) { + $rgb = 'whitesmoke'; + } + $background = Utils::parseHexColor($rgb); + + // Pick a border that's a bit darker. + $darker = $background; + $darker[0] = max($darker[0] - 64, 0); + $darker[1] = max($darker[1] - 64, 0); + $darker[2] = max($darker[2] - 64, 0); + $border = Utils::rgbToHex($darker); + + // Pick the foreground text color based on the background color. + $foreground = ($background[0] + $background[1] + $background[2] > 450) ? '#000000' : '#ffffff'; + if (!$matrixItem->isCorrect) { + $foreground = 'silver'; + $border = 'linen'; + } + + $ret = sprintf( + '%s', + $rgb, + $border, + $foreground, + $problem->getShortname() + ); + if (!$matrixItem->isCorrect) { + if ($matrixItem->numSubmissionsPending > 0) { + $ret = '' . $ret . ''; + } else if ($matrixItem->numSubmissions > 0) { + $ret = '' . $ret . ''; } } - return ''; + return $ret; + } + + public function problemBadgeForContest(Problem $problem, ?Contest $contest = null): string + { + $contest ??= $this->dj->getCurrentContest(); + $contestProblem = $contest?->getContestProblem($problem); + return $contestProblem === null ? '' : $this->problemBadge($contestProblem); } public function printMetadata(?string $metadata): string @@ -1184,14 +1231,17 @@ public function entityIdBadge(BaseApiEntity $entity, string $idPrefix = ''): str $propertyAccessor = PropertyAccess::createPropertyAccessor(); $metadata = $this->em->getClassMetadata($entity::class); $primaryKeyColumn = $metadata->getIdentifierColumnNames()[0]; - $externalIdField = $this->eventLogService->externalIdFieldForEntity($entity); $data = [ 'idPrefix' => $idPrefix, 'id' => $propertyAccessor->getValue($entity, $primaryKeyColumn), - 'externalId' => $externalIdField ? $propertyAccessor->getValue($entity, $externalIdField) : null, + 'externalId' => null, ]; + if ($entity instanceof HasExternalIdInterface) { + $data['externalId'] = $entity->getExternalid(); + } + if ($entity instanceof Team) { $data['label'] = $entity->getLabel(); } diff --git a/webapp/src/Utils/Scoreboard/Scoreboard.php b/webapp/src/Utils/Scoreboard/Scoreboard.php index bb81e6fd84..6e92330961 100644 --- a/webapp/src/Utils/Scoreboard/Scoreboard.php +++ b/webapp/src/Utils/Scoreboard/Scoreboard.php @@ -48,6 +48,14 @@ public function __construct( $this->calculateScoreboard(); } + /** + * @return bool Whether this Scoreboard has restricted access (either a jury member can see, or after unfreeze). + */ + public function hasRestrictedAccess(): bool + { + return $this->restricted; + } + /** * @return Team[] */ diff --git a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php index 76aae33bd2..3267322be7 100644 --- a/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php +++ b/webapp/src/Utils/Scoreboard/SingleTeamScoreboard.php @@ -65,8 +65,9 @@ protected function calculateScoreboard(): void $this->matrix[$scoreRow->getTeam()->getTeamid()][$scoreRow->getProblem()->getProbid()] = new ScoreboardMatrixItem( $scoreRow->getIsCorrect($this->restricted), $scoreRow->getIsCorrect($this->showRestrictedFts) && $scoreRow->getIsFirstToSolve(), - $scoreRow->getSubmissions($this->restricted), - $scoreRow->getPending($this->restricted), + // When public scoreboard is frozen, also show "x + y tries" for jury + $scoreRow->getSubmissions($this->freezeData->showFrozen() ? false : $this->restricted), + $scoreRow->getPending($this->freezeData->showFrozen() ? false : $this->restricted), $scoreRow->getSolveTime($this->restricted), $penalty, $scoreRow->getRuntime($this->restricted) diff --git a/webapp/src/Utils/Utils.php b/webapp/src/Utils/Utils.php index 0c7033f1af..c1ec4acb9a 100644 --- a/webapp/src/Utils/Utils.php +++ b/webapp/src/Utils/Utils.php @@ -441,7 +441,7 @@ public static function printsize(int $size, int $decimals = 1): string $exact = true; for ($i = 0; $i < count($units) && $display >= $factor; $i++) { - if (($display % $factor)!=0) { + if (((int)$display % $factor)!=0) { $exact = false; } $display /= $factor; diff --git a/symfony.lock b/webapp/symfony.lock similarity index 98% rename from symfony.lock rename to webapp/symfony.lock index 3f00c08f96..77c6511b57 100644 --- a/symfony.lock +++ b/webapp/symfony.lock @@ -26,16 +26,6 @@ "dflydev/dot-access-data": { "version": "v3.0.1" }, - "doctrine/annotations": { - "version": "2.0", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "main", - "version": "1.10", - "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" - }, - "files": [] - }, "doctrine/cache": { "version": "v1.8.0" }, @@ -208,6 +198,18 @@ "webapp/config/routes/nelmio_api_doc.yaml" ] }, + "nelmio/cors-bundle": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "webapp/config/packages/nelmio_cors.yaml" + ] + }, "nette/schema": { "version": "v1.2.2" }, diff --git a/webapp/templates/jury/analysis/contest_overview.html.twig b/webapp/templates/jury/analysis/contest_overview.html.twig index 3aeb6de59c..9a860e4ef1 100644 --- a/webapp/templates/jury/analysis/contest_overview.html.twig +++ b/webapp/templates/jury/analysis/contest_overview.html.twig @@ -24,6 +24,14 @@ table tr a { color: inherit; } + {% endblock %} {% block content %} @@ -36,7 +44,7 @@ table tr a { Contest Stats
-
Language Stats + + + Details +
diff --git a/webapp/templates/jury/analysis/languages.html.twig b/webapp/templates/jury/analysis/languages.html.twig new file mode 100644 index 0000000000..7cb29236af --- /dev/null +++ b/webapp/templates/jury/analysis/languages.html.twig @@ -0,0 +1,100 @@ +{% extends "jury/base.html.twig" %} + +{% block title %}Analysis - Languages in {{ current_contest.shortname | default('') }} - {{ parent() }}{% endblock %} + +{% block content %} +

Language stats

+ {% include 'jury/partials/analysis_filter.html.twig' %} + +
+ {% for langid, language in languages %} +
+
+
+ {{ language.name }} +
+
+ {{ language.team_count }} team{% if language.team_count != 1 %}s{% endif %} + {% if language.team_count > 0 %} +
+ + +
+
+
+ + + + + + + + + + {% for team in language.teams %} + + + + + + + {% endfor %} + +
TeamNumber of solved problems in {{ language.name }}Total attempts in {{ language.name }}
+ + {{ team.team | entityIdBadge('t') }} + + + + {{ team.team.effectiveName }} + + {{ team.solved }}{{ team.total }}
+
+
+ {% endif %} +
+ {{ language.total }} total submission{% if language.total != 1 %}s{% endif %} + for {{ language.problems_attempted_count }} problem{% if language.problems_attempted_count != 1 %}s{% endif %}:
+ {% for problem in problems %} + + {{ problem | problemBadge(language.problems_attempted[problem.probid] is not defined) }} + + {% endfor %} +
+ {{ language.solved }} submission{% if language.solved != 1 %}s{% endif %} solved problems + for {{ language.problems_solved_count }} problem{% if language.problems_solved_count != 1 %}s{% endif %}:
+ {% for problem in problems %} + + {{ problem | problemBadge(language.problems_solved[problem.probid] is not defined) }} + + {% endfor %} +
+ {{ language.not_solved }} submission{% if language.not_solved != 1 %}s{% endif %} did not solve a problem
+
+
+
+ {% endfor %} +
+{% endblock %} + +{% block extrafooter %} + +{% endblock %} diff --git a/webapp/templates/jury/base.html.twig b/webapp/templates/jury/base.html.twig index 10a61dfc31..a2e724d511 100644 --- a/webapp/templates/jury/base.html.twig +++ b/webapp/templates/jury/base.html.twig @@ -34,10 +34,54 @@ } } + $('#keys_disable').click(disableKeys); + $('#keys_enable').click(enableKeys); + var keysCookie = getCookie('domjudge_keys'); + if (keysCookie != 1 && keysCookie != "") { + $('#keys_enable').removeClass('d-none'); + } else { + $('#keys_disable').removeClass('d-none'); + } + updateMenuAlerts(); setInterval(updateMenuAlerts, 20000); $('[data-bs-toggle="tooltip"]').tooltip(); }); + + initializeKeyboardShortcuts(); + +
+

Keyboard shortcuts

+ + ? display this help, Escape to exit
+
+ + j go to the next item, e.g. next submission
+ k go to the previous item, e.g. previous submission
+
+ + s ↵ open the list of submissions
+ s [0-9]+ ↵ open a specific submission, e.g. s42↵ to go to submission 42
+
+ + t ↵ open the list of teams
+ t [0-9]+ ↵ open to a specific team
+
+ + p ↵ open the list of problems
+ p [0-9]+ ↵ open a specific problem
+
+ + c ↵ open the list of clarifications
+ c [0-9]+ ↵ open a specific clarification
+
+ + Shift + j [0-9]+ ↵ open a specific judging
+
+ + Shift + s open the scoreboard
+
+
{% endblock %} diff --git a/webapp/templates/jury/clarification.html.twig b/webapp/templates/jury/clarification.html.twig index 72feccb3fd..c1c50d768b 100644 --- a/webapp/templates/jury/clarification.html.twig +++ b/webapp/templates/jury/clarification.html.twig @@ -24,7 +24,7 @@
Clarification {{ clar.clarid }} - {% if showExternalId %} + {% if shadowMode() %} (external ID: {{ clar.externalid }}) {% endif %}
diff --git a/webapp/templates/jury/clarifications.html.twig b/webapp/templates/jury/clarifications.html.twig index 75d4414fd2..411917f668 100644 --- a/webapp/templates/jury/clarifications.html.twig +++ b/webapp/templates/jury/clarifications.html.twig @@ -23,9 +23,12 @@ {%- else %}
diff --git a/webapp/templates/jury/contest.html.twig b/webapp/templates/jury/contest.html.twig index a2c62333a3..a62eb9c3f1 100644 --- a/webapp/templates/jury/contest.html.twig +++ b/webapp/templates/jury/contest.html.twig @@ -40,75 +40,53 @@ CID c{{ contest.cid }} + - {% if showExternalId(contest) %} - - External ID - {{ contest.externalid }} - - {% endif %} - Short name - {{ contest.shortname }} + External ID + {{ contest.externalid }} + - Activate time - - {{ contest.activatetimeString }} - {% if contest.isActive %} - - {% endif %} - + Short name + {{ contest.shortname }} + - - Start time - - {% if contest.starttimeEnabled %} - {{ contest.starttimeString }} - {% if contest.state.started %} - + {% for type, data in contest.dataForJuryInterface %} + + {{ data.label }}: + + {{ data.time }} + {% if data.icon is defined %} + {% endif %} + + {% if is_granted('ROLE_ADMIN') %} + + {% if data.show_button %} + {% set button_label = type ~ " now" %} + {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': type}), button_label, 'primary btn-sm timebutton') }} + {% endif %} + {% if data.extra_button is defined %} + {{ button(path('jury_contest_donow', {'contestId': contest.cid, 'time': data.extra_button.type}), data.extra_button.label, 'primary btn-sm timebutton') }} + {% endif %} + {% if type == 'finalize' %} + {% if contest.finalizetime %} + {{ button(path('jury_contest_finalize', {'contestId': contest.cid}), 'Update finalization', 'secondary btn-sm timebutton') }} + {% endif %} + {% endif %} + {% else %} - {{ contest.starttimeString }} delayed + {% endif %} - - - - Scoreboard freeze - - {{ contest.freezetimeString|default('-') }} - {% if contest.state.frozen %} - - {% endif %} - - - - End time - - {{ contest.endtimeString }} - {% if contest.state.ended %} - - {% endif %} - - - - Scoreboard unfreeze - - {{ contest.unfreezetimeString|default('-') }} - {% if contest.state.thawed %} - - {% endif %} - - - - Deactivate time - {{ contest.deactivatetimeString }} - + + {% endfor %} Allow submit {% include 'jury/partials/contest_toggle.html.twig' with {type: 'submit', enabled: contest.allowSubmit} %} + {% if contest.contestProblemsetType is not empty %} @@ -119,6 +97,7 @@ class="fas fa-file-{{ contest.contestProblemsetType }}"> + {% endif %} @@ -126,6 +105,7 @@ {% include 'jury/partials/contest_toggle.html.twig' with {type: 'balloons', enabled: contest.processBalloons} %} + Runtime as tiebreaker @@ -138,6 +118,7 @@ {% include 'jury/partials/contest_toggle.html.twig' with {type: 'medals', enabled: contest.medalsEnabled} %} + Medals @@ -175,16 +156,19 @@ none {% endif %} + Publicly visible {% include 'jury/partials/contest_toggle.html.twig' with {type: 'public', enabled: contest.public} %} + Open to all teams {{ contest.openToAllTeams | printYesNo }} + Teams @@ -207,6 +191,7 @@ {% endfor %} {% endif %} + Public static scoreboard ZIP @@ -215,6 +200,7 @@ Download + Jury (unfrozen) static scoreboard ZIP @@ -223,6 +209,7 @@ Download + Sample data ZIP @@ -235,11 +222,9 @@ Contains samples, attachments and statement for all problems. + - {% set contestId = contest.cid %} - {% if showExternalId(contest) %} - {% set contestId = contest.externalid %} - {% endif %} + {% set contestId = contest.externalid %} {% set banner = contestId | assetPath('contest') %} {% if not banner %} {% set banner = globalBannerAssetPath() %} @@ -248,10 +233,13 @@ Banner + + {% endif %} Warning message {{ contest.warningMessage }} +
@@ -451,11 +439,6 @@ {{ button(path('jury_contest_edit', {'contestId': contest.cid}), 'Edit', 'primary', 'edit') }} {{ button(path('jury_contest_delete', {'contestId': contest.cid}), 'Delete', 'danger', 'trash-alt', true) }} {{ button(path('jury_contest_lock', {'contestId': contest.cid}), 'Lock', 'secondary', 'lock') }} - {% if contest.finalizetime %} - {{ button(path('jury_contest_finalize', {'contestId': contest.cid}), 'Update finalization', 'secondary', 'lock') }} - {% else %} - {{ button(path('jury_contest_finalize', {'contestId': contest.cid}), 'Finalize this contest', 'secondary', 'flag-checkered') }} - {% endif %} {% endif %} {{ button(path('jury_contest_request_remaining', {'contestId': contest.cid}), 'Judge remaining testcases', 'secondary', 'gavel') }} {% endif %} diff --git a/webapp/templates/jury/entity_id_badge.html.twig b/webapp/templates/jury/entity_id_badge.html.twig index 037714630a..8e304dc748 100644 --- a/webapp/templates/jury/entity_id_badge.html.twig +++ b/webapp/templates/jury/entity_id_badge.html.twig @@ -1,4 +1,4 @@ - + {% if label is defined and label | length %} {{ label }} {% elseif externalId is not null %} diff --git a/webapp/templates/jury/executable.html.twig b/webapp/templates/jury/executable.html.twig index 95cbe3956e..94569d1e4e 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 %} @@ -135,8 +135,8 @@ {{ form_widget(form) }}
- {{ form_end(form) }} {% endif %} + {{ form_end(form) }} {% if is_granted('ROLE_ADMIN') %}
diff --git a/webapp/templates/jury/export/results.html.twig b/webapp/templates/jury/export/results.html.twig index 9f3d10db52..3e359ad0b6 100644 --- a/webapp/templates/jury/export/results.html.twig +++ b/webapp/templates/jury/export/results.html.twig @@ -10,7 +10,7 @@ Award Solved problems Total time - Time of last submission + Time of last accepted submission @@ -27,25 +27,35 @@ -

Other ranked teams

- - - - - - - - - - {% for row in ranked %} - - - - - - {% endfor %} - -
RankTeamSolved problems
{{ row.rank }}{{ row.team }}{{ row.solved }}
+ {% for award in ['Ranked', 'Highest Honors', 'High Honors', 'Honors'] %} + {% if ranked[award] is defined %} +

+ {% if award == 'Ranked' %} + Other ranked teams + {% else %} + {{ award }} + {% endif %} +

+ + + + + + + + + + {% for row in ranked[award] %} + + + + + + {% endfor %} + +
RankTeamSolved problems
{{ row.rank }}{{ row.team }}{{ row.solved }}
+ {% endif %} + {% endfor %}

Honorable mentions

@@ -66,6 +76,7 @@ + @@ -73,6 +84,7 @@ + {% endfor %} @@ -84,6 +96,7 @@ + @@ -98,6 +111,13 @@ Not solved {% endif %} + - {% if showExternalId(language) %} - - - - - {% endif %} + + + + - {% if showExternalId %} + {% if shadowMode() %} {% endif %} {%- if current_contest is null and current_contests | length > 1 %} @@ -28,7 +28,7 @@ - {% if showExternalId %} + {% if shadowMode() %} {% endif %} {%- if current_contest is null and current_contests | length > 1 %} @@ -71,7 +71,7 @@ - {% if showExternalId(problem) %} - - - - - {% endif %} + + + + - {% if showExternalId(team) %} - - - - - {% endif %} + + + + - {% if showExternalId(teamAffiliation) %} - - - - - {% endif %} + + + + - {% if showExternalId(teamCategory) %} - - - - - {% endif %} + + + + - {% if showExternalId(user) %} - - - - - {% endif %} + + + + diff --git a/webapp/templates/jury/versions.html.twig b/webapp/templates/jury/versions.html.twig index 0024aa6290..e0171db627 100644 --- a/webapp/templates/jury/versions.html.twig +++ b/webapp/templates/jury/versions.html.twig @@ -15,7 +15,14 @@ {% for lang in data %}
-
Language {{ lang.language.langid }}
+
+ Language {{ lang.language.langid }} + {% if is_granted('ROLE_ADMIN') %} + + {{ button(path('jury_language_edit', {'langId': lang.language.langid}), 'Edit version command(s)', 'primary btn-sm', 'edit') }} + + {% endif %} +
diff --git a/webapp/templates/partials/problem_list.html.twig b/webapp/templates/partials/problem_list.html.twig index f00ef14482..bda100c09d 100644 --- a/webapp/templates/partials/problem_list.html.twig +++ b/webapp/templates/partials/problem_list.html.twig @@ -92,8 +92,8 @@
{% endfor %} diff --git a/webapp/templates/partials/scoreboard.html.twig b/webapp/templates/partials/scoreboard.html.twig index 96a9c9e939..4fb8a18bfb 100644 --- a/webapp/templates/partials/scoreboard.html.twig +++ b/webapp/templates/partials/scoreboard.html.twig @@ -18,31 +18,37 @@ {% endif %}
-
- {{ current_contest.name }} - - {% if scoreboard is null %} - {{ current_contest | printContestStart }} - {% elseif scoreboard.freezeData.showFinal(jury) %} - {% if current_contest.finalizetime is empty %} - preliminary results - not final - {% else %} - final standings - {% endif %} - {% elseif scoreboard.freezeData.stopped %} - contest over, waiting for results - {% elseif static %} - {% set now = 'now'|date('U') %} - {{ current_contest.starttime | printelapsedminutes(now) }} - {% else %} - {% if current_contest.freezeData.started %} - started: +
+
+
+ {{ current_contest.name }} +
+
+ + {% if scoreboard is null %} + {{ current_contest | printContestStart }} + {% elseif scoreboard.freezeData.showFinal(jury) %} + {% if current_contest.finalizetime is empty %} + preliminary results - not final + {% else %} + final standings + {% endif %} + {% elseif scoreboard.freezeData.stopped %} + contest over, waiting for results + {% elseif static %} + {% set now = 'now'|date('U') %} + {{ current_contest.starttime | printelapsedminutes(now) }} {% else %} - starts: + {% if current_contest.freezeData.started %} + started: + {% else %} + starts: + {% endif %} + {{ current_contest.starttime | printtime }} - ends: {{ current_contest.endtime | printtime }} {% endif %} - {{ current_contest.starttime | printtime }} - ends: {{ current_contest.endtime | printtime }} - {% endif %} - + +
+
{% if static %} @@ -93,11 +99,12 @@
{% endif %} - {% if scoreboard.freezeData.showFrozen(false) %} + {% if scoreboard.freezeData.showFrozen %}
Region TeamRank
{{ row.group }} {{ row.team }}{{ row.rank }}
Problem TeamRank Time
+ {% if row.rank is not null %} + {{ row.rank }} + {% else %} + - + {% endif %} + {% if row.time is not null %} {{ row.time }} diff --git a/webapp/templates/jury/import_export.html.twig b/webapp/templates/jury/import_export.html.twig index d7be0d7846..0cd8971f40 100644 --- a/webapp/templates/jury/import_export.html.twig +++ b/webapp/templates/jury/import_export.html.twig @@ -88,74 +88,50 @@

Results

-
-
-
+
-

Export <html>

- +

Export clarifications

+
-
-
-
+
-

Export tab-separated

-
    -
  • - wf_results.tsv: - -
  • -
  • - full_results.tsv: - -
  • -
-
+

Export results

+ {{ form(export_results_form) }} +
{% endblock %} +{% block extrafooter %} + {{ parent() }} + +{% endblock %} diff --git a/webapp/templates/jury/language.html.twig b/webapp/templates/jury/language.html.twig index f56609fc76..6991e61b8f 100644 --- a/webapp/templates/jury/language.html.twig +++ b/webapp/templates/jury/language.html.twig @@ -20,12 +20,10 @@
ID {{ language.langid }}
External ID{{ language.externalid }}
External ID{{ language.externalid }}
Entry point diff --git a/webapp/templates/jury/menu.html.twig b/webapp/templates/jury/menu.html.twig index 29cc607a8d..bf46240ec6 100644 --- a/webapp/templates/jury/menu.html.twig +++ b/webapp/templates/jury/menu.html.twig @@ -116,19 +116,25 @@ {% if refresh is defined and refresh %} - - {% if refresh_flag %} - Disable Refresh - {% else %} - Enable Refresh - {% endif %} - - {% if refresh %} - ({{ refresh.after }}s) - {% endif %} + + + {% if refresh_flag %} + Disable Refresh + {% else %} + Enable Refresh + {% endif %} + + ({{ refresh.after }}s) {% endif %} + + Disable keyboard shortcuts + + + Enable keyboard shortcuts + + Logout diff --git a/webapp/templates/jury/partials/clarification_list.html.twig b/webapp/templates/jury/partials/clarification_list.html.twig index 337499bb1c..33ffcbcf3e 100644 --- a/webapp/templates/jury/partials/clarification_list.html.twig +++ b/webapp/templates/jury/partials/clarification_list.html.twig @@ -3,7 +3,7 @@
IDexternal ID
{{ clarification.clarid }}{{ clarification.externalid }} {%- 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/jury/partials/contest_form.html.twig b/webapp/templates/jury/partials/contest_form.html.twig index 04ed8018be..9e30edaff0 100644 --- a/webapp/templates/jury/partials/contest_form.html.twig +++ b/webapp/templates/jury/partials/contest_form.html.twig @@ -5,9 +5,7 @@ {# These are the errors related to removed intervals #} {{ form_errors(form) }} - {% if form.offsetExists('externalid') %} - {{ form_row(form.externalid) }} - {% endif %} + {{ form_row(form.externalid) }} {{ form_row(form.shortname) }} {{ form_row(form.name) }} {{ form_row(form.activatetimeString) }} diff --git a/webapp/templates/jury/partials/submission_graph.html.twig b/webapp/templates/jury/partials/submission_graph.html.twig index 6a74534f57..d33220e2a9 100644 --- a/webapp/templates/jury/partials/submission_graph.html.twig +++ b/webapp/templates/jury/partials/submission_graph.html.twig @@ -1,21 +1,51 @@ {% set timelimit = submission.problem.timelimit * submission.language.timeFactor %}
{% if selectedJudging is not null %} -
-

Testcase Runtimes

+
+
+ testcase CPU times + + {%- if selectedJudging.result != 'compiler-error' -%} + | max: + {{ selectedJudging.maxRuntime | number_format(3, '.', '') }}s + | sum: {{ selectedJudging.sumRuntime | number_format(3, '.', '') }}s + {% endif %} + + +
+
+
{% endif %} {% if externalJudgement is not null and externalJudgement.result is not null %} -
-

External Testcase Runtimes

- +
+
+ External Testcase CPU times + + {%- if externalJudging.result != 'compiler-error' -%} + | max: + {{ externalJudging.maxRuntime | number_format(3, '.', '') }}s + | sum: {{ externalJudging.sumRuntime | number_format(3, '.', '') }}s + {% endif %} + + +
+
+ +
{% endif %} {% if judgings|length > 1 %} -
-

Max Runtimes

- +
+
+
+ Max. CPU times +
+
+ +
+
{% endif %}
@@ -59,7 +89,7 @@ } chart.yAxis .tickValues(tickValues) - .axisLabel('Runtime'); + .axisLabel('CPU time'); if (tickStep >= 1) { chart.yAxis.tickFormat(function(d) { return d3.format(',f')(d) + 's' }); } else { @@ -145,7 +175,7 @@ var format = d3.format(".3f"); return "Testcase " + obj.data.description + "
Runtime: " + format(obj.data.value) + "s"; }); - chart.xAxis.axisLabel("Testcase Rank"); + chart.xAxis.axisLabel("testcase rank"); d3.select('#testcaseruntime svg') .datum(testcase_times) .call(chart); diff --git a/webapp/templates/jury/partials/submission_list.html.twig b/webapp/templates/jury/partials/submission_list.html.twig index 522d2b5552..b25e24fa1e 100644 --- a/webapp/templates/jury/partials/submission_list.html.twig +++ b/webapp/templates/jury/partials/submission_list.html.twig @@ -107,7 +107,7 @@
s{{ submission.submitid }} - {% if submission.externalid %} + {% if shadowMode() and submission.externalid %} ({{ submission.externalid }}) {% endif %} diff --git a/webapp/templates/jury/partials/team_category_form.html.twig b/webapp/templates/jury/partials/team_category_form.html.twig index d0b8e67d0a..b20e6dd7ea 100644 --- a/webapp/templates/jury/partials/team_category_form.html.twig +++ b/webapp/templates/jury/partials/team_category_form.html.twig @@ -1,9 +1,7 @@
{{ form_start(form) }} - {% if form.offsetExists('externalid') %} - {{ form_row(form.externalid) }} - {% endif %} + {{ form_row(form.externalid) }} {{ form_row(form.icpcid) }} {{ form_row(form.name) }} {{ form_row(form.sortorder) }} diff --git a/webapp/templates/jury/partials/team_form.html.twig b/webapp/templates/jury/partials/team_form.html.twig index 0ad3c08775..81d3f9e508 100644 --- a/webapp/templates/jury/partials/team_form.html.twig +++ b/webapp/templates/jury/partials/team_form.html.twig @@ -1,9 +1,7 @@
{{ form_start(form) }} - {% if form.offsetExists('externalid') %} - {{ form_row(form.externalid) }} - {% endif %} + {{ form_row(form.externalid) }} {{ form_row(form.icpcid) }} {{ form_row(form.label) }} {{ form_row(form.name) }} diff --git a/webapp/templates/jury/problem.html.twig b/webapp/templates/jury/problem.html.twig index 40d7311b57..380054dbf3 100644 --- a/webapp/templates/jury/problem.html.twig +++ b/webapp/templates/jury/problem.html.twig @@ -20,12 +20,10 @@
ID p{{ problem.probid }}
External ID{{ problem.externalid }}
External ID{{ problem.externalid }}
Testcases diff --git a/webapp/templates/jury/rejudging.html.twig b/webapp/templates/jury/rejudging.html.twig index 34aea20c5a..2753198c8d 100644 --- a/webapp/templates/jury/rejudging.html.twig +++ b/webapp/templates/jury/rejudging.html.twig @@ -105,6 +105,27 @@
Judgings in this rejudging will be applied automatically.
{% endif %} + {% if disabledLangs %} +
+ The following languages are currently not allowed to be judged: +
    + {% for id, name in disabledLangs %} +
  • {{ name }}
  • + {% endfor %} +
+
+ {% endif %} + {% if disabledProbs %} +
+ The following problems are currently not allowed to be judged: +
    + {% for id, name in disabledProbs %} +
  • {{ name }}
  • + {% endfor %} +
+
+ {% endif %} +
{% include 'jury/partials/rejudging_matrix.html.twig' %}
diff --git a/webapp/templates/jury/submission.html.twig b/webapp/templates/jury/submission.html.twig index e6e2bf741c..b965604dce 100644 --- a/webapp/templates/jury/submission.html.twig +++ b/webapp/templates/jury/submission.html.twig @@ -220,7 +220,7 @@ {% endif %} - {% if submission.externalid %} + {% if shadowMode() and submission.externalid %}
External ID: {% if external_ccs_submission_url is empty %} @@ -343,7 +343,10 @@ {% if selectedJudging is not null or externalJudgement is not null %} - {% include 'jury/partials/submission_graph.html.twig' %} + {% if (selectedJudging is not null and selectedJudging.result != 'compiler-error') + or (externalJudgement is not null and externalJudgement.result != 'compiler-error') %} + {% include 'jury/partials/submission_graph.html.twig' %} + {% endif %} {% if selectedJudging is not null %} @@ -411,7 +414,6 @@
{% if not submission.importError %} - Result: {% if selectedJudging is null or selectedJudging.result is empty %} {%- if selectedJudging and selectedJudging.started %} {{- '' | printValidJuryResult -}} @@ -428,7 +430,7 @@ {%- if lastJudging is not null -%} {% set lastSubmissionLink = path('jury_submission', {submitId: lastSubmission.submitid}) %}{#- -#} - (s{{ lastSubmission.submitid }}: {{ lastJudging.result }}){#- + (s{{ lastSubmission.submitid }}: {{ lastJudging.result | printResult }}){#- -#} {%- endif -%} {%- if externalJudgement is not null %} @@ -439,24 +441,19 @@ {% endif %} {%- endif %} {%- if selectedJudging is not null and judgehosts is not empty -%} - , Judgehost(s): - {% for judgehostid, hostname in judgehosts %} - {% set judgehostLink = path('jury_judgehost', {judgehostid: judgehostid}) %} - {{ hostname | printHost }} - {% endfor %} - - {%- if selectedJudging.starttime -%} - Judging started: {{ selectedJudging.starttime | printtime('H:i:s') }} - {%- if selectedJudging.endtime -%} - , finished in {{ selectedJudging.starttime | printtimediff(selectedJudging.endtime) }}s - {%- elseif selectedJudging.valid or selectedJudging.rejudging -%} -  [still judging - busy {{ selectedJudging.starttime | printtimediff }}] - {%- else -%} -  [aborted] - {%- endif -%} - + , on {{ judgehosts | printHosts }} + {% if selectedJudging.starttime %} + {% if selectedJudging.endtime %} + , took {{ selectedJudging.starttime | printHumanTimeDiff(selectedJudging.endtime) }} + {% elseif selectedJudging.valid or selectedJudging.rejudging %} +  [still judging - busy {{ selectedJudging.starttime | printtimediff }}] + {% else %} +  [aborted] + {% endif %} + (started: {{ selectedJudging.starttime | printtime('H:i:s') }}) {% else %} - Judging not started yet - {%- endif -%} + , not started yet + {% endif %} {% endif -%} {%- if externalJudgement is not null %} (external judging started: {{ externalJudgement.starttime | printtime('H:i:s') }} @@ -467,21 +464,6 @@ {%- endif -%} ) {%- endif -%} - {%- if selectedJudging is not null and selectedJudging.result != 'compiler-error' -%} - , max/sum runtime: - {{ selectedJudging.maxRuntime | number_format(2, '.', '') }}/{{ selectedJudging.sumRuntime | number_format(2, '.', '') }}s - {%- if lastJudging is not null -%} - - (s{{ lastSubmission.submitid }}: - {{ lastJudging.maxRuntime | number_format(2, '.', '') }}{#- - -#}/{{ lastJudging.sumRuntime | number_format(2, '.', '') }}s) - - {%- endif -%} - {% endif -%} - {%- if externalJudgement is not null and externalJudgement.result != 'compiler-error' and externalJudgement.result != null -%} - , external max/sum runtime: - {{ externalJudgement.maxRuntime | number_format(2, '.', '') }}/{{ externalJudgement.sumRuntime | number_format(2, '.', '') }}s - {% endif %}
{# Display testcase results #} @@ -489,7 +471,6 @@ {% if not submission.importError %} - + {% endif %} - {% if lastJudging is not null %} - + {% if externalJudgement is not null %} + {% endif %} - {% if externalJudgement is not null %} - - + {% if lastJudging is not null %} + + {% endif %}
testcase runs: {% if selectedJudging is null %} {% set judgingDone = false %} @@ -497,6 +478,8 @@ {% set judgingDone = selectedJudging.endtime is not empty %} {% endif %} {{ runs | displayTestcaseResults(judgingDone) }} + {% if selectedJudging is not null and runsOutstanding %} {% if selectedJudging.judgeCompletely %} @@ -510,45 +493,40 @@
- s{{ lastSubmission.submitid }} runs: + {{ externalRuns | displayTestcaseResults(externalJudgement.endtime is not empty, true) }} - {{ lastRuns | displayTestcaseResults(lastJudging.endtime is not empty) }} + {% if externalSubmissionUrl and externalSubmissionUrl is not empty %} + + {% endif %} + external {{ externalJudgement.extjudgementid }} + {% if externalSubmissionUrl and externalSubmissionUrl is not empty %} + + {% endif %}
external runs:
- {{ externalRuns | displayTestcaseResults(externalJudgement.endtime is not empty, true) }} + {{ lastRuns | displayTestcaseResults(lastJudging.endtime is not empty) }} + + previous s{{ lastSubmission.submitid }} + {% if lastJudging.verifyComment %} + (verify comment: '{{ lastJudging.verifyComment }}') + {% endif %}
- {# Show JS toggle of previous submission results #} - {% if lastJudging is not null %} - - show/hide - results of previous submission s{{ lastSubmission.submitid }} - {% if lastJudging.verifyComment %} - (verify comment: '{{ lastJudging.verifyComment }}') - {% endif %} - - {% endif %} {% endif %}
- - {# Show verify info, but only when a result is known #} {% if selectedJudging is not null and selectedJudging.result is not empty %} {% include 'jury/partials/verify_form.html.twig' with { @@ -704,13 +682,13 @@ - {% if runsOutput[runIdx].is_output_run_truncated %} + {% if runsOutput[runIdx].is_output_run_truncated_in_db %}
+ {% if disabledLangs %} +
+ The following languages are currently not allowed to be judged: +
    + {% for id, name in disabledLangs %} +
  • {{ name }}
  • + {% endfor %} +
+
+ {% endif %} + {% if disabledProbs %} +
+ The following problems are currently not allowed to be judged: +
    + {% for id, name in disabledProbs %} +
  • {{ name }}
  • + {% endfor %} +
+
+ {% endif %} +
{%- include 'jury/partials/submission_list.html.twig' %}
diff --git a/webapp/templates/jury/team.html.twig b/webapp/templates/jury/team.html.twig index 8c0916d13a..c52a09ec0d 100644 --- a/webapp/templates/jury/team.html.twig +++ b/webapp/templates/jury/team.html.twig @@ -19,12 +19,10 @@
ID {{ team.teamid }}
External ID{{ team.externalid }}
External ID{{ team.externalid }}
ICPC ID @@ -126,10 +124,7 @@
Affiliation - {% set affiliationId = team.affiliation.affilid %} - {% if showExternalId(team.affiliation) %} - {% set affiliationId = team.affiliation.externalid %} - {% endif %} + {% set affiliationId = team.affiliation.externalid %} {% set affiliationLogo = affiliationId | assetPath('affilation') %} {% if affiliationLogo %} {{ team.affiliation.shortname }} - {% set teamId = team.teamid %} - {% if showExternalId(team) %} - {% set teamId = team.externalid %} - {% endif %} + {% set teamId = team.externalid %} {% set teamImage = teamId | assetPath('team') %} {% if teamImage %}
- Picture of team {{ team.name }}
{% endif %} diff --git a/webapp/templates/jury/team_affiliation.html.twig b/webapp/templates/jury/team_affiliation.html.twig index 3609b3d3c5..1de78a3446 100644 --- a/webapp/templates/jury/team_affiliation.html.twig +++ b/webapp/templates/jury/team_affiliation.html.twig @@ -19,12 +19,10 @@
ID {{ teamAffiliation.affilid }}
External ID{{ teamAffiliation.externalid }}
External ID{{ teamAffiliation.externalid }}
ICPC ID @@ -42,10 +40,7 @@
Logo {{ teamCategory.categoryid }}
External ID{{ teamCategory.externalid }}
External ID{{ teamCategory.externalid }}
ICPC ID diff --git a/webapp/templates/jury/user.html.twig b/webapp/templates/jury/user.html.twig index f75f67701b..2982b85c76 100644 --- a/webapp/templates/jury/user.html.twig +++ b/webapp/templates/jury/user.html.twig @@ -19,12 +19,10 @@ ID {{ user.userid }}
External ID{{ user.externalid }}
External ID{{ user.externalid }}
Login {{ user.username }}
+
{# output table column groups (for the styles) #} @@ -168,10 +168,7 @@ {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} {% endif %} - {% set affiliationId = score.team.affiliation.affilid %} - {% if showExternalId(score.team.affiliation) %} - {% set affiliationId = score.team.affiliation.externalid %} - {% endif %} + {% set affiliationId = score.team.affiliation.externalid %} {% set affiliationImage = affiliationId | assetPath('affiliation') %} {% if affiliationImage %} 0 %} @@ -260,6 +257,13 @@ {% elseif matrixItem.numSubmissions > 0 %} {% set scoreCssClass = 'score_incorrect' %} {% endif %} + {% if jury and showPending and matrixItem.numSubmissionsPending > 0 %} + {% if scoreCssClass == 'score_pending' %} + {% set scoreCssClass = scoreCssClass ~ ' score_incorrect' %} + {% else %} + {% set scoreCssClass = scoreCssClass ~ ' score_pending' %} + {% endif %} + {% endif %} {% set numSubmissions = matrixItem.numSubmissions %} {% if showPending and matrixItem.numSubmissionsPending > 0 %} @@ -314,6 +318,173 @@
+ + + {# output table column groups (for the styles) #} + + + {% if showFlags %} + + {% else %} + + {% endif %} + {% if showAffiliationLogos %} + + {% endif %} + + + + + + + {% set teamColspan = 2 %} + {% if showAffiliationLogos %} + {% set teamColspan = teamColspan + 1 %} + {% endif %} + + + + + + + + + {% set previousSortOrder = -1 %} + {% set previousTeam = null %} + {% set backgroundColors = {"#FFFFFF": 1} %} + {% set medalCount = 0 %} + {% for score in scores %} + {% set classes = [] %} + {% if score.team.category.sortorder != previousSortOrder %} + {% if previousSortOrder != -1 %} + {# Output summary of previous sort order #} + {% include 'partials/scoreboard_summary.html.twig' with {sortOrder: previousSortOrder} %} + {% endif %} + {% set classes = classes | merge(['sortorderswitch']) %} + {% set previousSortOrder = score.team.category.sortorder %} + {% set previousTeam = null %} + {% endif %} + + {# process medal color #} + {% set medalColor = '' %} + {% if showLegends %} + {% set medalColor = score.team | medalType(contest, scoreboard) %} + {% endif %} + + {# check whether this is us, otherwise use category colour #} + {% if myTeamId is defined and myTeamId == score.team.teamid %} + {% set classes = classes | merge(['scorethisisme']) %} + {% set color = '#FFFF99' %} + {% else %} + {% set color = score.team.category.color %} + {% endif %} + + + + {% if showAffiliationLogos %} + + {% endif %} + {% if color is null %} + {% set color = "#FFFFFF" %} + {% set colorClass = "_FFFFFF" %} + {% else %} + {% set colorClass = color | replace({"#": "_"}) %} + {% set backgroundColors = backgroundColors | merge({(color): 1}) %} + {% endif %} + + {% set totalTime = score.totalTime %} + {% if scoreInSeconds %} + {% set totalTime = totalTime | printTimeRelative %} + {% endif %} + {% set totalPoints = score.numPoints %} + + + + + + {% endfor %} + +
rankteamscore
+ {# Only print rank when score is different from the previous team #} + {% if not displayRank %} + ? + {% elseif previousTeam is null or scoreboard.scores[previousTeam.teamid].rank != score.rank %} + {{ score.rank }} + {% else %} + {% endif %} + {% set previousTeam = score.team %} + + {% if showFlags %} + {% if score.team.affiliation %} + {% set link = null %} + {% if jury %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% endif %} + + {{ score.team.affiliation.country|countryFlag }} + + {% endif %} + {% endif %} + + {% if score.team.affiliation %} + {% set link = null %} + {% if jury %} + {% set link = path('jury_team_affiliation', {'affilId': score.team.affiliation.affilid}) %} + {% endif %} + + {% set affiliationId = score.team.affiliation.externalid %} + {% set affiliationImage = affiliationId | assetPath('affiliation') %} + {% if affiliationImage %} + {{ score.team.affiliation.name }} + {% else %} + {{ affiliationId }} + {% endif %} + + {% endif %} + + {% set link = null %} + {% set extra = null %} + {% if static %} + {% set link = '#' %} + {% set extra = 'data-bs-toggle="modal" data-bs-target="#team-modal-' ~ score.team.teamid ~ '"' %} + {% else %} + {% if jury %} + {% set link = path('jury_team', {teamId: score.team.teamid}) %} + {% elseif public %} + {% set link = path('public_team', {teamId: score.team.teamid}) %} + {% set extra = 'data-ajax-modal' %} + {% else %} + {% set link = path('team_team', {teamId: score.team.teamid}) %} + {% set extra = 'data-ajax-modal' %} + {% endif %} + {% endif %} + + + {% if false and usedCategories | length > 1 and scoreboard.bestInCategory(score.team, limitToTeamIds) %} + + {{ score.team.category.name }} + + {% endif %} + {{ score.team.effectiveName }} + + {% if showAffiliations %} + + {% if score.team.affiliation %} + {{ score.team.affiliation.name }} + {% endif %} + + {% endif %} + + {{ totalPoints }}
{{ totalTime }}
+ + {% for problem in problems %} + {% set matrixItem = scoreboard.matrix[score.team.teamid][problem.probid] %} + {{ problem | problemBadgeMaybe(matrixItem) }} + {% endfor %} +
+ {% if static %} {% for score in scores %} {% embed 'partials/modal.html.twig' with {'modalId': 'team-modal-' ~ score.team.teamid} %} @@ -364,7 +535,7 @@ {% else %} {% set cellColors = {first: 'Solved first', correct: 'Solved', incorrect: 'Tried, incorrect', pending: 'Tried, pending', neutral: 'Untried'} %} {% endif %} - +
@@ -383,7 +554,7 @@ {% endif %} {% if medalsEnabled %} -
Cell colours
+
diff --git a/webapp/templates/partials/team.html.twig b/webapp/templates/partials/team.html.twig index be3bf714da..6bb371d8a0 100644 --- a/webapp/templates/partials/team.html.twig +++ b/webapp/templates/partials/team.html.twig @@ -25,10 +25,7 @@
Medals {% if not scoreboard.freezeData.showFinal %}(tentative){% endif %}
Affiliation - {% set affiliationId = team.affiliation.affilid %} - {% if showExternalId(team.affiliation) %} - {% set affiliationId = team.affiliation.externalid %} - {% endif %} + {% set affiliationId = team.affiliation.externalid %} {% set affiliationLogo = affiliationId | assetPath('affiliation') %} {% if affiliationLogo %} {{ team.affiliation.shortname }} - {% set teamId = team.teamid %} - {% if showExternalId(team) %} - {% set teamId = team.externalid %} - {% endif %} + {% set teamId = team.externalid %} {% set teamImage = teamId | assetPath('team') %} {% if teamImage %}
- Picture of team {{ team.effectiveName }}
{% endif %} diff --git a/webapp/templates/public/scoreboard.html.twig b/webapp/templates/public/scoreboard.html.twig index 9b87838085..e1f0dbb559 100644 --- a/webapp/templates/public/scoreboard.html.twig +++ b/webapp/templates/public/scoreboard.html.twig @@ -3,10 +3,7 @@ {% block title %}Scoreboard {{ contest.shortname | default('') }} - {{ parent() }}{% endblock %} {% block content %} {% if contest is defined %} - {% set contestId = contest.cid %} - {% if showExternalId(contest) %} - {% set contestId = contest.externalid %} - {% endif %} + {% set contestId = contest.externalid %} {% if contestId %} {% set bannerImage = contestId | assetPath('contest') %} {% endif %} @@ -15,7 +12,7 @@ {% set bannerImage = globalBannerAssetPath() %} {% endif %} {% if bannerImage %} - + {% endif %}
@@ -56,6 +53,7 @@ {% if static and refresh is defined %} disableRefreshOnModal(); {% endif %} + resizeMobileTeamNamesAndProblemBadges(); }; {% if static and refresh is defined %} 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/E2E/Controller/ControllerRolesTraversalTest.php b/webapp/tests/E2E/Controller/ControllerRolesTraversalTest.php index 600cdc0497..4b8600441f 100644 --- a/webapp/tests/E2E/Controller/ControllerRolesTraversalTest.php +++ b/webapp/tests/E2E/Controller/ControllerRolesTraversalTest.php @@ -32,14 +32,14 @@ class ControllerRolesTraversalTest extends BaseTestCase protected function getLoops(): array { - $dataSources = $this->getDatasourceLoops()['dataSources']; + $shadowModes = $this->getShadowModeLoops()['shadowModes']; $riskyURLs = []; if (array_key_exists('CRAWL_RISKY', getenv())) { $riskyURLs = explode(',', getenv('CRAWL_RISKY')); } elseif (!array_key_exists('CRAWL_ALL', getenv())) { $riskyURLs = array_slice(self::$riskyURLs, 0, 1); } - return ['dataSources' => $dataSources, 'riskyURLs' => $riskyURLs]; + return ['shadowModes' => $shadowModes, 'riskyURLs' => $riskyURLs]; } /** @@ -197,12 +197,12 @@ protected function verifyAccess(array $combinations, array $roleURLs, string $sk * @param string $roleBaseURL The base URL of the role. * @param string[] $baseRoles The default role of the user. * @param string[] $optionalRoles The roles which should not restrict the viewable pages. - * @param int $dataSource Put the installation in this dataSource mode. + * @param bool $shadowMode Put the installation in this shadow mode. * @dataProvider provideRoleAccessData */ - public function testRoleAccess(string $roleBaseURL, array $baseRoles, array $optionalRoles, bool $allPages, int $dataSource, string $skip): void + public function testRoleAccess(string $roleBaseURL, array $baseRoles, array $optionalRoles, bool $allPages, bool $shadowMode, string $skip): void { - $this->setupDatasource($dataSource); + $this->setupShadowMode($shadowMode); $this->roles = $baseRoles; $this->logOut(); $this->logIn(); @@ -224,7 +224,16 @@ public function visitWithNoContest(string $url, bool $dropdown): void $response = $this->client->getResponse(); self::assertNotEquals(500, $response->getStatusCode(), sprintf('Failed at %s', $url)); if ($dropdown && !str_contains($url, '/public')) { - self::assertSelectorExists('a#navbarDropdownContests:contains("no contest")'); + try { + self::assertSelectorExists('a#navbarDropdownContests:contains("no contest")', "Failed at: " . $url); + } catch (\Exception $e) { + $gitlabArtifacts = getenv('GITLABARTIFACTS'); + if ($gitlabArtifacts != '') { + $fileHandler = fopen(sprintf("%s/%s", $gitlabArtifacts, str_replace('/', '_s_', $url)), 'w'); + fwrite($fileHandler, $response->getContent()); + } + throw $e; + } } } @@ -242,10 +251,10 @@ public function testRoleAccessOtherRoles( array $roles, array $rolesOther, bool $allPages, - int $dataSource, + bool $shadowMode, string $skip ): void { - $this->setupDataSource($dataSource); + $this->setupShadowMode($shadowMode); $urlsToCheck = $this->getPagesRoles([$roleBaseURL], $roles, $allPages, $skip); $urlsToCheckOther = $this->getPagesRoles($roleOthersBaseURL, $rolesOther, $allPages, $skip); $this->roles = $roles; @@ -262,9 +271,9 @@ public function testRoleAccessOtherRoles( * Test that pages depending on an active contest do not crash on the server. * @dataProvider provideNoContestScenario */ - public function testNoContestAccess(string $roleBaseURL, array $baseRoles, int $dataSource, string $skip): void + public function testNoContestAccess(string $roleBaseURL, array $baseRoles, bool $shadowMode, string $skip): void { - $this->setupDataSource($dataSource); + $this->setupShadowMode($shadowMode); $this->roles = $baseRoles; $this->logOut(); $this->logIn(); @@ -284,16 +293,16 @@ public function testNoContestAccess(string $roleBaseURL, array $baseRoles, int $ */ public function provideRoleAccessData(): Generator { - ['dataSources' => $dataSources, 'riskyURLs' => $riskyURLs] = $this->getLoops(); + ['shadowModes' => $shadowModes, 'riskyURLs' => $riskyURLs] = $this->getLoops(); foreach ($riskyURLs as $skip) { - foreach ($dataSources as $str_data_source) { - $data_source = (int)$str_data_source; - yield ['/jury', ['admin'], ['jury','team','balloon','clarification_rw'], false, $data_source, $skip]; - yield ['/jury', ['jury'], ['admin','team','balloon','clarification_rw'], false, $data_source, $skip]; - yield ['/jury', ['balloon'], ['admin','team','clarification_rw'], true, $data_source, $skip]; - yield ['/jury', ['clarification_rw'], ['admin','team','balloon'], true, $data_source, $skip]; - yield ['/team', ['team'], ['admin','jury','balloon','clarification_rw'], true, $data_source, $skip]; - yield ['/public', [], ['team','admin','jury','balloon','clarification_rw'], true, $data_source, $skip]; + foreach ($shadowModes as $str_shadow_mode) { + $shadow_mode = (bool)$str_shadow_mode; + yield ['/jury', ['admin'], ['jury','team','balloon','clarification_rw'], false, $shadow_mode, $skip]; + yield ['/jury', ['jury'], ['admin','team','balloon','clarification_rw'], false, $shadow_mode, $skip]; + yield ['/jury', ['balloon'], ['admin','team','clarification_rw'], true, $shadow_mode, $skip]; + yield ['/jury', ['clarification_rw'], ['admin','team','balloon'], true, $shadow_mode, $skip]; + yield ['/team', ['team'], ['admin','jury','balloon','clarification_rw'], true, $shadow_mode, $skip]; + yield ['/public', [], ['team','admin','jury','balloon','clarification_rw'], true, $shadow_mode, $skip]; } } } @@ -309,31 +318,31 @@ public function provideRoleAccessData(): Generator **/ public function provideRoleAccessOtherRoles(): Generator { - ['dataSources' => $dataSources, 'riskyURLs' => $riskyURLs] = $this->getLoops(); + ['shadowModes' => $shadowModes, 'riskyURLs' => $riskyURLs] = $this->getLoops(); foreach ($riskyURLs as $skip) { - foreach ($dataSources as $str_data_source) { - $data_source = (int)$str_data_source; - yield ['/jury', ['/jury','/team'], ['admin'], ['jury','team'], false, $data_source, $skip]; - yield ['/jury', ['/jury','/team'], ['jury'], ['admin','team'], false, $data_source, $skip]; - yield ['/jury', ['/jury','/team'], ['balloon'], ['admin','team','clarification_rw'], false, $data_source, $skip]; - yield ['/jury', ['/jury','/team'], ['clarification_rw'], ['admin','team','balloon'], false, $data_source, $skip]; - yield ['/team', ['/jury'], ['team'], ['admin','jury','balloon','clarification_rw'], true, $data_source, $skip]; - yield ['/public', ['/jury','/team'], [], ['admin','jury','team','balloon','clarification_rw'], true, $data_source, $skip]; + foreach ($shadowModes as $str_shadow_mode) { + $shadow_mode = (bool)$str_shadow_mode; + yield ['/jury', ['/jury','/team'], ['admin'], ['jury','team'], false, $shadow_mode, $skip]; + yield ['/jury', ['/jury','/team'], ['jury'], ['admin','team'], false, $shadow_mode, $skip]; + yield ['/jury', ['/jury','/team'], ['balloon'], ['admin','team','clarification_rw'], false, $shadow_mode, $skip]; + yield ['/jury', ['/jury','/team'], ['clarification_rw'], ['admin','team','balloon'], false, $shadow_mode, $skip]; + yield ['/team', ['/jury'], ['team'], ['admin','jury','balloon','clarification_rw'], true, $shadow_mode, $skip]; + yield ['/public', ['/jury','/team'], [], ['admin','jury','team','balloon','clarification_rw'], true, $shadow_mode, $skip]; } } } public function provideNoContestScenario(): Generator { - ['dataSources' => $dataSources, 'riskyURLs' => $riskyURLs] = $this->getLoops(); + ['shadowModes' => $shadowModes, 'riskyURLs' => $riskyURLs] = $this->getLoops(); foreach ($riskyURLs as $skip) { - foreach ($dataSources as $str_data_source) { - $data_source = (int)$str_data_source; - yield ['/jury', ['admin'], $data_source, $skip]; - yield ['/jury', ['jury'], $data_source, $skip]; - yield ['/jury', ['balloon'], $data_source, $skip]; - yield ['/jury', ['clarification_rw'], $data_source, $skip]; - yield ['/team', ['team'], $data_source, $skip]; + foreach ($shadowModes as $str_shadow_modes) { + $shadow_mode = (bool)$str_shadow_modes; + yield ['/jury', ['admin'], $shadow_mode, $skip]; + yield ['/jury', ['jury'], $shadow_mode, $skip]; + yield ['/jury', ['balloon'], $shadow_mode, $skip]; + yield ['/jury', ['clarification_rw'], $shadow_mode, $skip]; + yield ['/team', ['team'], $shadow_mode, $skip]; } } } diff --git a/webapp/tests/Unit/BaseTestCase.php b/webapp/tests/Unit/BaseTestCase.php index fd3c611dc8..fbe52541b7 100644 --- a/webapp/tests/Unit/BaseTestCase.php +++ b/webapp/tests/Unit/BaseTestCase.php @@ -2,6 +2,7 @@ namespace App\Tests\Unit; +use App\Entity\HasExternalIdInterface; use App\Entity\Role; use App\Entity\User; use App\Service\ConfigurationService; @@ -34,9 +35,6 @@ abstract class BaseTestCase extends WebTestCase protected ?ORMExecutor $fixtureExecutor = null; /** @var string[] */ protected static array $fixtures = []; - protected static array $dataSources = [DOMJudgeService::DATA_SOURCE_LOCAL, - DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL, - DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL]; protected ?string $entityClass = null; @@ -143,7 +141,7 @@ protected function loginHelper( */ protected function logIn(): void { - $this->client->loginUser($this->setupUser()); + $this->client->loginUser($this->setupUser(), 'domjudge'); } /** @@ -248,22 +246,12 @@ protected function verifyRedirectToURL(string $url): void self::assertEquals($url, $crawler->getUri()); } - /** - * Whether the data source is local. - */ - protected function dataSourceIsLocal(): bool - { - $config = self::getContainer()->get(ConfigurationService::class); - $dataSource = $config->get('data_source'); - return $dataSource === DOMJudgeService::DATA_SOURCE_LOCAL; - } - /** * Resolve the entity ID for the given class if not running in local mode. */ protected function resolveEntityId(string $class, ?string $id): ?string { - if ($id !== null && !$this->dataSourceIsLocal()) { + if ($id !== null) { $entity = static::getContainer()->get(EntityManagerInterface::class)->getRepository($class)->find($id); // If we can't find the entity, assume we use an invalid one. if ($entity === null) { @@ -306,22 +294,22 @@ protected function removeTestContainer(): void self::bootKernel(); } - protected function getDatasourceLoops(): array + protected function getShadowModeLoops(): array { - $dataSources = []; - if (array_key_exists('CRAWL_DATASOURCES', getenv())) { - $dataSources = explode(',', getenv('CRAWL_DATASOURCES')); + $shadowModes = []; + if (array_key_exists('CRAWL_SHADOW_MODE', getenv())) { + $shadowModes = explode(',', getenv('CRAWL_SHADOW_MODE')); } elseif (!array_key_exists('CRAWL_ALL', getenv())) { - $dataSources = array_slice(self::$dataSources, 0, 1); + $shadowModes = [0]; } - return ['dataSources' => $dataSources]; + return ['shadowModes' => $shadowModes]; } - protected function setupDataSource(int $dataSource): void + protected function setupShadowMode(bool $shadowMode): void { $config = self::getContainer()->get(ConfigurationService::class); $eventLog = self::getContainer()->get(EventLogService::class); $dj = self::getContainer()->get(DOMJudgeService::class); - $config->saveChanges(['data_source'=>$dataSource], $eventLog, $dj, treatMissingBooleansAsFalse: false); + $config->saveChanges(['shadow_mode'=>$shadowMode], $eventLog, $dj, treatMissingBooleansAsFalse: false); } } diff --git a/webapp/tests/Unit/Controller/API/AccountBaseTestCase.php b/webapp/tests/Unit/Controller/API/AccountBaseTestCase.php index 40b5d0c44d..58ee2a4fa4 100644 --- a/webapp/tests/Unit/Controller/API/AccountBaseTestCase.php +++ b/webapp/tests/Unit/Controller/API/AccountBaseTestCase.php @@ -2,6 +2,7 @@ namespace App\Tests\Unit\Controller\API; +use App\DataFixtures\Test\TeamWithExternalIdEqualsOneFixture; use Generator; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Yaml\Yaml; @@ -135,6 +136,7 @@ public function provideNewAccount(): Generator */ public function testCreateUserFileImport(string $newUsersFile, string $type, array $newUserPostData, ?array $overwritten = null): void { + $this->loadFixture(TeamWithExternalIdEqualsOneFixture::class); $usersURL = $this->helperGetEndpointURL('users').'/accounts'; $myURL = $this->helperGetEndpointURL($this->apiEndpoint); $objectsBeforeTest = $this->verifyApiJsonResponse('GET', $myURL, 200, $this->apiUser); diff --git a/webapp/tests/Unit/Controller/API/AccountControllerTest.php b/webapp/tests/Unit/Controller/API/AccountControllerTest.php index 3672b0ff1f..4e63fd1b54 100644 --- a/webapp/tests/Unit/Controller/API/AccountControllerTest.php +++ b/webapp/tests/Unit/Controller/API/AccountControllerTest.php @@ -11,24 +11,24 @@ class AccountControllerTest extends AccountBaseTestCase protected ?string $apiUser = 'admin'; protected array $expectedObjects = [ - 1 => [ - "id" => "1", + 'admin' => [ + "id" => "admin", "username" => "admin", - "team_id" => "1", + "team_id" => "domjudge", "type" => "admin", "ip" => null, ], - 2 => [ - "id" => "2", + 'judgehost' => [ + "id" => "judgehost", "username" => "judgehost", "team_id" => null, "type" => null, "ip" => null, ], - 3 => [ - "id" => "3", + 'demo' => [ + "id" => "demo", "username" => "demo", - "team_id" => "2", + "team_id" => "exteam", "type" => "team", "ip" => null, ], @@ -56,7 +56,7 @@ public function testCurrentAccountNotLoggedIn(): void public function provideCurrentAccount(): Generator { - yield ['admin', ['id' => '1', 'team_id' => '1', 'username' => 'admin', 'type' => 'admin']]; - yield ['demo', ['id' => '3', 'team_id' => '2', 'username' => 'demo', 'type' => 'team']]; + yield ['admin', ['id' => 'admin', 'team_id' => 'domjudge', 'username' => 'admin', 'type' => 'admin']]; + yield ['demo', ['id' => 'demo', 'team_id' => 'exteam', 'username' => 'demo', 'type' => 'team']]; } } diff --git a/webapp/tests/Unit/Controller/API/AwardsControllerTest.php b/webapp/tests/Unit/Controller/API/AwardsControllerTest.php index 4e7b2397bd..e3d76e62d4 100644 --- a/webapp/tests/Unit/Controller/API/AwardsControllerTest.php +++ b/webapp/tests/Unit/Controller/API/AwardsControllerTest.php @@ -21,7 +21,7 @@ class AwardsControllerTest extends BaseTestCase * This means that there's only the winner/gold award. */ protected array $expectedObjects = [ - 'winner' => ["id" => "winner", "citation" => "Contest winner", "team_ids" => [2]], + 'winner' => ["id" => "winner", "citation" => "Contest winner", "team_ids" => ['exteam']], ]; protected array $expectedAbsent = ['bronze-medal', 'first-to-solve']; diff --git a/webapp/tests/Unit/Controller/API/BalloonsControllerTest.php b/webapp/tests/Unit/Controller/API/BalloonsControllerTest.php index a88b18c4c7..2780863bb5 100644 --- a/webapp/tests/Unit/Controller/API/BalloonsControllerTest.php +++ b/webapp/tests/Unit/Controller/API/BalloonsControllerTest.php @@ -18,10 +18,6 @@ public function getUnitContestId(): string $manager = static::getContainer()->get(EntityManagerInterface::class); /** @var Contest $contest */ $contest = $manager->getRepository(Contest::class)->findOneBy(['shortname' => 'beforeFreeze']); - if ($this->dataSourceIsLocal()) { - return (string)$contest->getCid(); - } - return $contest->getExternalid(); } diff --git a/webapp/tests/Unit/Controller/API/BaseTestCase.php b/webapp/tests/Unit/Controller/API/BaseTestCase.php index 49c4636648..1549cfcb80 100644 --- a/webapp/tests/Unit/Controller/API/BaseTestCase.php +++ b/webapp/tests/Unit/Controller/API/BaseTestCase.php @@ -31,7 +31,6 @@ abstract class BaseTestCase extends BaseBaseTestCase */ protected array $expectedObjects = []; - /** If the class to check uses external IDs in non-local mode, set the class name. */ protected ?string $objectClassForExternalId = null; /** @@ -67,10 +66,6 @@ protected function setUp(): void */ protected function getDemoContestId(): string { - if ($this->dataSourceIsLocal()) { - return (string)$this->demoContest->getCid(); - } - return $this->demoContest->getExternalid(); } diff --git a/webapp/tests/Unit/Controller/API/ClarificationControllerTest.php b/webapp/tests/Unit/Controller/API/ClarificationControllerTest.php index 4c3880f667..cb02f0099b 100644 --- a/webapp/tests/Unit/Controller/API/ClarificationControllerTest.php +++ b/webapp/tests/Unit/Controller/API/ClarificationControllerTest.php @@ -22,8 +22,8 @@ class ClarificationControllerTest extends BaseTestCase protected array $expectedObjects = [ ClarificationFixture::class . ':0' => [ - "problem_id" => "1", - "from_team_id" => "2", + "problem_id" => "hello", + "from_team_id" => "exteam", "to_team_id" => null, "reply_to_id" => null, "time" => "2018-02-11T21:48:58.901+00:00", @@ -40,9 +40,9 @@ class ClarificationControllerTest extends BaseTestCase "answered" => true, ], ClarificationFixture::class . ':2' => [ - "problem_id" => "1", + "problem_id" => "hello", "from_team_id" => null, - "to_team_id" => "2", + "to_team_id" => "exteam", "reply_to_id" => null, "time" => "2018-02-11T21:47:43.689+00:00", "text" => "There was a mistake in judging this problem. Please try again", @@ -90,7 +90,7 @@ public function testTeamOnlyGeneralAndRelatedToTeam(): void $clarificationFromApi = $this->verifyApiJsonResponse('GET', $clarificationApi.$postfix, 200, 'demo'); static::assertCount($expectedNumber, $clarificationFromApi); - static::assertEquals("2", $clarificationFromApi[0]['from_team_id']); + static::assertEquals("exteam", $clarificationFromApi[0]['from_team_id']); static::assertEquals("Is it necessary to read the problem statement carefully?", $clarificationFromApi[0]['text']); static::assertArrayNotHasKey('answered', $clarificationFromApi[0]); @@ -100,7 +100,7 @@ public function testTeamOnlyGeneralAndRelatedToTeam(): void static::assertArrayNotHasKey('answered', $clarificationFromApi[1]); } - static::assertEquals("2", $clarificationFromApi[$mistakJudgingId]['to_team_id']); + static::assertEquals("exteam", $clarificationFromApi[$mistakJudgingId]['to_team_id']); static::assertEquals("There was a mistake in judging this problem. Please try again", $clarificationFromApi[$mistakJudgingId]['text']); static::assertArrayNotHasKey('answered', $clarificationFromApi[$mistakJudgingId]); } @@ -143,16 +143,16 @@ public function testAddInvalidData(string $user, array $dataToSend, string $expe public function provideAddInvalidData(): Generator { yield ['demo', [], ""]; - yield ['demo', ['invalidfield' => 'value'], "/text:\n.*This value should be of type unknown./"]; - yield ['demo', ['text' => 'This is a clarification', 'from_team_id' => '1'], "Can not create a clarification from a different team."]; - yield ['demo', ['text' => 'This is a clarification', 'to_team_id' => '2'], "Can not create a clarification that is sent to a team."]; - yield ['demo', ['text' => 'This is a clarification', 'problem_id' => '4'], "Problem '4' not found."]; + yield ['demo', ['invalidfield' => 'value'], "/text:\n.*This value should be of type string./"]; + yield ['demo', ['text' => 'This is a clarification', 'from_team_id' => 'domjudge'], "Can not create a clarification from a different team."]; + yield ['demo', ['text' => 'This is a clarification', 'to_team_id' => 'exteam'], "Can not create a clarification that is sent to a team."]; + yield ['demo', ['text' => 'This is a clarification', 'problem_id' => 'prob'], "Problem 'prob' not found."]; yield ['demo', ['text' => 'This is a clarification', 'time' => '1234'], "A team can not assign time."]; yield ['demo', ['text' => 'This is a clarification', 'id' => '1234'], "A team can not assign id."]; yield ['demo', ['text' => 'This is a clarification', 'reply_to_id' => 'nonexistent'], "Clarification 'nonexistent' not found."]; - yield ['admin', ['text' => 'This is a clarification', 'from_team_id' => '2', 'to_team_id' => '2'], "Can not send a clarification from and to a team."]; - yield ['admin', ['text' => 'This is a clarification', 'from_team_id' => '3'], "Team with ID '3' not found in contest or not enabled."]; - yield ['admin', ['text' => 'This is a clarification', 'to_team_id' => '3'], "Team with ID '3' not found in contest or not enabled."]; + yield ['admin', ['text' => 'This is a clarification', 'from_team_id' => 'exteam', 'to_team_id' => 'exteam'], "Can not send a clarification from and to a team."]; + yield ['admin', ['text' => 'This is a clarification', 'from_team_id' => 'noteam'], "Team with ID 'noteam' not found in contest or not enabled."]; + yield ['admin', ['text' => 'This is a clarification', 'to_team_id' => 'noteam'], "Team with ID 'noteam' not found in contest or not enabled."]; yield ['admin', ['text' => 'This is a clarification', 'time' => 'this is not a time'], "Can not parse time 'this is not a time'."]; } @@ -202,10 +202,10 @@ public function testAddSuccess( array $dataToSend, bool $idIsExternal, string $expectedBody, - ?int $expectedProblemId, - ?int $expectedInReplyToId, - ?int $expectedSenderId, - ?int $expectedRecipientId, + ?string $expectedProblemId, + ?string $expectedInReplyToId, + ?string $expectedSenderId, + ?string $expectedRecipientId, ?string $expectedClarificationExternalId, // If known ?string $expectedTime // If known ): void { @@ -272,7 +272,7 @@ public function provideAddSuccess(): Generator 'This is some text', null, null, - 2, + 'exteam', null, null, null, @@ -315,24 +315,24 @@ public function provideAddSuccess(): Generator ]; yield [ 'admin', - ['text' => 'This is a clarification to a specific team', 'to_team_id' => '2'], + ['text' => 'This is a clarification to a specific team', 'to_team_id' => 'exteam'], false, 'This is a clarification to a specific team', null, null, null, - 2, + 'exteam', null, null, ]; yield [ 'admin', - ['text' => 'This is a clarification from a specific team', 'from_team_id' => '1'], + ['text' => 'This is a clarification from a specific team', 'from_team_id' => 'domjudge'], false, 'This is a clarification from a specific team', null, null, - 1, + 'domjudge', null, null, null, diff --git a/webapp/tests/Unit/Controller/API/ConfigControllerTest.php b/webapp/tests/Unit/Controller/API/ConfigControllerTest.php index 1f7530a251..1e83bfb29d 100644 --- a/webapp/tests/Unit/Controller/API/ConfigControllerTest.php +++ b/webapp/tests/Unit/Controller/API/ConfigControllerTest.php @@ -40,7 +40,7 @@ public function testConfigDoesNotReturnSecretVariables(?string $user): void $response = $this->verifyApiJsonResponse('GET', $this->endpoint, 200, $user); static::assertIsArray($response); - $secretvars = ['script_memory_limit', 'clar_answers', 'external_ccs_submission_url', 'data_source']; + $secretvars = ['script_memory_limit', 'clar_answers', 'external_ccs_submission_url', 'shadow_mode']; foreach ($secretvars as $secretvar) { static::assertArrayNotHasKey($secretvar, $response); } @@ -56,7 +56,7 @@ public function testConfigReturnsSecretVariablesForAdmin(): void static::assertIsArray($response['clar_answers']); static::assertEquals($answers, $response['clar_answers']); static::assertEquals("", $response['external_ccs_submission_url']); - static::assertEquals(0, $response['data_source']); + static::assertEquals(0, $response['shadow_mode']); } /** @@ -90,7 +90,7 @@ public function testConfigChangeAPIVisible(): void static::assertIsArray($response); static::assertEquals(false, $response['compile_penalty']); static::assertEquals(20, $response['penalty_time']); - static::assertEquals(0, $response['data_source']); + static::assertEquals(0, $response['shadow_mode']); $proposedChange = ['compile_penalty' => true, 'penalty_time' => 21]; $response = $this->verifyApiJsonResponse('PUT', $this->endpoint, 200, 'admin', $proposedChange); @@ -98,7 +98,7 @@ public function testConfigChangeAPIVisible(): void static::assertIsArray($response); static::assertEquals(true, $response['compile_penalty']); static::assertEquals(21, $response['penalty_time']); - static::assertEquals(0, $response['data_source']); + static::assertEquals(0, $response['shadow_mode']); } /** diff --git a/webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php b/webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php index 3430b19799..09519b4ca3 100644 --- a/webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php +++ b/webapp/tests/Unit/Controller/API/ContestControllerAdminTest.php @@ -61,6 +61,10 @@ public function testAddYaml(): void end_time: '2021-03-27T11:00:00+00:00' duration: 2:00:00.000 penalty_time: 20 +medals: + gold: 4 + silver: 4 + bronze: 4 scoreboard_freeze_time: '2021-03-27T10:30:00+00:00' scoreboard_freeze_duration: 0:30:00 EOF; @@ -114,13 +118,7 @@ protected function getContest(int|string $cid): Contest { // First clear the entity manager to have all data. static::getContainer()->get(EntityManagerInterface::class)->clear(); - $config = static::getContainer()->get(ConfigurationService::class); - $dataSource = $config->get('data_source'); - if ($dataSource === DOMJudgeService::DATA_SOURCE_LOCAL) { - return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Contest::class)->find($cid); - } else { - return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Contest::class)->findOneBy(['externalid' => $cid]); - } + return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Contest::class)->findOneBy(['externalid' => $cid]); } public function testBannerManagement(): void @@ -177,7 +175,7 @@ public function testProblemsetManagement(): void self::assertArrayNotHasKey('problemset', $object); // Now upload a problemset - $problemsetFile = __DIR__ . '/../../../../../webapp/public/doc/logos/DOMjudgelogo.pdf'; + $problemsetFile = __DIR__ . '/../../../../public/doc/logos/DOMjudgelogo.pdf'; $problemset = new UploadedFile($problemsetFile, 'DOMjudgelogo.pdf'); $this->verifyApiJsonResponse('POST', $url . '/problemset', 204, $this->apiUser, null, ['problemset' => $problemset]); @@ -201,7 +199,7 @@ public function testProblemsetManagement(): void self::assertEquals(file_get_contents($problemsetFile), $callbackData); // Upload the problemset again, this time using PUT to also test that - $problemsetFile = __DIR__ . '/../../../../../webapp/public/doc/logos/DOMjudgelogo.pdf'; + $problemsetFile = __DIR__ . '/../../../../public/doc/logos/DOMjudgelogo.pdf'; $problemset = new UploadedFile($problemsetFile, 'DOMjudgelogo.pdf'); $this->verifyApiJsonResponse('PUT', $url . '/problemset', 204, $this->apiUser, null, ['problemset' => $problemset]); @@ -276,7 +274,7 @@ public function provideChangeTimes(): Generator // General tests yield [[], 400, '']; - yield [['dummy' => 'dummy'], 400, "This value should be of type unknown."]; + yield [['dummy' => 'dummy'], 400, "This value should be of type string."]; yield [['id' => 1], 400, 'Missing \"start_time\" or \"scoreboard_thaw_time\" in request.']; yield [['id' => 1, 'start_time' => null, 'scoreboard_thaw_time' => date('Y-m-d\TH:i:s', strtotime('+15 seconds'))], 400, 'Setting both \"start_time\" and \"scoreboard_thaw_time\" at the same time is not allowed.']; diff --git a/webapp/tests/Unit/Controller/API/ContestControllerTest.php b/webapp/tests/Unit/Controller/API/ContestControllerTest.php index 08ff2c08fc..ae937d858e 100644 --- a/webapp/tests/Unit/Controller/API/ContestControllerTest.php +++ b/webapp/tests/Unit/Controller/API/ContestControllerTest.php @@ -2,20 +2,21 @@ namespace App\Tests\Unit\Controller\API; +use App\Entity\Contest; + class ContestControllerTest extends BaseTestCase { protected ?string $apiEndpoint = 'contests'; protected array $expectedObjects = [ - '1' => [ + 'demo' => [ 'formal_name' => 'Demo contest', 'penalty_time' => 20, // 'start_time' => '2021-01-01T11:00:00+00:00', // 'end_time' => '2024-01-01T16:00:00+00:00', 'duration' => '5:00:00.000', 'scoreboard_freeze_duration' => '1:00:00.000', - 'id' => '1', - 'external_id' => 'demo', + 'id' => 'demo', 'name' => 'Demo contest', 'shortname' => 'demo', 'banner' => null, @@ -23,4 +24,6 @@ class ContestControllerTest extends BaseTestCase ]; protected array $expectedAbsent = ['4242', 'nonexistent']; + + protected ?string $objectClassForExternalId = Contest::class; } diff --git a/webapp/tests/Unit/Controller/API/GroupControllerTest.php b/webapp/tests/Unit/Controller/API/GroupControllerTest.php index cf90038274..63f75d6274 100644 --- a/webapp/tests/Unit/Controller/API/GroupControllerTest.php +++ b/webapp/tests/Unit/Controller/API/GroupControllerTest.php @@ -10,26 +10,26 @@ class GroupControllerTest extends BaseTestCase protected ?string $apiEndpoint = 'groups'; protected array $expectedObjects = [ - '2' => [ + 'self-registered' => [ 'hidden' => false, 'icpc_id' => null, - 'id' => '2', + 'id' => 'self-registered', 'name' => 'Self-Registered', 'sortorder' => 8, 'color' => '#33cc44' ], - '3' => [ + 'participants' => [ 'hidden' => false, 'icpc_id' => null, - 'id' => '3', + 'id' => 'participants', 'name' => 'Participants', 'sortorder' => 0, 'color' => null ], - '4' => [ + 'observers' => [ 'hidden' => false, 'icpc_id' => null, - 'id' => '4', + 'id' => 'observers', 'name' => 'Observers', 'sortorder' => 1, 'color' => '#ffcc33' @@ -91,9 +91,6 @@ public function testNewAddedGroupPostWithId(): void */ public function testNewAddedGroupPut(array $newGroupPostData): void { - // This only works for non-local data sources - $this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL); - $url = $this->helperGetEndpointURL($this->apiEndpoint); $objectsBeforeTest = $this->verifyApiJsonResponse('GET', $url, 200, $this->apiUser); @@ -118,9 +115,6 @@ public function testNewAddedGroupPut(array $newGroupPostData): void */ public function testNewAddedGroupPutWithoutId(array $newGroupPostData): void { - // This only works for non-local data sources - $this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL); - $url = $this->helperGetEndpointURL($this->apiEndpoint); $returnedObject = $this->verifyApiJsonResponse('PUT', $url . '/someid', 400, 'admin', $newGroupPostData); self::assertStringContainsString('ID in URL does not match ID in payload', $returnedObject['message']); @@ -131,9 +125,6 @@ public function testNewAddedGroupPutWithoutId(array $newGroupPostData): void */ public function testNewAddedGroupPutWithDifferentId(array $newGroupPostData): void { - // This only works for non-local data sources - $this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL); - $newGroupPostData['id'] = 'someotherid'; $url = $this->helperGetEndpointURL($this->apiEndpoint); $returnedObject = $this->verifyApiJsonResponse('PUT', $url . '/someid', 400, 'admin', $newGroupPostData); diff --git a/webapp/tests/Unit/Controller/API/OrganizationControllerTest.php b/webapp/tests/Unit/Controller/API/OrganizationControllerTest.php index 8c9676b373..fda292a563 100644 --- a/webapp/tests/Unit/Controller/API/OrganizationControllerTest.php +++ b/webapp/tests/Unit/Controller/API/OrganizationControllerTest.php @@ -14,10 +14,10 @@ class OrganizationControllerTest extends BaseTestCase protected ?string $entityClass = TeamAffiliation::class; protected array $expectedObjects = [ - '1' => [ + 'utrecht' => [ 'icpc_id' => null, 'shortname' => 'UU', - 'id' => '1', + 'id' => 'utrecht', 'name' => 'UU', 'formal_name' => 'Utrecht University', 'country' => 'NLD', @@ -113,7 +113,7 @@ public function testCountryAbsentWhenDisabled(): void $apiEndpoint = $this->apiEndpoint; $contestId = $this->getDemoContestId(); // The hardcoded 1 here is the team affiliation from the TeamAffiliationFixture example data fixture. - $organizationId = $this->dataSourceIsLocal() ? 1 : 'utrecht'; + $organizationId = 'utrecht'; $response = $this->verifyApiJsonResponse('GET', "/contests/$contestId/$apiEndpoint/$organizationId", 200); static::assertArrayNotHasKey('country', $response); @@ -143,7 +143,7 @@ public function testLogoManagement(): void $object = $this->verifyApiJsonResponse('GET', $url, 200, 'admin'); $logoConfig = [ [ - 'href' => "contests/1/organizations/$id/logo", + 'href' => "contests/demo/organizations/$id/logo", 'mime' => 'image/png', 'filename' => 'logo.png', 'width' => 181, diff --git a/webapp/tests/Unit/Controller/API/ProblemControllerAdminTest.php b/webapp/tests/Unit/Controller/API/ProblemControllerAdminTest.php index 96b1562f9c..30247b78b2 100644 --- a/webapp/tests/Unit/Controller/API/ProblemControllerAdminTest.php +++ b/webapp/tests/Unit/Controller/API/ProblemControllerAdminTest.php @@ -5,8 +5,6 @@ use App\DataFixtures\Test\DummyProblemFixture; use App\DataFixtures\Test\LockedContestFixture; use App\Entity\Problem; -use App\Service\ConfigurationService; -use App\Service\DOMJudgeService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -17,9 +15,9 @@ class ProblemControllerAdminTest extends ProblemControllerTest protected function setUp(): void { // When queried as admin, extra information is returned about each problem. - $this->expectedObjects[1]['test_data_count'] = 1; - $this->expectedObjects[2]['test_data_count'] = 1+3; // 1 sample, 3 secret cases - $this->expectedObjects[3]['test_data_count'] = 1; + $this->expectedObjects['boolfind']['test_data_count'] = 1; + $this->expectedObjects['fltcmp']['test_data_count'] = 1+3; // 1 sample, 3 secret cases + $this->expectedObjects['hello']['test_data_count'] = 1; parent::setUp(); } @@ -80,16 +78,8 @@ public function testAddJson(): void $addedProblems = []; // Now load the problems with the given IDs. - $config = static::getContainer()->get(ConfigurationService::class); - $dataSource = $config->get('data_source'); foreach ($ids as $id) { - if ($dataSource === DOMJudgeService::DATA_SOURCE_LOCAL) { - /** @var Problem $problem */ - $problem = static::getContainer()->get(EntityManagerInterface::class)->getRepository(Problem::class)->find($id); - } else { - $problem = static::getContainer()->get(EntityManagerInterface::class)->getRepository(Problem::class)->findOneBy(['externalid' => $id]); - } - + $problem = static::getContainer()->get(EntityManagerInterface::class)->getRepository(Problem::class)->findOneBy(['externalid' => $id]); $addedProblems[$problem->getContestProblems()->first()->getShortName()] = $problem->getExternalid(); } @@ -99,7 +89,7 @@ public function testAddJson(): void public function testDelete(): void { // Check that we can delete the problem - $url = $this->helperGetEndpointURL($this->apiEndpoint) . '/2'; + $url = $this->helperGetEndpointURL($this->apiEndpoint) . '/fltcmp'; $this->verifyApiJsonResponse('DELETE', $url, 204, $this->apiUser); // Check that we now have two problems left @@ -128,6 +118,7 @@ public function testAdd(): void ]; $problemId = $this->resolveReference(DummyProblemFixture::class . ':0'); + $problemId = $this->resolveEntityId(Problem::class, (string)$problemId); // Check that we can not add any problem $url = $this->helperGetEndpointURL($this->apiEndpoint) . '/' . $problemId; @@ -167,7 +158,7 @@ public function testAddExisting(): void $this->loadFixture(DummyProblemFixture::class); // Check that we can not add a problem that is already added - $url = $this->helperGetEndpointURL($this->apiEndpoint) . '/2'; + $url = $this->helperGetEndpointURL($this->apiEndpoint) . '/fltcmp'; $response = $this->verifyApiJsonResponse('PUT', $url, 400, $this->apiUser, ['label' => 'dummy']); self::assertEquals('Problem already linked to contest', $response['message']); } @@ -186,6 +177,7 @@ public function testAddToLocked(): void ]; $problemId = $this->resolveReference(DummyProblemFixture::class . ':0'); + $problemId = $this->resolveEntityId(Problem::class, (string)$problemId); $url = $this->helperGetEndpointURL($this->apiEndpoint) . '/' . $problemId; $problemResponse = $this->verifyApiJsonResponse('PUT', $url, 403, $this->apiUser, $body); @@ -197,7 +189,7 @@ public function testDeleteFromLocked(): void $this->loadFixture(LockedContestFixture::class); // Check that we cannot delete the problem. - $url = $this->helperGetEndpointURL($this->apiEndpoint) . '/2'; + $url = $this->helperGetEndpointURL($this->apiEndpoint) . '/fltcmp'; $problemResponse = $this->verifyApiJsonResponse('DELETE', $url, 403, $this->apiUser); self::assertStringContainsString('Contest is locked', $problemResponse['message']); } diff --git a/webapp/tests/Unit/Controller/API/ProblemControllerTest.php b/webapp/tests/Unit/Controller/API/ProblemControllerTest.php index 85093e9608..dfd3cc8a60 100644 --- a/webapp/tests/Unit/Controller/API/ProblemControllerTest.php +++ b/webapp/tests/Unit/Controller/API/ProblemControllerTest.php @@ -15,55 +15,52 @@ class ProblemControllerTest extends BaseTestCase protected ?string $entityClass = Problem::class; protected array $expectedObjects = [ - 3 => [ + 'boolfind' => [ "ordinal" => 2, - "id" => "3", + "id" => "boolfind", "short_name" => "C", "label" => "C", "time_limit" => 5, - "externalid" => "boolfind", "name" => "Boolean switch search", "rgb" => "#9B630C", "color" => "saddlebrown", "statement" => [ [ - 'href' => 'contests/1/problems/3/statement', + 'href' => 'contests/demo/problems/boolfind/statement', 'mime' => 'application/pdf', 'filename' => 'C.pdf', ], ], ], - 2 => [ + 'fltcmp' => [ "ordinal" => 1, - "id" => "2", + "id" => "fltcmp", "short_name" => "B", "label" => "B", "time_limit" => 5, - "externalid" => "fltcmp", "name" => "Float special compare test", "rgb" => "#E93603", "color" => "orangered", "statement" => [ [ - 'href' => 'contests/1/problems/2/statement', + 'href' => 'contests/demo/problems/fltcmp/statement', 'mime' => 'application/pdf', 'filename' => 'B.pdf', ], ], ], - 1 => [ + 'hello' => [ "ordinal" => 0, - "id" => "1", + "id" => "hello", "short_name" => "A", "label" => "A", "time_limit" => 5, - "externalid" => "hello", "name" => "Hello World", "rgb" => "#9486EA", "color" => "mediumpurple", "statement" => [ [ - 'href' => 'contests/1/problems/1/statement', + 'href' => 'contests/demo/problems/hello/statement', 'mime' => 'application/pdf', 'filename' => 'A.pdf', ], diff --git a/webapp/tests/Unit/Controller/API/SubmissionControllerTest.php b/webapp/tests/Unit/Controller/API/SubmissionControllerTest.php index 71c6d08087..c255ff498c 100644 --- a/webapp/tests/Unit/Controller/API/SubmissionControllerTest.php +++ b/webapp/tests/Unit/Controller/API/SubmissionControllerTest.php @@ -26,16 +26,16 @@ class SubmissionControllerTest extends BaseTestCase protected array $expectedObjects = [ SampleSubmissionsFixture::class . ':0' => [ - 'problem_id' => '1', + 'problem_id' => 'hello', 'language_id' => 'cpp', - 'team_id' => '1', + 'team_id' => 'domjudge', 'entry_point' => null, 'time' => '2021-01-01T12:34:56.000+00:00', ], SampleSubmissionsFixture::class . ':1' => [ - 'problem_id' => '3', + 'problem_id' => 'boolfind', 'language_id' => 'java', - 'team_id' => '2', + 'team_id' => 'exteam', 'entry_point' => 'Main', 'time' => '2021-03-04T12:00:00.000+00:00', ], @@ -94,29 +94,29 @@ public function provideAddInvalidData(): Generator { yield ['demo', [], ""]; yield ['demo', ['unknown_key'], "/One of the arguments 'problem', 'problem_id' is required/"]; - yield ['demo', ['problem' => 1], "/One of the arguments 'language', 'language_id' is required/"]; - yield ['demo', ['problem_id' => 1], "/One of the arguments 'language', 'language_id' is required/"]; - yield ['demo', ['problem_id' => 4, 'language' => 'cpp'], "Problem '4' not found or not submittable."]; - yield ['demo', ['problem_id' => 1, 'language' => 'cpp'], "No files specified."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'cpp'], "No files specified."]; - yield ['demo', ['problem_id' => 1, 'language' => 'abc'], "Language 'abc' not found or not submittable."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'abc'], "Language 'abc' not found or not submittable."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'abc'], "Language 'abc' not found or not submittable."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'cpp', 'team_id' => '1'], "Can not submit for a different team."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'cpp', 'user_id' => 1], "Can not submit for a different user."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'cpp', 'id' => '123'], "A team can not assign id."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'cpp', 'time' => '2021-01-01T00:00:00'], "A team can not assign time."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'cpp', 'files' => []], "No files specified."]; - yield ['demo', ['problem_id' => 1, 'language_id' => 'cpp', 'files' => [['invalidkey' => 'somevalue']]], "/files\[0\].data:\n.*This value should be of type unknown./"]; + yield ['demo', ['problem' => 'hello'], "/One of the arguments 'language', 'language_id' is required/"]; + yield ['demo', ['problem_id' => 'hello'], "/One of the arguments 'language', 'language_id' is required/"]; + yield ['demo', ['problem_id' => 'noprob', 'language' => 'cpp'], "Problem 'noprob' not found or not submittable."]; + yield ['demo', ['problem_id' => 'hello', 'language' => 'cpp'], "No files specified."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'cpp'], "No files specified."]; + yield ['demo', ['problem_id' => 'hello', 'language' => 'abc'], "Language 'abc' not found or not submittable."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'abc'], "Language 'abc' not found or not submittable."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'abc'], "Language 'abc' not found or not submittable."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'cpp', 'team_id' => 'domjudge'], "Can not submit for a different team."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'cpp', 'user_id' => 'admin'], "Can not submit for a different user."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'cpp', 'id' => '123'], "A team can not assign id."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'cpp', 'time' => '2021-01-01T00:00:00'], "A team can not assign time."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'cpp', 'files' => []], "No files specified."]; + yield ['demo', ['problem_id' => 'hello', 'language_id' => 'cpp', 'files' => [['invalidkey' => 'somevalue']]], "/files\[0\].data:\n.*This value should be of type string./"]; yield [ 'demo', - ['problem_id' => 1, 'language_id' => 'cpp', 'files' => 'this is not an array'], + ['problem_id' => 'hello', 'language_id' => 'cpp', 'files' => 'this is not an array'], "/files:\n.*This value should be of type array/" ]; yield [ 'demo', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', 'files' => [ // More than one item @@ -129,7 +129,7 @@ public function provideAddInvalidData(): Generator yield [ 'demo', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', 'files' => [ // Not valid base64 @@ -141,19 +141,19 @@ public function provideAddInvalidData(): Generator yield [ 'demo', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', 'files' => [ // Valid base64, but not a ZIP file ['data' => 'aaa'], ], ], - "No valid zip archive given" + "No valid ZIP archive given." ]; yield [ 'demo', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', 'files' => [ ['data' => 'aaa', 'mime' => 'wrong'], @@ -161,19 +161,19 @@ public function provideAddInvalidData(): Generator ], "The 'files[0].mime' attribute must be application/zip if provided." ]; - yield ['admin', ['problem_id' => 1, 'language' => 'cpp', 'team_id' => '1'], "No files specified."]; - yield ['admin', ['problem_id' => 1, 'language' => 'cpp', 'team_id' => '3'], "Team with ID '3' not found in contest or not enabled."]; - yield ['admin', ['problem_id' => 1, 'language' => 'cpp', 'team_id' => '1', 'user_id' => 'doesnotexist'], "User not found."]; - yield ['admin', ['problem_id' => 1, 'language' => 'cpp', 'team_id' => '1', 'user_id' => AddMoreDemoUsersFixture::class . ':seconddemo'], "User not linked to provided team."]; - yield ['admin', ['problem_id' => 1, 'language' => 'cpp', 'team_id' => '1', 'user_id' => AddMoreDemoUsersFixture::class . ':thirddemo'], "User not enabled."]; - yield ['admin', ['problem_id' => 1, 'language' => 'cpp', 'team_id' => '1', 'user_id' => AddMoreDemoUsersFixture::class . ':fourthdemo'], "User not linked to a team."]; - yield ['admin', ['problem_id' => 1, 'language' => 'cpp', 'team_id' => '1', 'time' => 'this is not a time'], "Can not parse time 'this is not a time'."]; + yield ['admin', ['problem_id' => 'hello', 'language' => 'cpp', 'team_id' => 'domjudge'], "No files specified."]; + yield ['admin', ['problem_id' => 'hello', 'language' => 'cpp', 'team_id' => 'noteam'], "Team with ID 'noteam' not found in contest or not enabled."]; + yield ['admin', ['problem_id' => 'hello', 'language' => 'cpp', 'team_id' => 'domjudge', 'user_id' => 'doesnotexist'], "User not found."]; + yield ['admin', ['problem_id' => 'hello', 'language' => 'cpp', 'team_id' => 'domjudge', 'user_id' => AddMoreDemoUsersFixture::class . ':seconddemo'], "User not linked to provided team."]; + yield ['admin', ['problem_id' => 'hello', 'language' => 'cpp', 'team_id' => 'domjudge', 'user_id' => AddMoreDemoUsersFixture::class . ':thirddemo'], "User not enabled."]; + yield ['admin', ['problem_id' => 'hello', 'language' => 'cpp', 'team_id' => 'domjudge', 'user_id' => AddMoreDemoUsersFixture::class . ':fourthdemo'], "User not linked to a team."]; + yield ['admin', ['problem_id' => 'hello', 'language' => 'cpp', 'team_id' => 'domjudge', 'time' => 'this is not a time'], "Can not parse time 'this is not a time'."]; yield [ 'admin', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', - 'team_id' => '1', + 'team_id' => 'domjudge', 'id' => '$this is not valid$', ], "ID '\$this is not valid$' is not valid.", @@ -332,7 +332,7 @@ public function provideAddSuccess(): Generator yield [ 'demo', [ - 'problem' => 1, + 'problem' => 'hello', 'language' => 'cpp', ], null, @@ -350,7 +350,7 @@ public function provideAddSuccess(): Generator yield [ 'demo', [ - 'problem' => 1, + 'problem' => 'hello', 'language' => 'cpp', ], null, @@ -376,7 +376,7 @@ public function provideAddSuccess(): Generator yield [ 'demo', [ - 'problem' => 1, + 'problem' => 'hello', 'language' => 'kotlin', 'entry_point' => 'SomeFileKt', ], @@ -400,7 +400,7 @@ public function provideAddSuccess(): Generator yield [ 'demo', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', ], ['main.cpp' => '// No content'], @@ -418,7 +418,7 @@ public function provideAddSuccess(): Generator yield [ 'demo', [ - 'problem_id' => 2, + 'problem_id' => 'fltcmp', 'language_id' => 'java', 'files' => [ ['mime' => 'application/zip'], @@ -445,9 +445,9 @@ public function provideAddSuccess(): Generator yield [ 'admin', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', - 'team_id' => '2', + 'team_id' => 'exteam', ], ['main.cpp' => '// No content'], [], @@ -464,9 +464,9 @@ public function provideAddSuccess(): Generator yield [ 'admin', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', - 'team_id' => '2', + 'team_id' => 'exteam', 'user_id' => AddMoreDemoUsersFixture::class . ':seconddemo', ], ['main.cpp' => '// No content'], @@ -484,9 +484,9 @@ public function provideAddSuccess(): Generator yield [ 'admin', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', - 'team_id' => '1', + 'team_id' => 'domjudge', 'id' => 'myextid123', ], ['main.cpp' => '// No content'], @@ -504,9 +504,9 @@ public function provideAddSuccess(): Generator yield [ 'admin', [ - 'problem_id' => 1, + 'problem_id' => 'hello', 'language_id' => 'cpp', - 'team_id' => '1', + 'team_id' => 'domjudge', 'time' => '2020-01-01T12:34:56', ], ['main.cpp' => '// No content'], diff --git a/webapp/tests/Unit/Controller/API/TeamControllerTest.php b/webapp/tests/Unit/Controller/API/TeamControllerTest.php index 9101e9e324..9fb775e9ba 100644 --- a/webapp/tests/Unit/Controller/API/TeamControllerTest.php +++ b/webapp/tests/Unit/Controller/API/TeamControllerTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Unit\Controller\API; use App\DataFixtures\Test\AddLocationToTeamFixture; +use App\Entity\Team; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -11,12 +12,12 @@ class TeamControllerTest extends BaseTestCase protected ?string $apiEndpoint = 'teams'; protected array $expectedObjects = [ - '2' => [ - 'organization_id' => '1', - 'group_ids' => ['3'], + 'exteam' => [ + 'organization_id' => 'utrecht', + 'group_ids' => ['participants'], 'affiliation' => 'Utrecht University', 'nationality' => 'NLD', - 'id' => '2', + 'id' => 'exteam', 'icpc_id' => 'exteam', 'name' => 'Example teamname', 'display_name' => null, @@ -30,6 +31,8 @@ class TeamControllerTest extends BaseTestCase protected array $expectedAbsent = ['4242', 'nonexistent']; + protected ?string $objectClassForExternalId = Team::class; + public function testLogoManagement(): void { // Note: we are doing this as admin as we require privileges @@ -52,7 +55,7 @@ public function testLogoManagement(): void $object = $this->verifyApiJsonResponse('GET', $url, 200, 'admin'); $logoConfig = [ [ - 'href' => "contests/1/teams/$id/photo", + 'href' => "contests/demo/teams/$id/photo", 'mime' => 'image/jpeg', 'filename' => 'photo.jpg', 'width' => 320, diff --git a/webapp/tests/Unit/Controller/API/UserControllerTest.php b/webapp/tests/Unit/Controller/API/UserControllerTest.php index 1676b44223..fc92392a2e 100644 --- a/webapp/tests/Unit/Controller/API/UserControllerTest.php +++ b/webapp/tests/Unit/Controller/API/UserControllerTest.php @@ -9,37 +9,37 @@ class UserControllerTest extends AccountBaseTestCase protected ?string $apiEndpoint = 'users'; protected array $expectedObjects = [ - 1 => [ + 'admin' => [ "team" => "DOMjudge", "roles" => [ "admin", "team" ], - "id" => "1", + "id" => "admin", "username" => "admin", "name" => "Administrator", "email" => null, "ip" => null, "enabled" => true ], - 2 => [ + 'judgehost' => [ "team" => null, "roles" => [ "judgehost" ], - "id" => "2", + "id" => "judgehost", "username" => "judgehost", "name" => "User for judgedaemons", "email" => null, "ip" => null, "enabled" => true ], - 3 => [ + 'demo' => [ "team" => "Example teamname", "roles" => [ "team" ], - "id" => "3", + "id" => "demo", "username" => "demo", "name" => "demo user for example team", "email" => null, @@ -64,9 +64,8 @@ public function testAddLocal(): void static::assertEquals(['team'], $response['roles']); } - public function testUpdateNonLocal(): void + public function testUpdate(): void { - $this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL); $data = [ 'id' => 'someid', 'username' => 'testuser', @@ -82,9 +81,8 @@ public function testUpdateNonLocal(): void static::assertEquals(['team'], $response['roles']); } - public function testUpdateNonLocalNoId(): void + public function testUpdateNoId(): void { - $this->setupDataSource(DOMJudgeService::DATA_SOURCE_CONFIGURATION_EXTERNAL); $data = [ 'username' => 'testuser', 'name' => 'Test User', @@ -93,6 +91,6 @@ public function testUpdateNonLocalNoId(): void ]; $response = $this->verifyApiJsonResponse('PUT', $this->helperGetEndpointURL($this->apiEndpoint) . '/someid', 400, 'admin', $data); - static::assertMatchesRegularExpression('/id:\n.*This value should be of type unknown./', $response['message']); + static::assertMatchesRegularExpression('/id:\n.*This value should be of type string./', $response['message']); } } diff --git a/webapp/tests/Unit/Controller/Jury/ContestControllerTest.php b/webapp/tests/Unit/Controller/Jury/ContestControllerTest.php index 78bbc1aeb8..a3b0fba1cf 100644 --- a/webapp/tests/Unit/Controller/Jury/ContestControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/ContestControllerTest.php @@ -237,15 +237,8 @@ class ContestControllerTest extends JuryControllerTestCase 'lazyEvalResults' => '0']], 'name' => 'No known problem']], 'Only alphanumeric characters and ._- are allowed' => [['shortname' => '"quoted"']]]; - protected static array $addEntitiesFailureNonLocal = ['This value should not be blank.' => [['externalid' => '', 'name' => 'Empty externalid']]]; - protected function helperProvideTranslateAddEntity(array $entity, array $expected): array { - // Add external ID's when needed. - if (!$this->dataSourceIsLocal()) { - $entity['externalid'] = $entity['shortname']; - $expected['externalid'] = $entity['shortname']; - } return [$entity, $expected]; } @@ -390,7 +383,6 @@ public function testLockedContest(): void 'Edit', 'Delete', 'Lock', - 'Finalize this contest', 'Judge remaining testcases', 'Heat up judgehosts with contest data', ]; diff --git a/webapp/tests/Unit/Controller/Jury/ImportExportControllerTest.php b/webapp/tests/Unit/Controller/Jury/ImportExportControllerTest.php index d061d4f517..c81d532efa 100644 --- a/webapp/tests/Unit/Controller/Jury/ImportExportControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/ImportExportControllerTest.php @@ -23,10 +23,6 @@ public function testIndexBasic(): void self::assertSelectorExists(sprintf('h2:contains("%s")', $section)); } self::assertSelectorExists('div.help-text:contains(\'Create a "Web Services Token"\')'); - - // We've reached the end of the page. - self::assertSelectorExists('li:contains("wf_results.tsv")'); - self::assertSelectorExists('li:contains("full_results.tsv")'); } /** @@ -42,18 +38,6 @@ public function testIndexContestDropdowns(string $contest): void self::assertSelectorExists(sprintf('select#contest_export_contest > option:contains("%s")', $contest)); } - /** - * Test that the expected dynamic items on the index page are present. - * - * @dataProvider provideSortOrders - */ - public function testIndexGeneratedItems(string $sortOrder): void - { - $this->verifyPageResponse('GET', '/jury/import-export', 200); - - self::assertSelectorExists(sprintf('li:contains("for sort order %s")', $sortOrder)); - } - public function provideContests(): Generator { yield ['Demo contest']; @@ -102,6 +86,10 @@ public function provideContestYamlContents(): Generator duration: '5:00:00.000' penalty_time: 20 activate_time: '{$pastYear}-01-01T08:00:00+00:00' +medals: + gold: 4 + silver: 4 + bronze: 4 scoreboard_freeze_time: '{$year}-01-01T12:00:00+00:00' scoreboard_freeze_duration: '1:00:00' problems: @@ -148,22 +136,12 @@ public function testGroupsTeamsTsvExport(string $linkname, string $expectedData) public function provideTsvContents(): Generator { yield ['a:contains("teams.tsv")', 'teams 1 -2 exteam 3 Example teamname Utrecht University UU NLD utrecht -']; - yield ['li:contains("wf_results.tsv") a:contains("for sort order 0")', 'results 1 -exteam 1 Gold Medal 0 0 0 Participants -']; - yield ['li:contains("wf_results.tsv") a:contains("for sort order 1")', 'results 1 -']; - yield ['li:contains("full_results.tsv") a:contains("for sort order 0")', 'results 1 -exteam 1 Gold Medal 0 0 0 Participants -']; - yield ['li:contains("full_results.tsv") a:contains("for sort order 1")', 'results 1 +exteam exteam participants Example teamname Utrecht University UU NLD utrecht ']; yield ['a:contains("groups.tsv")', 'groups 1 -2 Self-Registered -3 Participants -4 Observers +self-registered Self-Registered +participants Participants +observers Observers ']; } @@ -184,30 +162,81 @@ public function testClarificationsHtmlExport(): void } /** - * Test export of wf_results.html. + * Test export of results.html + * + * @dataProvider provideResultsHtmlExport */ - public function testWfResultsHtmlExport(): void + public function testResultsHtmlExport(bool $individuallyRanked, bool $honors, string $format): void { $this->loadFixture(ClarificationFixture::class); $this->verifyPageResponse('GET', '/jury/import-export', 200); - $link = $this->getCurrentCrawler()->filter('li:contains("wf_results.html") a:contains("for sort order 0")')->link(); - $this->client->click($link); + $this->client->submitForm('export_results_export', [ + 'export_results[sortorder]' => 0, + 'export_results[individually_ranked]' => (int)$individuallyRanked, + 'export_results[honors]' => (int)$honors, + 'export_results[format]' => $format, + ]); self::assertSelectorExists('h1:contains("Results for Demo contest")'); - self::assertSelectorExists('th:contains("Example teamname")'); self::assertSelectorExists('th:contains("A: Hello World")'); + self::assertSelectorExists('td:contains("Example teamname")'); + } + + public function provideResultsHtmlExport(): Generator + { + yield [true, true, 'html_inline']; + yield [true, false, 'html_inline']; + yield [false, true, 'html_inline']; + yield [false, true, 'html_inline']; + yield [true, true, 'html_download']; + yield [true, false, 'html_download']; + yield [false, true, 'html_download']; + yield [false, true, 'html_download']; } /** - * Test export of full_results.html. + * Test export of results.tsv + * + * @dataProvider provideResultsTsvExport */ - public function testFullResultsHtmlExport(): void - { + public function testResultsTsvExport( + int $sortOrder, + bool $individuallyRanked, + bool $honors, + string $expectedData + ): void { $this->loadFixture(ClarificationFixture::class); $this->verifyPageResponse('GET', '/jury/import-export', 200); - $link = $this->getCurrentCrawler()->filter('li:contains("full_results.html") a:contains("for sort order 0")')->link(); - $this->client->click($link); - self::assertSelectorExists('h1:contains("Results for Demo contest")'); - self::assertSelectorExists('th:contains("Example teamname")'); - self::assertSelectorExists('th:contains("A: Hello World")'); + $this->client->submitForm('export_results_export', [ + 'export_results[sortorder]' => $sortOrder, + 'export_results[individually_ranked]' => (int)$individuallyRanked, + 'export_results[honors]' => (int)$honors, + 'export_results[format]' => 'tsv', + ]); + + static::assertEquals($expectedData, $this->client->getInternalResponse()->getContent()); + } + + public function provideResultsTsvExport(): Generator + { + yield [0, true, true, 'results 1 +exteam Honorable 0 0 0 +']; + yield [0, true, false, 'results 1 +exteam Honorable 0 0 0 +']; + yield [0, false, true, 'results 1 +exteam Honorable 0 0 0 +']; + yield [0, false, true, 'results 1 +exteam Honorable 0 0 0 +']; + yield [1, true, true, 'results 1 +']; + yield [1, true, false, 'results 1 +']; + yield [1, false, true, 'results 1 +']; + yield [1, false, true, 'results 1 +']; } } diff --git a/webapp/tests/Unit/Controller/Jury/JuryControllerTestCase.php b/webapp/tests/Unit/Controller/Jury/JuryControllerTestCase.php index 3d58e4690e..9293cd4248 100644 --- a/webapp/tests/Unit/Controller/Jury/JuryControllerTestCase.php +++ b/webapp/tests/Unit/Controller/Jury/JuryControllerTestCase.php @@ -49,8 +49,6 @@ abstract class JuryControllerTestCase extends BaseTestCase protected static array $addEntitiesCount = []; protected static array $addEntitiesShown = []; protected static array $addEntitiesFailure = []; - protected static array $addEntitiesNonLocal = []; - protected static array $addEntitiesFailureNonLocal = []; protected static ?string $defaultEditEntityName = null; protected static array $specialFieldOnlyUpdate = []; protected static array $editEntitiesSkipFields = []; @@ -455,9 +453,6 @@ public function testCheckEditEntityAdminFailure(array $formDataKeys, array $form public function provideAddCorrectEntities(): Generator { $entities = static::$addEntities; - if (!$this->dataSourceIsLocal()) { - $entities = [...$entities, ...static::$addEntitiesNonLocal]; - } foreach ($entities as $element) { [$combinedValues, $element] = $this->helperProvideMergeAddEntity($element); [$combinedValues, $element] = $this->helperProvideTranslateAddEntity($combinedValues, $element); @@ -468,9 +463,6 @@ public function provideAddCorrectEntities(): Generator public function provideAddFailureEntities(): Generator { $entities = static::$addEntitiesFailure; - if (!$this->dataSourceIsLocal()) { - $entities = [...$entities, ...static::$addEntitiesFailureNonLocal]; - } foreach ($entities as $message => $elementList) { foreach ($elementList as $element) { [$entity, $expected] = $this->helperProvideMergeAddEntity($element); @@ -534,7 +526,7 @@ public function provideEditFailureEntities(): Generator if (in_array(array_key_first($element), static::$editEntitiesSkipFields)) { continue; } - if (key_exists('externalid', $element) && $this->dataSourceIsLocal()) { + if (key_exists('externalid', $element)) { continue; } [$formdataKeys, $formdataValues] = $this->helperProvideMergeEditEntity($element); 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 { diff --git a/webapp/tests/Unit/Controller/Jury/LanguagesControllerTest.php b/webapp/tests/Unit/Controller/Jury/LanguagesControllerTest.php index 4a835be2cc..467a90f44f 100644 --- a/webapp/tests/Unit/Controller/Jury/LanguagesControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/LanguagesControllerTest.php @@ -74,14 +74,13 @@ class LanguagesControllerTest extends JuryControllerTestCase 'runnerVersionCommand' => 'run -x |yes|tr "\n" "\`true\`"']]; protected static array $addEntitiesFailure = ['Only alphanumeric characters and ._- are allowed' => [['langid' => '§$#`"'], ['langid' => '()*&']], - 'Only letters, numbers, dashes, underscores and dots are allowed' => [['externalid' => '§$#'], + 'Only letters, numbers, dashes, underscores and dots are allowed.' => [['externalid' => '§$#'], ['externalid' => '@#()|']], 'This value should be positive.' => [['timeFactor' => '0'], ['timeFactor' => '-1'], ['timeFactor' => '-.1']], 'This value should not be blank.' => [['langid' => ''], - ['name' => ''], - ['externalid' => '']]]; + ['name' => '']]]; public function helperProvideTranslateAddEntity(array $entity, array $expected): array { diff --git a/webapp/tests/Unit/Controller/Jury/ProblemControllerTest.php b/webapp/tests/Unit/Controller/Jury/ProblemControllerTest.php index ba69e995e1..ddf9504f0f 100644 --- a/webapp/tests/Unit/Controller/Jury/ProblemControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/ProblemControllerTest.php @@ -56,6 +56,7 @@ class ProblemControllerTest extends JuryControllerTestCase 'combinedRunCompare' => false], ['externalid' => '._-3xternal1']]; protected static array $addEntitiesFailure = ['This value should not be blank.' => [['name' => '']], + 'Only letters, numbers, dashes, underscores and dots are allowed.' => [['externalid' => 'limited_special_chars!']], // This is a placeholder on the Add/Edit page 'leave empty for default' => [['memlimit' => 'zero'], ['timelimit' => 'zero'], @@ -63,18 +64,9 @@ class ProblemControllerTest extends JuryControllerTestCase ['memlimit' => '-1'], ['timelimit' => '-1'], ['outputlimit' => '-1']]]; - protected static array $addEntitiesFailureNonLocal = ['Only letters, numbers, dashes, underscores and dots are allowed' => [['externalid' => 'limited_special_chars!']]]; public function helperProvideTranslateAddEntity(array $entity, array $expected): array { - // Add external IDs when needed. - if (!$this->dataSourceIsLocal()) { - $entity['externalid'] = md5(json_encode($entity, JSON_THROW_ON_ERROR)); - $expected['externalid'] = md5(json_encode($entity, JSON_THROW_ON_ERROR)); - } else { - unset($entity['externalid']); - unset($expected['externalid']); - } return [$entity, $expected]; } diff --git a/webapp/tests/Unit/Controller/Jury/QueueTaskControllerTest.php b/webapp/tests/Unit/Controller/Jury/QueueTaskControllerTest.php index 56f29e01d2..0cc32fd6cf 100644 --- a/webapp/tests/Unit/Controller/Jury/QueueTaskControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/QueueTaskControllerTest.php @@ -130,11 +130,11 @@ public function testData(): void } /** - * @dataProvider provideLazyDataSource + * @dataProvider provideLazyShadowMode */ - public function testLazy(int $dataSource, int $globalLazy, int $problemLazy): void + public function testLazy(bool $shadowMode, int $globalLazy, int $problemLazy): void { - $this->setupDatasource($dataSource); + $this->setupShadowMode($shadowMode); $contest = $this->em->getRepository(Contest::class)->findOneBy(['shortname' => 'demo']); $problem = $this->em->getRepository(Problem::class)->findOneBy(['externalid' => 'hello']); $hello = $this->em->getRepository(ContestProblem::class)->find( @@ -144,7 +144,7 @@ public function testLazy(int $dataSource, int $globalLazy, int $problemLazy): vo $config = self::getContainer()->get(ConfigurationService::class); $eventLog = self::getContainer()->get(EventLogService::class); $dj = self::getContainer()->get(DOMJudgeService::class); - $config->saveChanges(['lazy_eval_results'=>$globalLazy], $eventLog, $dj); + $config->saveChanges(['lazy_eval_results'=>$globalLazy], $eventLog, $dj, treatMissingBooleansAsFalse: false); $this->roles = ['admin']; $this->logOut(); @@ -159,7 +159,7 @@ public function testLazy(int $dataSource, int $globalLazy, int $problemLazy): vo $tableBody = $crawler->filter('table.data-table.table.table-sm.table-striped tbody'); $expectedNumberQueueItems = 4; // In case we shadow we judge all local submissions to keep analyst working. - if ($dataSource !== DOMJudgeService::DATA_SOURCE_CONFIGURATION_AND_LIVE_EXTERNAL) { + if (!$shadowMode) { if ($globalLazy === DOMJudgeService::EVAL_DEMAND) { if ($problemLazy === DOMJudgeService::EVAL_DEMAND || (int)$problemLazy === (int)DOMJudgeService::EVAL_DEFAULT) { $expectedNumberQueueItems = 0; @@ -299,17 +299,17 @@ protected function verifyJudgetaskPage(Submission $submission): void } } - public function provideLazyDataSource(): Generator + public function provideLazyShadowMode(): Generator { - ['dataSources' => $dataSources] = $this->getDatasourceLoops(); - foreach ($dataSources as $str_data_source) { - $dataSource = (int)$str_data_source; + ['shadowModes' => $shadowModes] = $this->getShadowModeLoops(); + foreach ($shadowModes as $str_shadow_mode) { + $shadowMode = (bool)$str_shadow_mode; foreach ([DOMJudgeService::EVAL_DEMAND, DOMJudgeService::EVAL_FULL] as $globalLazy) { foreach ([(int)DOMJudgeService::EVAL_DEFAULT, DOMJudgeService::EVAL_DEMAND, DOMJudgeService::EVAL_FULL] as $problemLazy) { - yield [$dataSource, $globalLazy, $problemLazy]; + yield [$shadowMode, $globalLazy, $problemLazy]; } } } diff --git a/webapp/tests/Unit/Controller/Jury/TeamAffiliationControllerTest.php b/webapp/tests/Unit/Controller/Jury/TeamAffiliationControllerTest.php index 07d99e4b4a..7903adf473 100644 --- a/webapp/tests/Unit/Controller/Jury/TeamAffiliationControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/TeamAffiliationControllerTest.php @@ -40,14 +40,14 @@ class TeamAffiliationControllerTest extends JuryControllerTestCase ['name' => 'icpc (alpnum-_)', 'icpcid' => '-_1aZ'], ['name' => 'Special chars 😀', - 'shortname' => 'yes😀']]; - protected static array $addEntitiesNonLocal = [['name' => 'External set', 'externalid' => 'ext12-._']]; + 'shortname' => 'yes😀'], + ['name' => 'External set', + 'externalid' => 'ext12-._']]; protected static array $addEntitiesFailure = ['This value should not be blank.' => [['shortname' => ''], ['name' => '']], 'Only letters, numbers, dashes and underscores are allowed.' => [['icpcid' => '()viol'], - ['icpcid' => '|viol']]]; - protected static array $addEntitiesFailureNonLocal = ['This value should not be blank.' => [['externalid' => '']], - 'Only letters, numbers, dashes, underscores and dots are allowed' => [['externalid' => '()']]]; + ['icpcid' => '|viol']], + 'Only letters, numbers, dashes, underscores and dots are allowed.' => [['externalid' => '()']]]; protected function helperProvideTranslateAddEntity(array $entity, array $expected): array { @@ -58,14 +58,6 @@ protected function helperProvideTranslateAddEntity(array $entity, array $expecte unset($entity['country']); unset($expected['country']); } - // Remove/Add external ID's when needed - if ($this->dataSourceIsLocal()) { - unset($entity['externalid']); - unset($expected['externalid']); - } else { - $entity['externalid'] = $entity['shortname']; - $expected['externalid'] = $entity['shortname']; - } return [$entity, $expected]; } } diff --git a/webapp/tests/Unit/Controller/Jury/TeamCategoryControllerTest.php b/webapp/tests/Unit/Controller/Jury/TeamCategoryControllerTest.php index f6596baabc..b2478852c9 100644 --- a/webapp/tests/Unit/Controller/Jury/TeamCategoryControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/TeamCategoryControllerTest.php @@ -52,17 +52,11 @@ class TeamCategoryControllerTest extends JuryControllerTestCase protected static array $addEntitiesFailure = ['Only non-negative sortorders are supported' => [['sortorder' => '-10']], 'Only letters, numbers, dashes and underscores are allowed.' => [['icpcid' => '|violation', 'name' => 'ICPCid violation-1'], ['icpcid' => '()violation', 'name' => 'ICPCid violation-2']], + 'Only letters, numbers, dashes, underscores and dots are allowed.' => [['externalid' => 'yes|']], 'This value should not be blank.' => [['name' => '']]]; - protected static array $addEntitiesFailureNonLocal = ['Only letters, numbers, dashes, underscores and dots are allowed' => [['externalid' => 'yes|']], - 'This value should not be blank.' => [['externalid' => '']]]; protected function helperProvideTranslateAddEntity(array $entity, array $expected): array { - // Remove external ID's when we're local - if ($this->dataSourceIsLocal()) { - unset($entity['externalid']); - unset($expected['externalid']); - } return [$entity, $expected]; } } diff --git a/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php b/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php index c90d4e8a01..89d30a05fd 100644 --- a/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php @@ -3,6 +3,8 @@ namespace App\Tests\Unit\Controller\Jury; use App\Entity\Team; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; class TeamControllerTest extends JuryControllerTestCase { @@ -91,4 +93,35 @@ class TeamControllerTest extends JuryControllerTestCase 'Only letters, numbers, dashes and underscores are allowed.' => [['icpcid' => '|viol', 'name' => 'icpcid violation-1'], ['icpcid' => '&viol', 'name' => 'icpcid violation-2']], 'This value should not be blank.' => [['name' => '', 'displayName' => 'Teams should have a name']]]; + + /** + * Test that adding a team without a user and then editing it to add a user works. + */ + public function testAddWithoutUserThenEdit(): void + { + $teamToAdd = static::$addEntities[0]; + $this->roles = ['admin']; + $this->logOut(); + $this->logIn(); + $this->verifyPageResponse('GET', static::$baseUrl, 200); + $this->helperSubmitFields($teamToAdd); + $viewPage = $this->client->followRedirect()->getUri(); + $editPage = $viewPage . static::$edit; + $this->verifyPageResponse('GET', $editPage, 200); + $formFields = [ + static::$addForm . 'addUserForTeam]' => Team::CREATE_NEW_USER, + static::$addForm . 'newUsername]' => 'somelinkeduser', + ]; + $button = $this->client->getCrawler()->selectButton('Save'); + $form = $button->form($formFields, 'POST'); + $this->client->submit($form); + self::assertNotEquals(500, $this->client->getResponse()->getStatusCode()); + + /** @var EntityManagerInterface $em */ + $em = $this->getContainer()->get(EntityManagerInterface::class); + $user = $em->getRepository(User::class)->findOneBy(['username' => 'somelinkeduser']); + + static::assertNotNull($user); + static::assertEquals('New Team', $user->getTeam()->getName()); + } } diff --git a/webapp/tests/Unit/Controller/Jury/UserControllerTest.php b/webapp/tests/Unit/Controller/Jury/UserControllerTest.php index d041be8d96..3e6d6ce6c4 100644 --- a/webapp/tests/Unit/Controller/Jury/UserControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/UserControllerTest.php @@ -89,5 +89,4 @@ class UserControllerTest extends JuryControllerTestCase ['ipAddress' => '1.1.1.256'], ['ipAddress' => '1.1.1.1.1'], ['ipAddress' => '::g']]]; - protected static array $addEntitiesFailureNonLocal = ['This value should not be blank.' => [['externalid' => '', 'name' => 'Empty externalid (Skipped when datasource=0)']]]; } diff --git a/webapp/tests/Unit/Fixtures/sample/results-full-honors.tsv b/webapp/tests/Unit/Fixtures/sample/results-full-honors.tsv new file mode 100644 index 0000000000..44c8950ffd --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/results-full-honors.tsv @@ -0,0 +1,10 @@ +results 1 +10001 1 Gold Medal 4 36 20 Sample Group A +10002 2 Gold Medal 2 5 3 +10003 2 Gold Medal 2 5 3 +10004 4 Highest Honors 1 12 12 Sample Group B +10005 5 Highest Honors 1 13 13 +10006 6 Highest Honors 1 14 14 +10007 7 Bronze Medal 1 15 15 Sample Group C +10008 Honorable 0 0 0 +10009 Honorable 0 0 0 diff --git a/webapp/tests/Unit/Fixtures/sample/results-full-ranked.tsv b/webapp/tests/Unit/Fixtures/sample/results-full-ranked.tsv new file mode 100644 index 0000000000..02167ced5a --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/results-full-ranked.tsv @@ -0,0 +1,10 @@ +results 1 +10001 1 Gold Medal 4 36 20 Sample Group A +10002 2 Gold Medal 2 5 3 +10003 2 Gold Medal 2 5 3 +10004 4 Ranked 1 12 12 Sample Group B +10005 5 Ranked 1 13 13 +10006 6 Ranked 1 14 14 +10007 7 Bronze Medal 1 15 15 Sample Group C +10008 Honorable 0 0 0 +10009 Honorable 0 0 0 diff --git a/webapp/tests/Unit/Fixtures/sample/results-wf-honors.tsv b/webapp/tests/Unit/Fixtures/sample/results-wf-honors.tsv new file mode 100644 index 0000000000..e1b6deee07 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/results-wf-honors.tsv @@ -0,0 +1,10 @@ +results 1 +10001 1 Gold Medal 4 36 20 Sample Group A +10002 2 Gold Medal 2 5 3 +10003 2 Gold Medal 2 5 3 +10004 4 Highest Honors 1 12 12 Sample Group B +10005 4 Highest Honors 1 13 13 +10006 4 Highest Honors 1 14 14 +10007 7 Bronze Medal 1 15 15 Sample Group C +10008 Honorable 0 0 0 +10009 Honorable 0 0 0 diff --git a/webapp/tests/Unit/Fixtures/sample/results-wf-ranked.tsv b/webapp/tests/Unit/Fixtures/sample/results-wf-ranked.tsv new file mode 100644 index 0000000000..1b4985f273 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/results-wf-ranked.tsv @@ -0,0 +1,10 @@ +results 1 +10001 1 Gold Medal 4 36 20 Sample Group A +10002 2 Gold Medal 2 5 3 +10003 2 Gold Medal 2 5 3 +10004 4 Ranked 1 12 12 Sample Group B +10005 4 Ranked 1 13 13 +10006 4 Ranked 1 14 14 +10007 7 Bronze Medal 1 15 15 Sample Group C +10008 Honorable 0 0 0 +10009 Honorable 0 0 0 diff --git a/webapp/tests/Unit/Fixtures/sample/sample-groups.json b/webapp/tests/Unit/Fixtures/sample/sample-groups.json new file mode 100644 index 0000000000..9a1b000069 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/sample-groups.json @@ -0,0 +1,32 @@ +[ + { + "hidden": false, + "categoryid": 5, + "id": "10001", + "icpc_id": "10001", + "name": "Sample Group A", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 6, + "id": "10002", + "icpc_id": "10002", + "name": "Sample Group B", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 7, + "id": "10003", + "icpc_id": "10003", + "name": "Sample Group C", + "sortorder": 0, + "color": null, + "allow_self_registration": false + } +] diff --git a/webapp/tests/Unit/Fixtures/sample/sample-medals.json b/webapp/tests/Unit/Fixtures/sample/sample-medals.json new file mode 100644 index 0000000000..6336011001 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/sample-medals.json @@ -0,0 +1,11 @@ +{ + "medals": { + "gold": 2, + "silver": 1, + "bronze": 1 + }, + "medal_categories": [ + "10001", + "10003" + ] +} diff --git a/webapp/tests/Unit/Fixtures/sample/sample-problems.json b/webapp/tests/Unit/Fixtures/sample/sample-problems.json new file mode 100644 index 0000000000..9b851ebe06 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/sample-problems.json @@ -0,0 +1,78 @@ +[ + { + "ordinal": 0, + "probid": 1, + "short_name": "A", + "rgb": "#FF0000", + "color": "red", + "label": "A", + "time_limit": 1.0, + "statement": [ + { + "href": "contests/sample_contests/problems/sample-problem-a/statement", + "mime": "application/pdf", + "filename": "A.pdf" + } + ], + "id": "sample-problem-a", + "name": "Sample Problem A", + "test_data_count": 50 + }, + { + "ordinal": 1, + "probid": 2, + "short_name": "B", + "rgb": "#00FF00", + "color": "lime", + "label": "B", + "time_limit": 2.0, + "statement": [ + { + "href": "contests/sample_contests/problems/sample-problem-b/statement", + "mime": "application/pdf", + "filename": "B.pdf" + } + ], + "id": "sample-problem-b", + "name": "Sample Problem B", + "test_data_count": 32 + }, + { + "ordinal": 2, + "probid": 3, + "short_name": "C", + "rgb": "#0000FF", + "color": "blue", + "label": "C", + "time_limit": 3.0, + "statement": [ + { + "href": "contests/sample_contests/problems/sample-problem-c/statement", + "mime": "application/pdf", + "filename": "C.pdf" + } + ], + "id": "sample-problem-c", + "name": "Sample Problem C", + "test_data_count": 78 + }, + { + "ordinal": 3, + "probid": 4, + "short_name": "D", + "rgb": "#FFFF00", + "color": "yellow", + "label": "D", + "time_limit": 4.0, + "statement": [ + { + "href": "contests/sample_contests/problems/sample-problem-d/statement", + "mime": "application/pdf", + "filename": "D.pdf" + } + ], + "id": "sample-problem-d", + "name": "Sample Problem D", + "test_data_count": 100 + } +] diff --git a/webapp/tests/Unit/Fixtures/sample/sample-scoreboard.json b/webapp/tests/Unit/Fixtures/sample/sample-scoreboard.json new file mode 100644 index 0000000000..458042dc9d --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/sample-scoreboard.json @@ -0,0 +1,202 @@ +{ + "event_id": "88478", + "time": "2024-04-18T16:48:58.897+02:00", + "contest_time": "5:00:59.897", + "state": { + "started": "2024-04-18T11:47:59.000+02:00", + "ended": "2024-04-18T16:47:59.000+02:00", + "frozen": "2024-04-18T15:47:59.000+02:00", + "thawed": null, + "finalized": null, + "end_of_updates": null + }, + "rows": [ + { + "rank": 1, + "team_id": "A", + "score": { + "total_time": 36 + }, + "problems": [ + { + "label": "A", + "problem_id": "sample-problem-a", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 1, + "first_to_solve": true + }, + { + "label": "B", + "problem_id": "sample-problem-b", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 5, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "sample-problem-c", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 10, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "sample-problem-d", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 20, + "first_to_solve": false + } + ] + }, + { + "rank": 2, + "team_id": "B", + "score": { + "total_time": 5 + }, + "problems": [ + { + "label": "B", + "problem_id": "sample-problem-b", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 2, + "first_to_solve": true + }, + { + "label": "C", + "problem_id": "sample-problem-c", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 3, + "first_to_solve": true + } + ] + }, + { + "rank": 3, + "team_id": "C", + "score": { + "total_time": 5 + }, + "problems": [ + { + "label": "B", + "problem_id": "sample-problem-b", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 2, + "first_to_solve": true + }, + { + "label": "C", + "problem_id": "sample-problem-c", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 3, + "first_to_solve": true + } + ] + }, + { + "rank": 4, + "team_id": "D", + "score": { + "total_time": 12 + }, + "problems": [ + { + "label": "D", + "problem_id": "sample-problem-d", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 12, + "first_to_solve": true + } + ] + }, + { + "rank": 5, + "team_id": "E", + "score": { + "total_time": 13 + }, + "problems": [ + { + "label": "D", + "problem_id": "sample-problem-d", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 13, + "first_to_solve": false + } + ] + }, + { + "rank": 6, + "team_id": "F", + "score": { + "total_time": 14 + }, + "problems": [ + { + "label": "D", + "problem_id": "sample-problem-d", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 14, + "first_to_solve": false + } + ] + }, + { + "rank": 7, + "team_id": "G", + "score": { + "total_time": 15 + }, + "problems": [ + { + "label": "D", + "problem_id": "sample-problem-d", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 15, + "first_to_solve": false + } + ] + }, + { + "rank": 8, + "team_id": "H", + "score": { + "total_time": 0 + }, + "problems": [] + }, + { + "rank": 9, + "team_id": "I", + "score": { + "total_time": 0 + }, + "problems": [] + } + ] +} diff --git a/webapp/tests/Unit/Fixtures/sample/sample-teams.json b/webapp/tests/Unit/Fixtures/sample/sample-teams.json new file mode 100644 index 0000000000..49fc9e2584 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/sample/sample-teams.json @@ -0,0 +1,128 @@ +[ + { + "location": null, + "organization_id": "1", + "hidden": false, + "group_ids": ["10001"], + "affiliation": "", + "teamid": 2, + "id": "A", + "icpc_id": "10001", + "label": "A", + "name": "Team A", + "display_name": "Team A", + "public_description": "" + }, + { + "location": null, + "organization_id": "2", + "hidden": false, + "group_ids": ["10001"], + "affiliation": "", + "teamid": 3, + "id": "B", + "icpc_id": "10002", + "label": "B", + "name": "Team B", + "display_name": "Team B", + "public_description": "" + }, + { + "location": null, + "organization_id": "3", + "hidden": false, + "group_ids": ["10001"], + "affiliation": "", + "teamid": 4, + "id": "C", + "icpc_id": "10003", + "label": "C", + "name": "Team C", + "display_name": "Team C", + "public_description": "" + }, + { + "location": null, + "organization_id": "4", + "hidden": false, + "group_ids": ["10002"], + "affiliation": "", + "teamid": 5, + "id": "D", + "icpc_id": "10004", + "label": "D", + "name": "Team D", + "display_name": "Team D", + "public_description": "" + }, + { + "location": null, + "organization_id": "5", + "hidden": false, + "group_ids": ["10002"], + "affiliation": "", + "teamid": 6, + "id": "E", + "icpc_id": "10005", + "label": "E", + "name": "Team E", + "display_name": "Team E", + "public_description": "" + }, + { + "location": null, + "organization_id": "6", + "hidden": false, + "group_ids": ["10002"], + "affiliation": "", + "teamid": 7, + "id": "F", + "icpc_id": "10006", + "label": "F", + "name": "Team F", + "display_name": "Team F", + "public_description": "" + }, + { + "location": null, + "organization_id": "7", + "hidden": false, + "group_ids": ["10003"], + "affiliation": "", + "teamid": 8, + "id": "G", + "icpc_id": "10007", + "label": "G", + "name": "Team G", + "display_name": "Team G", + "public_description": "" + }, + { + "location": null, + "organization_id": "8", + "hidden": false, + "group_ids": ["10003"], + "affiliation": "", + "teamid": 9, + "id": "H", + "icpc_id": "10008", + "label": "H", + "name": "Team H", + "display_name": "Team H", + "public_description": "" + }, + { + "location": null, + "organization_id": "9", + "hidden": false, + "group_ids": ["10003"], + "affiliation": "", + "teamid": 10, + "id": "I", + "icpc_id": "10009", + "label": "I", + "name": "Team I", + "display_name": "Team I", + "public_description": "" + } +] diff --git a/webapp/tests/Unit/Fixtures/wf/results-full-honors.tsv b/webapp/tests/Unit/Fixtures/wf/results-full-honors.tsv new file mode 100644 index 0000000000..102ab2f505 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/results-full-honors.tsv @@ -0,0 +1,131 @@ +results 1 +870679 1 Gold Medal 9 995 216 Northern Eurasia +870257 2 Gold Medal 9 1068 227 Asia East +870678 3 Gold Medal 9 1143 206 +873624 4 Gold Medal 9 1304 292 Europe +870259 5 Silver Medal 9 1524 274 +870260 6 Silver Medal 8 1013 281 +928309 7 Silver Medal 8 1102 230 Asia Pacific +870037 8 Silver Medal 8 1120 268 North America +870583 9 Bronze Medal 8 1121 260 +870584 10 Bronze Medal 8 1424 291 +870051 11 Bronze Medal 7 842 279 +870647 12 Bronze Medal 7 940 259 +870670 13 Highest Honors 7 955 291 Latin America +870585 14 Highest Honors 7 962 290 +870649 14 Highest Honors 7 962 290 +870271 16 Highest Honors 7 980 283 +870642 17 Highest Honors 7 1021 256 +870045 18 Highest Honors 7 1076 271 +870582 19 Highest Honors 7 1128 278 +870654 20 Highest Honors 7 1130 284 +868994 21 Highest Honors 7 1381 296 +870644 22 High Honors 6 510 187 +870646 23 High Honors 6 642 216 +870680 24 High Honors 6 645 218 +881825 25 High Honors 6 680 237 +871349 26 High Honors 6 683 246 +870692 27 High Honors 6 708 243 +870041 28 High Honors 6 718 260 +870268 29 High Honors 6 765 292 +870681 30 High Honors 6 932 287 +870040 31 High Honors 6 968 238 +870044 32 High Honors 6 1010 275 +870658 33 High Honors 6 1046 293 +870038 34 High Honors 6 1103 282 +870696 35 High Honors 6 1189 290 +870650 36 Honors 5 398 137 +870672 37 Honors 5 489 158 +870656 38 Honors 5 496 116 +870043 39 Honors 5 522 160 +870648 40 Honors 5 573 168 +870652 41 Honors 5 578 143 +870627 42 Honors 5 579 180 Asia West +870639 43 Honors 5 582 213 +870273 44 Honors 5 592 199 +870653 45 Honors 5 630 292 +870659 46 Honors 5 644 154 +870683 47 Honors 5 653 207 +870874 48 Honors 5 660 221 +870052 49 Honors 5 662 181 +870270 50 Honors 5 683 239 +870046 51 Honors 5 737 227 +870050 52 Honors 5 739 260 +870637 53 Honors 5 742 255 +870048 54 Honors 5 743 271 +870630 55 Honors 5 747 247 +870272 56 Honors 5 747 284 +870667 57 Honors 5 770 216 +870686 58 Honors 5 795 219 +870578 59 Honors 5 807 257 +870579 60 Honors 5 822 205 +870267 61 Honors 5 833 257 +870674 62 Honors 5 837 226 +870691 63 Honors 5 839 243 +870264 64 Honors 5 850 209 +870635 65 Honors 5 862 275 +870590 66 Honors 5 867 245 +870269 67 Honors 5 878 267 +870668 68 Honors 5 889 257 +870263 69 Honors 5 891 220 +870065 70 Honors 5 908 238 Africa and Arab +870053 71 Honors 5 968 260 +870042 72 Honors 5 971 292 +870689 73 Honors 5 1008 298 +870685 74 Honors 5 1048 267 +870638 75 Honors 5 1164 294 +871379 76 Honors 5 1227 273 +870056 Honorable 2 465 299 +870055 Honorable 4 465 164 +870063 Honorable 1 348 288 +870066 Honorable 2 289 173 +870054 Honorable 4 693 255 +870067 Honorable 2 405 259 +870688 Honorable 4 632 198 +870690 Honorable 3 691 271 +870574 Honorable 4 339 128 +870640 Honorable 3 435 195 +870636 Honorable 3 333 130 +870061 Honorable 1 140 140 +871347 Honorable 3 599 287 +870577 Honorable 4 590 215 +870057 Honorable 2 367 253 +870641 Honorable 3 448 243 +870663 Honorable 0 0 0 +870662 Honorable 2 459 238 +870058 Honorable 2 312 196 +870629 Honorable 3 538 299 +870628 Honorable 4 712 298 +870631 Honorable 4 421 191 +870632 Honorable 4 603 266 +870633 Honorable 3 469 250 +870634 Honorable 1 96 96 +870694 Honorable 4 879 290 +870068 Honorable 1 74 74 +870693 Honorable 3 650 279 +870587 Honorable 4 447 228 +870588 Honorable 4 707 244 +873768 Honorable 4 651 210 +870687 Honorable 4 870 268 +870643 Honorable 4 379 192 +870581 Honorable 2 398 287 +870258 Honorable 4 448 141 +870062 Honorable 1 58 58 +869963 Honorable 4 920 274 +870675 Honorable 1 162 162 +870664 Honorable 2 255 226 +870660 Honorable 3 766 289 +870676 Honorable 2 279 150 +870673 Honorable 1 230 190 +870671 Honorable 3 333 196 +870661 Honorable 3 728 272 +870669 Honorable 4 654 210 +870666 Honorable 3 382 177 +870665 Honorable 3 568 224 +870657 Honorable 4 333 107 +870651 Honorable 4 474 160 +870645 Honorable 3 609 277 +870591 Honorable 1 86 66 +870589 Honorable 3 480 178 +870039 Honorable 3 596 252 +870697 Honorable 3 761 275 diff --git a/webapp/tests/Unit/Fixtures/wf/results-full-ranked.tsv b/webapp/tests/Unit/Fixtures/wf/results-full-ranked.tsv new file mode 100644 index 0000000000..ee2759be99 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/results-full-ranked.tsv @@ -0,0 +1,131 @@ +results 1 +870679 1 Gold Medal 9 995 216 Northern Eurasia +870257 2 Gold Medal 9 1068 227 Asia East +870678 3 Gold Medal 9 1143 206 +873624 4 Gold Medal 9 1304 292 Europe +870259 5 Silver Medal 9 1524 274 +870260 6 Silver Medal 8 1013 281 +928309 7 Silver Medal 8 1102 230 Asia Pacific +870037 8 Silver Medal 8 1120 268 North America +870583 9 Bronze Medal 8 1121 260 +870584 10 Bronze Medal 8 1424 291 +870051 11 Bronze Medal 7 842 279 +870647 12 Bronze Medal 7 940 259 +870670 13 Ranked 7 955 291 Latin America +870585 14 Ranked 7 962 290 +870649 14 Ranked 7 962 290 +870271 16 Ranked 7 980 283 +870642 17 Ranked 7 1021 256 +870045 18 Ranked 7 1076 271 +870582 19 Ranked 7 1128 278 +870654 20 Ranked 7 1130 284 +868994 21 Ranked 7 1381 296 +870644 22 Ranked 6 510 187 +870646 23 Ranked 6 642 216 +870680 24 Ranked 6 645 218 +881825 25 Ranked 6 680 237 +871349 26 Ranked 6 683 246 +870692 27 Ranked 6 708 243 +870041 28 Ranked 6 718 260 +870268 29 Ranked 6 765 292 +870681 30 Ranked 6 932 287 +870040 31 Ranked 6 968 238 +870044 32 Ranked 6 1010 275 +870658 33 Ranked 6 1046 293 +870038 34 Ranked 6 1103 282 +870696 35 Ranked 6 1189 290 +870650 36 Ranked 5 398 137 +870672 37 Ranked 5 489 158 +870656 38 Ranked 5 496 116 +870043 39 Ranked 5 522 160 +870648 40 Ranked 5 573 168 +870652 41 Ranked 5 578 143 +870627 42 Ranked 5 579 180 Asia West +870639 43 Ranked 5 582 213 +870273 44 Ranked 5 592 199 +870653 45 Ranked 5 630 292 +870659 46 Ranked 5 644 154 +870683 47 Ranked 5 653 207 +870874 48 Ranked 5 660 221 +870052 49 Ranked 5 662 181 +870270 50 Ranked 5 683 239 +870046 51 Ranked 5 737 227 +870050 52 Ranked 5 739 260 +870637 53 Ranked 5 742 255 +870048 54 Ranked 5 743 271 +870630 55 Ranked 5 747 247 +870272 56 Ranked 5 747 284 +870667 57 Ranked 5 770 216 +870686 58 Ranked 5 795 219 +870578 59 Ranked 5 807 257 +870579 60 Ranked 5 822 205 +870267 61 Ranked 5 833 257 +870674 62 Ranked 5 837 226 +870691 63 Ranked 5 839 243 +870264 64 Ranked 5 850 209 +870635 65 Ranked 5 862 275 +870590 66 Ranked 5 867 245 +870269 67 Ranked 5 878 267 +870668 68 Ranked 5 889 257 +870263 69 Ranked 5 891 220 +870065 70 Ranked 5 908 238 Africa and Arab +870053 71 Ranked 5 968 260 +870042 72 Ranked 5 971 292 +870689 73 Ranked 5 1008 298 +870685 74 Ranked 5 1048 267 +870638 75 Ranked 5 1164 294 +871379 76 Ranked 5 1227 273 +870056 Honorable 2 465 299 +870055 Honorable 4 465 164 +870063 Honorable 1 348 288 +870066 Honorable 2 289 173 +870054 Honorable 4 693 255 +870067 Honorable 2 405 259 +870688 Honorable 4 632 198 +870690 Honorable 3 691 271 +870574 Honorable 4 339 128 +870640 Honorable 3 435 195 +870636 Honorable 3 333 130 +870061 Honorable 1 140 140 +871347 Honorable 3 599 287 +870577 Honorable 4 590 215 +870057 Honorable 2 367 253 +870641 Honorable 3 448 243 +870663 Honorable 0 0 0 +870662 Honorable 2 459 238 +870058 Honorable 2 312 196 +870629 Honorable 3 538 299 +870628 Honorable 4 712 298 +870631 Honorable 4 421 191 +870632 Honorable 4 603 266 +870633 Honorable 3 469 250 +870634 Honorable 1 96 96 +870694 Honorable 4 879 290 +870068 Honorable 1 74 74 +870693 Honorable 3 650 279 +870587 Honorable 4 447 228 +870588 Honorable 4 707 244 +873768 Honorable 4 651 210 +870687 Honorable 4 870 268 +870643 Honorable 4 379 192 +870581 Honorable 2 398 287 +870258 Honorable 4 448 141 +870062 Honorable 1 58 58 +869963 Honorable 4 920 274 +870675 Honorable 1 162 162 +870664 Honorable 2 255 226 +870660 Honorable 3 766 289 +870676 Honorable 2 279 150 +870673 Honorable 1 230 190 +870671 Honorable 3 333 196 +870661 Honorable 3 728 272 +870669 Honorable 4 654 210 +870666 Honorable 3 382 177 +870665 Honorable 3 568 224 +870657 Honorable 4 333 107 +870651 Honorable 4 474 160 +870645 Honorable 3 609 277 +870591 Honorable 1 86 66 +870589 Honorable 3 480 178 +870039 Honorable 3 596 252 +870697 Honorable 3 761 275 diff --git a/webapp/tests/Unit/Fixtures/wf/results-wf-honors.tsv b/webapp/tests/Unit/Fixtures/wf/results-wf-honors.tsv new file mode 100644 index 0000000000..6245dad638 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/results-wf-honors.tsv @@ -0,0 +1,131 @@ +results 1 +870679 1 Gold Medal 9 995 216 Northern Eurasia +870257 2 Gold Medal 9 1068 227 Asia East +870678 3 Gold Medal 9 1143 206 +873624 4 Gold Medal 9 1304 292 Europe +870259 5 Silver Medal 9 1524 274 +870260 6 Silver Medal 8 1013 281 +928309 7 Silver Medal 8 1102 230 Asia Pacific +870037 8 Silver Medal 8 1120 268 North America +870583 9 Bronze Medal 8 1121 260 +870584 10 Bronze Medal 8 1424 291 +870051 11 Bronze Medal 7 842 279 +870647 12 Bronze Medal 7 940 259 +870654 13 Highest Honors 7 1130 284 +870582 13 Highest Honors 7 1128 278 +870045 13 Highest Honors 7 1076 271 +870585 13 Highest Honors 7 962 290 +870670 13 Highest Honors 7 955 291 Latin America +870649 13 Highest Honors 7 962 290 +868994 13 Highest Honors 7 1381 296 +870642 13 Highest Honors 7 1021 256 +870271 13 Highest Honors 7 980 283 +870681 22 High Honors 6 932 287 +870038 22 High Honors 6 1103 282 +870044 22 High Honors 6 1010 275 +870268 22 High Honors 6 765 292 +870646 22 High Honors 6 642 216 +870644 22 High Honors 6 510 187 +870040 22 High Honors 6 968 238 +870692 22 High Honors 6 708 243 +870680 22 High Honors 6 645 218 +870658 22 High Honors 6 1046 293 +881825 22 High Honors 6 680 237 +870041 22 High Honors 6 718 260 +870696 22 High Honors 6 1189 290 +871349 22 High Honors 6 683 246 +870635 36 Honors 5 862 275 +870267 36 Honors 5 833 257 +870638 36 Honors 5 1164 294 +870264 36 Honors 5 850 209 +870648 36 Honors 5 573 168 +870674 36 Honors 5 837 226 +870656 36 Honors 5 496 116 +870065 36 Honors 5 908 238 Africa and Arab +870269 36 Honors 5 878 267 +870639 36 Honors 5 582 213 +870627 36 Honors 5 579 180 Asia West +870630 36 Honors 5 747 247 +870668 36 Honors 5 889 257 +870637 36 Honors 5 742 255 +870579 36 Honors 5 822 205 +870685 36 Honors 5 1048 267 +870650 36 Honors 5 398 137 +870689 36 Honors 5 1008 298 +870578 36 Honors 5 807 257 +870052 36 Honors 5 662 181 +870691 36 Honors 5 839 243 +870672 36 Honors 5 489 158 +870653 36 Honors 5 630 292 +870263 36 Honors 5 891 220 +870874 36 Honors 5 660 221 +870686 36 Honors 5 795 219 +870683 36 Honors 5 653 207 +870043 36 Honors 5 522 160 +870053 36 Honors 5 968 260 +870659 36 Honors 5 644 154 +870667 36 Honors 5 770 216 +871379 36 Honors 5 1227 273 +870042 36 Honors 5 971 292 +870050 36 Honors 5 739 260 +870048 36 Honors 5 743 271 +870272 36 Honors 5 747 284 +870270 36 Honors 5 683 239 +870652 36 Honors 5 578 143 +870273 36 Honors 5 592 199 +870590 36 Honors 5 867 245 +870046 36 Honors 5 737 227 +870056 Honorable 2 465 299 +870055 Honorable 4 465 164 +870063 Honorable 1 348 288 +870066 Honorable 2 289 173 +870054 Honorable 4 693 255 +870067 Honorable 2 405 259 +870688 Honorable 4 632 198 +870690 Honorable 3 691 271 +870574 Honorable 4 339 128 +870640 Honorable 3 435 195 +870636 Honorable 3 333 130 +870061 Honorable 1 140 140 +871347 Honorable 3 599 287 +870577 Honorable 4 590 215 +870057 Honorable 2 367 253 +870641 Honorable 3 448 243 +870663 Honorable 0 0 0 +870662 Honorable 2 459 238 +870058 Honorable 2 312 196 +870629 Honorable 3 538 299 +870628 Honorable 4 712 298 +870631 Honorable 4 421 191 +870632 Honorable 4 603 266 +870633 Honorable 3 469 250 +870634 Honorable 1 96 96 +870694 Honorable 4 879 290 +870068 Honorable 1 74 74 +870693 Honorable 3 650 279 +870587 Honorable 4 447 228 +870588 Honorable 4 707 244 +873768 Honorable 4 651 210 +870687 Honorable 4 870 268 +870643 Honorable 4 379 192 +870581 Honorable 2 398 287 +870258 Honorable 4 448 141 +870062 Honorable 1 58 58 +869963 Honorable 4 920 274 +870675 Honorable 1 162 162 +870664 Honorable 2 255 226 +870660 Honorable 3 766 289 +870676 Honorable 2 279 150 +870673 Honorable 1 230 190 +870671 Honorable 3 333 196 +870661 Honorable 3 728 272 +870669 Honorable 4 654 210 +870666 Honorable 3 382 177 +870665 Honorable 3 568 224 +870657 Honorable 4 333 107 +870651 Honorable 4 474 160 +870645 Honorable 3 609 277 +870591 Honorable 1 86 66 +870589 Honorable 3 480 178 +870039 Honorable 3 596 252 +870697 Honorable 3 761 275 diff --git a/webapp/tests/Unit/Fixtures/wf/results-wf-ranked.tsv b/webapp/tests/Unit/Fixtures/wf/results-wf-ranked.tsv new file mode 100644 index 0000000000..ee11ca9453 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/results-wf-ranked.tsv @@ -0,0 +1,131 @@ +results 1 +870679 1 Gold Medal 9 995 216 Northern Eurasia +870257 2 Gold Medal 9 1068 227 Asia East +870678 3 Gold Medal 9 1143 206 +873624 4 Gold Medal 9 1304 292 Europe +870259 5 Silver Medal 9 1524 274 +870260 6 Silver Medal 8 1013 281 +928309 7 Silver Medal 8 1102 230 Asia Pacific +870037 8 Silver Medal 8 1120 268 North America +870583 9 Bronze Medal 8 1121 260 +870584 10 Bronze Medal 8 1424 291 +870051 11 Bronze Medal 7 842 279 +870647 12 Bronze Medal 7 940 259 +870654 13 Ranked 7 1130 284 +870582 13 Ranked 7 1128 278 +870045 13 Ranked 7 1076 271 +870585 13 Ranked 7 962 290 +870670 13 Ranked 7 955 291 Latin America +870649 13 Ranked 7 962 290 +868994 13 Ranked 7 1381 296 +870642 13 Ranked 7 1021 256 +870271 13 Ranked 7 980 283 +870681 22 Ranked 6 932 287 +870038 22 Ranked 6 1103 282 +870044 22 Ranked 6 1010 275 +870268 22 Ranked 6 765 292 +870646 22 Ranked 6 642 216 +870644 22 Ranked 6 510 187 +870040 22 Ranked 6 968 238 +870692 22 Ranked 6 708 243 +870680 22 Ranked 6 645 218 +870658 22 Ranked 6 1046 293 +881825 22 Ranked 6 680 237 +870041 22 Ranked 6 718 260 +870696 22 Ranked 6 1189 290 +871349 22 Ranked 6 683 246 +870635 36 Ranked 5 862 275 +870267 36 Ranked 5 833 257 +870638 36 Ranked 5 1164 294 +870264 36 Ranked 5 850 209 +870648 36 Ranked 5 573 168 +870674 36 Ranked 5 837 226 +870656 36 Ranked 5 496 116 +870065 36 Ranked 5 908 238 Africa and Arab +870269 36 Ranked 5 878 267 +870639 36 Ranked 5 582 213 +870627 36 Ranked 5 579 180 Asia West +870630 36 Ranked 5 747 247 +870668 36 Ranked 5 889 257 +870637 36 Ranked 5 742 255 +870579 36 Ranked 5 822 205 +870685 36 Ranked 5 1048 267 +870650 36 Ranked 5 398 137 +870689 36 Ranked 5 1008 298 +870578 36 Ranked 5 807 257 +870052 36 Ranked 5 662 181 +870691 36 Ranked 5 839 243 +870672 36 Ranked 5 489 158 +870653 36 Ranked 5 630 292 +870263 36 Ranked 5 891 220 +870874 36 Ranked 5 660 221 +870686 36 Ranked 5 795 219 +870683 36 Ranked 5 653 207 +870043 36 Ranked 5 522 160 +870053 36 Ranked 5 968 260 +870659 36 Ranked 5 644 154 +870667 36 Ranked 5 770 216 +871379 36 Ranked 5 1227 273 +870042 36 Ranked 5 971 292 +870050 36 Ranked 5 739 260 +870048 36 Ranked 5 743 271 +870272 36 Ranked 5 747 284 +870270 36 Ranked 5 683 239 +870652 36 Ranked 5 578 143 +870273 36 Ranked 5 592 199 +870590 36 Ranked 5 867 245 +870046 36 Ranked 5 737 227 +870056 Honorable 2 465 299 +870055 Honorable 4 465 164 +870063 Honorable 1 348 288 +870066 Honorable 2 289 173 +870054 Honorable 4 693 255 +870067 Honorable 2 405 259 +870688 Honorable 4 632 198 +870690 Honorable 3 691 271 +870574 Honorable 4 339 128 +870640 Honorable 3 435 195 +870636 Honorable 3 333 130 +870061 Honorable 1 140 140 +871347 Honorable 3 599 287 +870577 Honorable 4 590 215 +870057 Honorable 2 367 253 +870641 Honorable 3 448 243 +870663 Honorable 0 0 0 +870662 Honorable 2 459 238 +870058 Honorable 2 312 196 +870629 Honorable 3 538 299 +870628 Honorable 4 712 298 +870631 Honorable 4 421 191 +870632 Honorable 4 603 266 +870633 Honorable 3 469 250 +870634 Honorable 1 96 96 +870694 Honorable 4 879 290 +870068 Honorable 1 74 74 +870693 Honorable 3 650 279 +870587 Honorable 4 447 228 +870588 Honorable 4 707 244 +873768 Honorable 4 651 210 +870687 Honorable 4 870 268 +870643 Honorable 4 379 192 +870581 Honorable 2 398 287 +870258 Honorable 4 448 141 +870062 Honorable 1 58 58 +869963 Honorable 4 920 274 +870675 Honorable 1 162 162 +870664 Honorable 2 255 226 +870660 Honorable 3 766 289 +870676 Honorable 2 279 150 +870673 Honorable 1 230 190 +870671 Honorable 3 333 196 +870661 Honorable 3 728 272 +870669 Honorable 4 654 210 +870666 Honorable 3 382 177 +870665 Honorable 3 568 224 +870657 Honorable 4 333 107 +870651 Honorable 4 474 160 +870645 Honorable 3 609 277 +870591 Honorable 1 86 66 +870589 Honorable 3 480 178 +870039 Honorable 3 596 252 +870697 Honorable 3 761 275 diff --git a/webapp/tests/Unit/Fixtures/wf/sample-groups.json b/webapp/tests/Unit/Fixtures/wf/sample-groups.json new file mode 100644 index 0000000000..c964b11165 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/sample-groups.json @@ -0,0 +1,102 @@ +[ + { + "hidden": false, + "categoryid": 5, + "id": "21495", + "icpc_id": "21495", + "name": "South Pacific", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 6, + "id": "21496", + "icpc_id": "21496", + "name": "Europe", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 7, + "id": "21497", + "icpc_id": "21497", + "name": "North America", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 10, + "id": "21500", + "icpc_id": "21500", + "name": "Asia Pacific", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 11, + "id": "21501", + "icpc_id": "21501", + "name": "Asia West", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 12, + "id": "21502", + "icpc_id": "21502", + "name": "Northern Eurasia", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 23, + "id": "21513", + "icpc_id": "21513", + "name": "Africa and Arab", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 24, + "id": "21514", + "icpc_id": "21514", + "name": "Latin America", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 25, + "id": "21515", + "icpc_id": "21515", + "name": "Asia", + "sortorder": 0, + "color": null, + "allow_self_registration": false + }, + { + "hidden": false, + "categoryid": 27, + "id": "21517", + "icpc_id": "21517", + "name": "Asia East", + "sortorder": 0, + "color": null, + "allow_self_registration": false + } +] diff --git a/webapp/tests/Unit/Fixtures/wf/sample-medals.json b/webapp/tests/Unit/Fixtures/wf/sample-medals.json new file mode 100644 index 0000000000..554639ba08 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/sample-medals.json @@ -0,0 +1,17 @@ +{ + "medals": { + "gold": 4, + "silver": 4, + "bronze": 4 + }, + "medal_categories": [ + "21496", + "21497", + "21500", + "21501", + "21502", + "21513", + "21514", + "21517" + ] +} diff --git a/webapp/tests/Unit/Fixtures/wf/sample-problems.json b/webapp/tests/Unit/Fixtures/wf/sample-problems.json new file mode 100644 index 0000000000..0f45dc98d2 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/sample-problems.json @@ -0,0 +1,211 @@ +[ + { + "ordinal": 0, + "probid": 1, + "short_name": "A", + "rgb": "#C0C0C0", + "color": "silver", + "label": "A", + "time_limit": 2.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/riddleofthesphinx/statement", + "mime": "application/pdf", + "filename": "A.pdf" + } + ], + "id": "riddleofthesphinx", + "name": "Riddle of the Sphinx", + "test_data_count": 50 + }, + { + "ordinal": 1, + "probid": 2, + "short_name": "B", + "rgb": "#32CD32", + "color": "limegreen", + "label": "B", + "time_limit": 2.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/schedule/statement", + "mime": "application/pdf", + "filename": "B.pdf" + } + ], + "id": "schedule", + "name": "Schedule", + "test_data_count": 32 + }, + { + "ordinal": 2, + "probid": 3, + "short_name": "C", + "rgb": "#000000", + "color": "black", + "label": "C", + "time_limit": 1.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/threekindsofdice/statement", + "mime": "application/pdf", + "filename": "C.pdf" + } + ], + "id": "threekindsofdice", + "name": "Three Kinds of Dice", + "test_data_count": 78 + }, + { + "ordinal": 3, + "probid": 4, + "short_name": "D", + "rgb": "#FFB6C1", + "color": "lightpink", + "label": "D", + "time_limit": 1.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/carlsvacation/statement", + "mime": "application/pdf", + "filename": "D.pdf" + } + ], + "id": "carlsvacation", + "name": "Carl's Vacation", + "test_data_count": 45 + }, + { + "ordinal": 4, + "probid": 5, + "short_name": "E", + "rgb": "#0000FF", + "color": "blue", + "label": "E", + "time_limit": 20.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/arecurringproblem/statement", + "mime": "application/pdf", + "filename": "E.pdf" + } + ], + "id": "arecurringproblem", + "name": "A Recurring Problem", + "test_data_count": 52 + }, + { + "ordinal": 5, + "probid": 6, + "short_name": "F", + "rgb": "#FF4500", + "color": "orangered", + "label": "F", + "time_limit": 3.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/tiltingtiles/statement", + "mime": "application/pdf", + "filename": "F.pdf" + } + ], + "id": "tiltingtiles", + "name": "Tilting Tiles", + "test_data_count": 55 + }, + { + "ordinal": 6, + "probid": 7, + "short_name": "G", + "rgb": "#00BFFF", + "color": "deepskyblue", + "label": "G", + "time_limit": 3.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/turningred/statement", + "mime": "application/pdf", + "filename": "G.pdf" + } + ], + "id": "turningred", + "name": "Turning Red", + "test_data_count": 33 + }, + { + "ordinal": 7, + "probid": 8, + "short_name": "H", + "rgb": "#FF8C00", + "color": "darkorange", + "label": "H", + "time_limit": 2.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/jetlag/statement", + "mime": "application/pdf", + "filename": "H.pdf" + } + ], + "id": "jetlag", + "name": "Jet Lag", + "test_data_count": 126 + }, + { + "ordinal": 8, + "probid": 9, + "short_name": "I", + "rgb": "#FFFF00", + "color": "yellow", + "label": "I", + "time_limit": 3.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/waterworld/statement", + "mime": "application/pdf", + "filename": "I.pdf" + } + ], + "id": "waterworld", + "name": "Waterworld", + "test_data_count": 49 + }, + { + "ordinal": 9, + "probid": 10, + "short_name": "J", + "rgb": "#9370DB", + "color": "mediumpurple", + "label": "J", + "time_limit": 4.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/bridgingthegap/statement", + "mime": "application/pdf", + "filename": "J.pdf" + } + ], + "id": "bridgingthegap", + "name": "Bridging the Gap", + "test_data_count": 104 + }, + { + "ordinal": 10, + "probid": 11, + "short_name": "K", + "rgb": "#D2B48C", + "color": "tan", + "label": "K", + "time_limit": 10.0, + "statement": [ + { + "href": "contests/wf47_finals/problems/aleaiactaest/statement", + "mime": "application/pdf", + "filename": "K.pdf" + } + ], + "id": "aleaiactaest", + "name": "Alea Iacta Est", + "test_data_count": 27 + } +] diff --git a/webapp/tests/Unit/Fixtures/wf/sample-scoreboard.json b/webapp/tests/Unit/Fixtures/wf/sample-scoreboard.json new file mode 100644 index 0000000000..fbbf98ae7b --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/sample-scoreboard.json @@ -0,0 +1,13359 @@ +{ + "event_id": "88478", + "time": "2024-04-18T16:48:58.897+02:00", + "contest_time": "5:00:59.897", + "state": { + "started": "2024-04-18T11:47:59.000+02:00", + "ended": "2024-04-18T16:47:59.000+02:00", + "frozen": "2024-04-18T15:47:59.000+02:00", + "thawed": null, + "finalized": null, + "end_of_updates": null + }, + "rows": [ + { + "rank": 1, + "team_id": "47065", + "score": { + "num_solved": 9, + "total_time": 995 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 58, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 216, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 142, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 110, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 94, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 45, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 78, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 35, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 177, + "first_to_solve": false + } + ] + }, + { + "rank": 2, + "team_id": "47074", + "score": { + "num_solved": 9, + "total_time": 1068 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 45, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 227, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 82, + "first_to_solve": true + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 72, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 152, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 64, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 99, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 62, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 205, + "first_to_solve": false + } + ] + }, + { + "rank": 3, + "team_id": "47060", + "score": { + "num_solved": 9, + "total_time": 1143 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 20, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 206, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 201, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 104, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 90, + "first_to_solve": true + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 40, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 151, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 24, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 187, + "first_to_solve": false + } + ] + }, + { + "rank": 4, + "team_id": "47034", + "score": { + "num_solved": 9, + "total_time": 1304 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 25, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 153, + "first_to_solve": true + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 253, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 52, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 220, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 45, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 74, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 70, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 11, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 292, + "first_to_solve": false + } + ] + }, + { + "rank": 5, + "team_id": "47095", + "score": { + "num_solved": 9, + "total_time": 1524 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 26, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 274, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 158, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 99, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 8, + "num_pending": 0, + "solved": true, + "time": 203, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 29, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 64, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 57, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 5, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 254, + "first_to_solve": false + } + ] + }, + { + "rank": 6, + "team_id": "47062", + "score": { + "num_solved": 8, + "total_time": 1013 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 21, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 159, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 54, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 281, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 43, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 79, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 60, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 236, + "first_to_solve": false + } + ] + }, + { + "rank": 7, + "team_id": "47080", + "score": { + "num_solved": 8, + "total_time": 1102 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 45, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 145, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 93, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 219, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 52, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 139, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 39, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 230, + "first_to_solve": false + } + ] + }, + { + "rank": 8, + "team_id": "47133", + "score": { + "num_solved": 8, + "total_time": 1120 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 21, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 263, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 208, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 70, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 268, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 56, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 93, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 41, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 9, + "team_id": "47093", + "score": { + "num_solved": 8, + "total_time": 1121 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 26, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 14, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 260, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 113, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 233, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 56, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 132, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 19, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 182, + "first_to_solve": false + } + ] + }, + { + "rank": 10, + "team_id": "47094", + "score": { + "num_solved": 8, + "total_time": 1424 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 10, + "first_to_solve": true + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 286, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 226, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 291, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 68, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 98, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 35, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 230, + "first_to_solve": false + } + ] + }, + { + "rank": 11, + "team_id": "47017", + "score": { + "num_solved": 7, + "total_time": 842 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 25, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 208, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 101, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 279, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 59, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 101, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 29, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 12, + "team_id": "47132", + "score": { + "num_solved": 7, + "total_time": 940 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 44, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 259, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 21, + "first_to_solve": true + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 254, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 90, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 141, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 31, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 13, + "team_id": "47096", + "score": { + "num_solved": 7, + "total_time": 955 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 46, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 291, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 88, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 214, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 81, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 125, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 50, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 14, + "team_id": "47066", + "score": { + "num_solved": 7, + "total_time": 962 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 55, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 290, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 86, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 197, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 98, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 89, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 27, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 14, + "team_id": "47119", + "score": { + "num_solved": 7, + "total_time": 962 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 28, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 287, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 99, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 290, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 73, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 51, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 54, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 16, + "team_id": "47135", + "score": { + "num_solved": 7, + "total_time": 980 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 44, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 283, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 141, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 256, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 23, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 126, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 67, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 17, + "team_id": "47127", + "score": { + "num_solved": 7, + "total_time": 1021 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 67, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 256, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 134, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 255, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 56, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 99, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 114, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 18, + "team_id": "47059", + "score": { + "num_solved": 7, + "total_time": 1076 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 52, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 162, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 187, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 271, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 121, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 97, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 46, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 19, + "team_id": "47056", + "score": { + "num_solved": 7, + "total_time": 1128 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 54, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 216, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 174, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 278, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 21, + "first_to_solve": true + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 139, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 26, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 20, + "team_id": "47027", + "score": { + "num_solved": 7, + "total_time": 1130 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 17, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 284, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 57, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 95, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 160, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 62, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 235, + "first_to_solve": false + } + ] + }, + { + "rank": 21, + "team_id": "47120", + "score": { + "num_solved": 7, + "total_time": 1381 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 152, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 209, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 113, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 296, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 142, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 265, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 44, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 22, + "team_id": "47058", + "score": { + "num_solved": 6, + "total_time": 510 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 24, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 187, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 67, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 5, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 54, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 110, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 28, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 23, + "team_id": "47051", + "score": { + "num_solved": 6, + "total_time": 642 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 51, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 7, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 84, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 213, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 43, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 216, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 35, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 24, + "team_id": "47088", + "score": { + "num_solved": 6, + "total_time": 645 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 33, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 82, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 193, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 32, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 218, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 27, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 25, + "team_id": "47112", + "score": { + "num_solved": 6, + "total_time": 680 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 53, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 237, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 114, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 9, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 51, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 88, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 37, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 26, + "team_id": "47136", + "score": { + "num_solved": 6, + "total_time": 683 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 42, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 246, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 107, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 60, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 93, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 55, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 27, + "team_id": "47078", + "score": { + "num_solved": 6, + "total_time": 708 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 33, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 243, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 94, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 67, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 137, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 54, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 28, + "team_id": "47116", + "score": { + "num_solved": 6, + "total_time": 718 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 50, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 153, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 260, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 42, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 92, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 121, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 29, + "team_id": "47037", + "score": { + "num_solved": 6, + "total_time": 765 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 42, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 119, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 73, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 78, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 21, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 292, + "first_to_solve": true + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 30, + "team_id": "47010", + "score": { + "num_solved": 6, + "total_time": 932 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 95, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 61, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 287, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 119, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 225, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 65, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 31, + "team_id": "47076", + "score": { + "num_solved": 6, + "total_time": 968 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 14, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 188, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 238, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 76, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 161, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 211, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 32, + "team_id": "47031", + "score": { + "num_solved": 6, + "total_time": 1010 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 41, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 275, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 184, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 122, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 200, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 28, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 33, + "team_id": "47106", + "score": { + "num_solved": 6, + "total_time": 1046 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 61, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 188, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 293, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 80, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 159, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 125, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 34, + "team_id": "47020", + "score": { + "num_solved": 6, + "total_time": 1103 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 53, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 202, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 282, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 80, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 206, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 100, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 35, + "team_id": "47134", + "score": { + "num_solved": 6, + "total_time": 1189 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 26, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 284, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 232, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 290, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 93, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 44, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 36, + "team_id": "47057", + "score": { + "num_solved": 5, + "total_time": 398 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 27, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 81, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 115, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 137, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 38, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 37, + "team_id": "47075", + "score": { + "num_solved": 5, + "total_time": 489 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 97, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 114, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 9, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 75, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 158, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 45, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 38, + "team_id": "47029", + "score": { + "num_solved": 5, + "total_time": 496 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 73, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 116, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 6, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 64, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 91, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 32, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 39, + "team_id": "47089", + "score": { + "num_solved": 5, + "total_time": 522 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 32, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 160, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 100, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 126, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 84, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 40, + "team_id": "47022", + "score": { + "num_solved": 5, + "total_time": 573 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 162, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 168, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 90, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 63, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 30, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 41, + "team_id": "47128", + "score": { + "num_solved": 5, + "total_time": 578 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 53, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 143, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 96, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 134, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 92, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 42, + "team_id": "47040", + "score": { + "num_solved": 5, + "total_time": 579 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 42, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 160, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 100, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 180, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 37, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 43, + "team_id": "47039", + "score": { + "num_solved": 5, + "total_time": 582 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 20, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 213, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 81, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 194, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 34, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 44, + "team_id": "47129", + "score": { + "num_solved": 5, + "total_time": 592 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 24, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 99, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 199, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 197, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 33, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 45, + "team_id": "47077", + "score": { + "num_solved": 5, + "total_time": 630 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 17, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 292, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 6, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 39, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 140, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 82, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 46, + "team_id": "47100", + "score": { + "num_solved": 5, + "total_time": 644 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 154, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 146, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 93, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 124, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 67, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 47, + "team_id": "47087", + "score": { + "num_solved": 5, + "total_time": 653 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 52, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 126, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 109, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 207, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 59, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 48, + "team_id": "47082", + "score": { + "num_solved": 5, + "total_time": 660 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 34, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 221, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 95, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 219, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 51, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 49, + "team_id": "47072", + "score": { + "num_solved": 5, + "total_time": 662 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 162, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 181, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 6, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 50, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 149, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 60, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 50, + "team_id": "47124", + "score": { + "num_solved": 5, + "total_time": 683 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 27, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 130, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 98, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 239, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 69, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 7, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 51, + "team_id": "47131", + "score": { + "num_solved": 5, + "total_time": 737 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 88, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 227, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 101, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 33, + "first_to_solve": true + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 128, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 52, + "team_id": "47118", + "score": { + "num_solved": 5, + "total_time": 739 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 38, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 146, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 103, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 260, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 152, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 53, + "team_id": "47050", + "score": { + "num_solved": 5, + "total_time": 742 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 61, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 212, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 119, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 255, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 95, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 54, + "team_id": "47117", + "score": { + "num_solved": 5, + "total_time": 743 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 23, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 98, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 232, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 271, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 39, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 55, + "team_id": "47042", + "score": { + "num_solved": 5, + "total_time": 747 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 48, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 247, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 109, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 187, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 96, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 56, + "team_id": "47122", + "score": { + "num_solved": 5, + "total_time": 747 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 105, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 284, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 135, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 54, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 89, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 57, + "team_id": "47107", + "score": { + "num_solved": 5, + "total_time": 770 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 103, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 216, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 144, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 211, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 96, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 58, + "team_id": "47086", + "score": { + "num_solved": 5, + "total_time": 795 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 23, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 219, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 180, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 208, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 65, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 59, + "team_id": "47063", + "score": { + "num_solved": 5, + "total_time": 807 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 26, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 62, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 210, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 257, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 152, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 60, + "team_id": "47053", + "score": { + "num_solved": 5, + "total_time": 822 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 111, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 12, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 101, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 137, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 205, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 108, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 61, + "team_id": "47009", + "score": { + "num_solved": 5, + "total_time": 833 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 29, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 257, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 13, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 60, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 249, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 78, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 62, + "team_id": "47026", + "score": { + "num_solved": 5, + "total_time": 837 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 188, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 187, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 92, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 226, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 44, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 63, + "team_id": "47073", + "score": { + "num_solved": 5, + "total_time": 839 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 141, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 192, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 79, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 243, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 144, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 64, + "team_id": "47021", + "score": { + "num_solved": 5, + "total_time": 850 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 117, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 178, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 164, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 209, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 122, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 65, + "team_id": "47008", + "score": { + "num_solved": 5, + "total_time": 862 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 196, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 275, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 104, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 160, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 87, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 66, + "team_id": "47130", + "score": { + "num_solved": 5, + "total_time": 867 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 75, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 193, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 39, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 215, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 245, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 67, + "team_id": "47038", + "score": { + "num_solved": 5, + "total_time": 878 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 102, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 267, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 114, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 150, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 125, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 68, + "team_id": "47048", + "score": { + "num_solved": 5, + "total_time": 889 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 179, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 112, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 6, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 257, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 202, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 39, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 69, + "team_id": "47081", + "score": { + "num_solved": 5, + "total_time": 891 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 91, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 163, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 174, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 220, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 103, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 70, + "team_id": "47036", + "score": { + "num_solved": 5, + "total_time": 908 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 117, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 238, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 14, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 219, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 190, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 84, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 71, + "team_id": "47092", + "score": { + "num_solved": 5, + "total_time": 968 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 57, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 260, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 152, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 222, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 97, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 72, + "team_id": "47115", + "score": { + "num_solved": 5, + "total_time": 971 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 31, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 9, + "num_pending": 0, + "solved": true, + "time": 292, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 99, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 146, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 163, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 73, + "team_id": "47061", + "score": { + "num_solved": 5, + "total_time": 1008 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 298, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 137, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 67, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 291, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 75, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 74, + "team_id": "47055", + "score": { + "num_solved": 5, + "total_time": 1048 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 267, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 210, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 5, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 79, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 263, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 169, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 75, + "team_id": "47016", + "score": { + "num_solved": 5, + "total_time": 1164 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 79, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 217, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 9, + "num_pending": 0, + "solved": true, + "time": 294, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 173, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 141, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 76, + "team_id": "47111", + "score": { + "num_solved": 5, + "total_time": 1227 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 175, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 273, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 7, + "num_pending": 0, + "solved": true, + "time": 147, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 269, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 183, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 77, + "team_id": "47110", + "score": { + "num_solved": 4, + "total_time": 333 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 31, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 107, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 94, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 41, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 78, + "team_id": "47012", + "score": { + "num_solved": 4, + "total_time": 339 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 32, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 100, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 6, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 59, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 8, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 128, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 79, + "team_id": "47071", + "score": { + "num_solved": 4, + "total_time": 379 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 47, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 192, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 49, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 51, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 80, + "team_id": "47043", + "score": { + "num_solved": 4, + "total_time": 421 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 25, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 11, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 145, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 191, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 40, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 81, + "team_id": "47067", + "score": { + "num_solved": 4, + "total_time": 447 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 48, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 26, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 105, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 228, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 66, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 82, + "team_id": "47085", + "score": { + "num_solved": 4, + "total_time": 448 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 88, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 27, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 38, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 121, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 141, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 83, + "team_id": "47002", + "score": { + "num_solved": 4, + "total_time": 465 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 68, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 9, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 122, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 164, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 71, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 84, + "team_id": "47113", + "score": { + "num_solved": 4, + "total_time": 474 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 55, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 113, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 106, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 160, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 85, + "team_id": "47023", + "score": { + "num_solved": 4, + "total_time": 590 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 80, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 215, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 124, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 111, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 86, + "team_id": "47044", + "score": { + "num_solved": 4, + "total_time": 603 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 105, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 16, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 156, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 266, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 16, + "first_to_solve": true + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 87, + "team_id": "47007", + "score": { + "num_solved": 4, + "total_time": 632 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 91, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 171, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 92, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 198, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 88, + "team_id": "47069", + "score": { + "num_solved": 4, + "total_time": 651 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 178, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 96, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 210, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 127, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 89, + "team_id": "47105", + "score": { + "num_solved": 4, + "total_time": 654 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 151, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 181, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 92, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 210, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 90, + "team_id": "47005", + "score": { + "num_solved": 4, + "total_time": 693 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 118, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 255, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 165, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 135, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 91, + "team_id": "47068", + "score": { + "num_solved": 4, + "total_time": 707 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 70, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 60, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 11, + "num_pending": 0, + "solved": true, + "time": 244, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 93, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 92, + "team_id": "47041", + "score": { + "num_solved": 4, + "total_time": 712 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 59, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 298, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 182, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 73, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 93, + "team_id": "47070", + "score": { + "num_solved": 4, + "total_time": 870 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 268, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 201, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 122, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 159, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 94, + "team_id": "47049", + "score": { + "num_solved": 4, + "total_time": 879 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 68, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 289, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 172, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 290, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 95, + "team_id": "47091", + "score": { + "num_solved": 4, + "total_time": 920 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 214, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 141, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 274, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 251, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 96, + "team_id": "47015", + "score": { + "num_solved": 3, + "total_time": 333 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 69, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 130, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 7, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 94, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 97, + "team_id": "47103", + "score": { + "num_solved": 3, + "total_time": 333 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 102, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 6, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 196, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 8, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 35, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 98, + "team_id": "47108", + "score": { + "num_solved": 3, + "total_time": 382 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 41, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 177, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 124, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 99, + "team_id": "47013", + "score": { + "num_solved": 3, + "total_time": 435 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 59, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 5, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 195, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 141, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 100, + "team_id": "47025", + "score": { + "num_solved": 3, + "total_time": 448 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 110, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 243, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 55, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 101, + "team_id": "47045", + "score": { + "num_solved": 3, + "total_time": 469 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 89, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 70, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 250, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 102, + "team_id": "47123", + "score": { + "num_solved": 3, + "total_time": 480 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 42, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 160, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 178, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 103, + "team_id": "47033", + "score": { + "num_solved": 3, + "total_time": 538 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 41, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 158, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 299, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 104, + "team_id": "47109", + "score": { + "num_solved": 3, + "total_time": 568 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 164, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 160, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 224, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 105, + "team_id": "47125", + "score": { + "num_solved": 3, + "total_time": 596 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 252, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 171, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 113, + "first_to_solve": true + } + ] + }, + { + "rank": 106, + "team_id": "47019", + "score": { + "num_solved": 3, + "total_time": 599 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 159, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 133, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 287, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 107, + "team_id": "47114", + "score": { + "num_solved": 3, + "total_time": 609 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 122, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 190, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 277, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 108, + "team_id": "47064", + "score": { + "num_solved": 3, + "total_time": 650 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 121, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 279, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 190, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 109, + "team_id": "47011", + "score": { + "num_solved": 3, + "total_time": 691 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 93, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 271, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 227, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 110, + "team_id": "47104", + "score": { + "num_solved": 3, + "total_time": 728 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 229, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 147, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 272, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 111, + "team_id": "47126", + "score": { + "num_solved": 3, + "total_time": 761 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 87, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 11, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 219, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 6, + "num_pending": 0, + "solved": true, + "time": 275, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 9, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 112, + "team_id": "47099", + "score": { + "num_solved": 3, + "total_time": 766 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 248, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 189, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 289, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 113, + "team_id": "47098", + "score": { + "num_solved": 2, + "total_time": 255 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 226, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 29, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 114, + "team_id": "47101", + "score": { + "num_solved": 2, + "total_time": 279 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 89, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 5, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 150, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 115, + "team_id": "47004", + "score": { + "num_solved": 2, + "total_time": 289 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 173, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 6, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 116, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 116, + "team_id": "47032", + "score": { + "num_solved": 2, + "total_time": 312 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 196, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 116, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 117, + "team_id": "47024", + "score": { + "num_solved": 2, + "total_time": 367 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 94, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 253, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 5, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 118, + "team_id": "47084", + "score": { + "num_solved": 2, + "total_time": 398 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 71, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 287, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 5, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 119, + "team_id": "47006", + "score": { + "num_solved": 2, + "total_time": 405 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 259, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 126, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 120, + "team_id": "47030", + "score": { + "num_solved": 2, + "total_time": 459 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 161, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 238, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 121, + "team_id": "47001", + "score": { + "num_solved": 2, + "total_time": 465 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 5, + "num_pending": 0, + "solved": true, + "time": 299, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 86, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 122, + "team_id": "47090", + "score": { + "num_solved": 1, + "total_time": 58 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 58, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 123, + "team_id": "47052", + "score": { + "num_solved": 1, + "total_time": 74 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 74, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 124, + "team_id": "47121", + "score": { + "num_solved": 1, + "total_time": 86 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": true, + "time": 66, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 6, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 125, + "team_id": "47046", + "score": { + "num_solved": 1, + "total_time": 96 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 4, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 96, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 126, + "team_id": "47018", + "score": { + "num_solved": 1, + "total_time": 140 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 140, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 127, + "team_id": "47097", + "score": { + "num_solved": 1, + "total_time": 162 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 1, + "num_pending": 0, + "solved": true, + "time": 162, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 1, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 128, + "team_id": "47102", + "score": { + "num_solved": 1, + "total_time": 230 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 3, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 3, + "num_pending": 0, + "solved": true, + "time": 190, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 2, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 129, + "team_id": "47003", + "score": { + "num_solved": 1, + "total_time": 348 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 4, + "num_pending": 0, + "solved": true, + "time": 288, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + }, + { + "rank": 130, + "team_id": "47028", + "score": { + "num_solved": 0, + "total_time": 0 + }, + "problems": [ + { + "label": "A", + "problem_id": "riddleofthesphinx", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "B", + "problem_id": "schedule", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "C", + "problem_id": "threekindsofdice", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "D", + "problem_id": "carlsvacation", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "E", + "problem_id": "arecurringproblem", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "F", + "problem_id": "tiltingtiles", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "G", + "problem_id": "turningred", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "H", + "problem_id": "jetlag", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "I", + "problem_id": "waterworld", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "J", + "problem_id": "bridgingthegap", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + }, + { + "label": "K", + "problem_id": "aleaiactaest", + "num_judged": 0, + "num_pending": 0, + "solved": false, + "first_to_solve": false + } + ] + } + ] +} diff --git a/webapp/tests/Unit/Fixtures/wf/sample-teams.json b/webapp/tests/Unit/Fixtures/wf/sample-teams.json new file mode 100644 index 0000000000..c2b5239e97 --- /dev/null +++ b/webapp/tests/Unit/Fixtures/wf/sample-teams.json @@ -0,0 +1,2082 @@ +[ + { + "location": null, + "organization_id": "2349", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "University of Central Florida", + "teamid": 2, + "id": "47120", + "icpc_id": "868994", + "label": "47120", + "name": "UCF Beehive", + "display_name": "University of Central Florida", + "public_description": "" + }, + { + "location": null, + "organization_id": "1872", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Taras Shevchenko National University of Kyiv", + "teamid": 3, + "id": "47091", + "icpc_id": "869963", + "label": "47091", + "name": "KNU_stascool5", + "display_name": "Taras Shevchenko National University of Kyiv", + "public_description": "" + }, + { + "location": null, + "organization_id": "2566", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "University of Waterloo", + "teamid": 4, + "id": "47133", + "icpc_id": "870037", + "label": "47133", + "name": "Waterloo Black", + "display_name": "University of Waterloo", + "public_description": "" + }, + { + "location": null, + "organization_id": "232", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "Carnegie Mellon University", + "teamid": 5, + "id": "47020", + "icpc_id": "870038", + "label": "47020", + "name": "CMU2", + "display_name": "Carnegie Mellon University", + "public_description": "" + }, + { + "location": null, + "organization_id": "2411", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "University of Maryland", + "teamid": 6, + "id": "47125", + "icpc_id": "870039", + "label": "47125", + "name": "UMD RED", + "display_name": "University of Maryland", + "public_description": "" + }, + { + "location": null, + "organization_id": "3328", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "Purdue University", + "teamid": 7, + "id": "47076", + "icpc_id": "870040", + "label": "47076", + "name": "Purdue BTR", + "display_name": "Purdue University", + "public_description": "" + }, + { + "location": null, + "organization_id": "2334", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "University of California San Diego", + "teamid": 8, + "id": "47116", + "icpc_id": "870041", + "label": "47116", + "name": "UCSD Fallen Star", + "display_name": "University of California San Diego", + "public_description": "" + }, + { + "location": null, + "organization_id": "3670", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "University of California Irvine", + "teamid": 9, + "id": "47115", + "icpc_id": "870042", + "label": "47115", + "name": "UCI Map", + "display_name": "University of California Irvine", + "public_description": "" + }, + { + "location": null, + "organization_id": "1822", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "Stanford University", + "teamid": 10, + "id": "47089", + "icpc_id": "870043", + "label": "47089", + "name": "Stanford Cardinal", + "display_name": "Stanford University", + "public_description": "" + }, + { + "location": null, + "organization_id": "597", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "Georgia Institute of Technology", + "teamid": 11, + "id": "47031", + "icpc_id": "870044", + "label": "47031", + "name": "Georgia Tech Pandas", + "display_name": "Georgia Institute of Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "1145", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "Massachusetts Institute of Technology", + "teamid": 12, + "id": "47059", + "icpc_id": "870045", + "label": "47059", + "name": "Taxi", + "display_name": "Massachusetts Institute of Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "2556", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "University of Toronto", + "teamid": 13, + "id": "47131", + "icpc_id": "870046", + "label": "47131", + "name": "UofT Blue", + "display_name": "University of Toronto", + "public_description": "" + }, + { + "location": null, + "organization_id": "2337", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "University of California, Berkeley", + "teamid": 14, + "id": "47117", + "icpc_id": "870048", + "label": "47117", + "name": "Golden State Geeks", + "display_name": "University of California, Berkeley", + "public_description": "" + }, + { + "location": null, + "organization_id": "2339", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "University of California Los Angeles", + "teamid": 15, + "id": "47118", + "icpc_id": "870050", + "label": "47118", + "name": "UCLA Tortellini", + "display_name": "University of California Los Angeles", + "public_description": "" + }, + { + "location": null, + "organization_id": "187", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "Brigham Young University", + "teamid": 16, + "id": "47017", + "icpc_id": "870051", + "label": "47017", + "name": "BYU TTL 255", + "display_name": "Brigham Young University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1329", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "New York University", + "teamid": 17, + "id": "47072", + "icpc_id": "870052", + "label": "47072", + "name": "RTFP", + "display_name": "New York University", + "public_description": "" + }, + { + "location": null, + "organization_id": "2538", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "The University of Texas at Dallas", + "teamid": 18, + "id": "47092", + "icpc_id": "870053", + "label": "47092", + "name": "\ud83d\udc4cwhoosh\ud83d\udc4c", + "display_name": "The University of Texas at Dallas", + "public_description": "" + }, + { + "location": null, + "organization_id": "4369", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Arab Academy for Science, Technology and Maritime Transport - Cairo", + "teamid": 19, + "id": "47005", + "icpc_id": "870054", + "label": "47005", + "name": "Three Sannin", + "display_name": "Arab Academy for Science, Technology and Maritime Transport - Cairo", + "public_description": "" + }, + { + "location": null, + "organization_id": "7113", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Al-Baath University", + "teamid": 20, + "id": "47002", + "icpc_id": "870055", + "label": "47002", + "name": "Kindergarteners > Project Managers", + "display_name": "Al-Baath University", + "public_description": "" + }, + { + "location": null, + "organization_id": "3664", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Ain Shams University - Faculty of Computer and Information Sciences", + "teamid": 21, + "id": "47001", + "icpc_id": "870056", + "label": "47001", + "name": "Sa3t El Sefr", + "display_name": "Ain Shams University - Faculty of Computer and Information Sciences", + "public_description": "" + }, + { + "location": null, + "organization_id": "358", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Damascus University", + "teamid": 22, + "id": "47024", + "icpc_id": "870057", + "label": "47024", + "name": "Win Win Situation", + "display_name": "Damascus University", + "public_description": "" + }, + { + "location": null, + "organization_id": "3519", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "German University in Cairo", + "teamid": 23, + "id": "47032", + "icpc_id": "870058", + "label": "47032", + "name": "Persistent Imposters", + "display_name": "German University in Cairo", + "public_description": "" + }, + { + "location": null, + "organization_id": "3348", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Cairo University - Faculty of Computers and Artificial Intelligence", + "teamid": 24, + "id": "47018", + "icpc_id": "870061", + "label": "47018", + "name": "Kofta Sentinel Edition", + "display_name": "Cairo University - Faculty of Computers and Artificial Intelligence", + "public_description": "" + }, + { + "location": null, + "organization_id": "8162", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Syrian Virtual University", + "teamid": 25, + "id": "47090", + "icpc_id": "870062", + "label": "47090", + "name": "AHA", + "display_name": "Syrian Virtual University", + "public_description": "" + }, + { + "location": null, + "organization_id": "5627", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Aleppo University", + "teamid": 26, + "id": "47003", + "icpc_id": "870063", + "label": "47003", + "name": "ZER\u00d8s", + "display_name": "Aleppo University", + "public_description": "" + }, + { + "location": null, + "organization_id": "6581", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Higher Institute for Applied Sciences and Technology", + "teamid": 27, + "id": "47036", + "icpc_id": "870065", + "label": "47036", + "name": "nON-sTOP", + "display_name": "Higher Institute for Applied Sciences and Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "65", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "American University of Beirut", + "teamid": 28, + "id": "47004", + "icpc_id": "870066", + "label": "47004", + "name": "AIM", + "display_name": "American University of Beirut", + "public_description": "" + }, + { + "location": null, + "organization_id": "93", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Assiut University", + "teamid": 29, + "id": "47006", + "icpc_id": "870067", + "label": "47006", + "name": "Assuit U", + "display_name": "Assiut University", + "public_description": "" + }, + { + "location": null, + "organization_id": "5676", + "hidden": false, + "group_ids": [ + "21513" + ], + "affiliation": "Jordan University of Science and Technology", + "teamid": 30, + "id": "47052", + "icpc_id": "870068", + "label": "47052", + "name": "divideAndKrunker", + "display_name": "Jordan University of Science and Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "1438", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Peking University", + "teamid": 31, + "id": "47074", + "icpc_id": "870257", + "label": "47074", + "name": "Let It Rot", + "display_name": "Peking University", + "public_description": "" + }, + { + "location": null, + "organization_id": "7255", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Southern University of Science and Technology", + "teamid": 32, + "id": "47085", + "icpc_id": "870258", + "label": "47085", + "name": "SUSTech-Vichy", + "display_name": "Southern University of Science and Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "2039", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Tsinghua University", + "teamid": 33, + "id": "47095", + "icpc_id": "870259", + "label": "47095", + "name": "there is a hand owning feet", + "display_name": "Tsinghua University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1268", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Nanjing University", + "teamid": 34, + "id": "47062", + "icpc_id": "870260", + "label": "47062", + "name": "triple_dogs", + "display_name": "Nanjing University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1657", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Shandong University", + "teamid": 35, + "id": "47081", + "icpc_id": "870263", + "label": "47081", + "name": "Big Black Dog Candy", + "display_name": "Shandong University", + "public_description": "" + }, + { + "location": null, + "organization_id": "245", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Central South University", + "teamid": 36, + "id": "47021", + "icpc_id": "870264", + "label": "47021", + "name": "Trio of Tomorrow Winds", + "display_name": "Central South University", + "public_description": "" + }, + { + "location": null, + "organization_id": "3120", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Beihang University", + "teamid": 37, + "id": "47009", + "icpc_id": "870267", + "label": "47009", + "name": "Dauntless Shield", + "display_name": "Beihang University", + "public_description": "" + }, + { + "location": null, + "organization_id": "13852", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Huazhong University of Science and Technology", + "teamid": 38, + "id": "47037", + "icpc_id": "870268", + "label": "47037", + "name": "Clover", + "display_name": "Huazhong University of Science and Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "680", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Hunan University", + "teamid": 39, + "id": "47038", + "icpc_id": "870269", + "label": "47038", + "name": "Gold legend", + "display_name": "Hunan University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1975", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "University of Hong Kong", + "teamid": 40, + "id": "47124", + "icpc_id": "870270", + "label": "47124", + "name": "I am not in danger. I am the danger.", + "display_name": "University of Hong Kong", + "public_description": "" + }, + { + "location": null, + "organization_id": "2720", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Xi'an Jiaotong University", + "teamid": 41, + "id": "47135", + "icpc_id": "870271", + "label": "47135", + "name": "Dodo Kindergarten", + "display_name": "Xi'an Jiaotong University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1973", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "University of Electronic Science and Technology of China", + "teamid": 42, + "id": "47122", + "icpc_id": "870272", + "label": "47122", + "name": "UESTC_Hanabi", + "display_name": "University of Electronic Science and Technology of China", + "public_description": "" + }, + { + "location": null, + "organization_id": "2505", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "University of Science and Technology of China", + "teamid": 43, + "id": "47129", + "icpc_id": "870273", + "label": "47129", + "name": "penalty limit exceeded", + "display_name": "University of Science and Technology of China", + "public_description": "" + }, + { + "location": null, + "organization_id": "161", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "Bina Nusantara University", + "teamid": 44, + "id": "47012", + "icpc_id": "870574", + "label": "47012", + "name": "okePeko", + "display_name": "Bina Nusantara University", + "public_description": "" + }, + { + "location": null, + "organization_id": "3514", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "Chulalongkorn University", + "teamid": 45, + "id": "47023", + "icpc_id": "870577", + "label": "47023", + "name": "Waifu=Senpai:Re", + "display_name": "Chulalongkorn University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1276", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "Nanyang Technological University", + "teamid": 46, + "id": "47063", + "icpc_id": "870578", + "label": "47063", + "name": "UknoWho", + "display_name": "Nanyang Technological University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1025", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "KAIST", + "teamid": 47, + "id": "47053", + "icpc_id": "870579", + "label": "47053", + "name": "BabyPenguin", + "display_name": "KAIST", + "public_description": "" + }, + { + "location": null, + "organization_id": "1735", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "Soongsil University", + "teamid": 48, + "id": "47084", + "icpc_id": "870581", + "label": "47084", + "name": "NLP", + "display_name": "Soongsil University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1058", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "Kyoto University", + "teamid": 49, + "id": "47056", + "icpc_id": "870582", + "label": "47056", + "name": "Heno World", + "display_name": "Kyoto University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1987", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "The University of Tokyo", + "teamid": 50, + "id": "47093", + "icpc_id": "870583", + "label": "47093", + "name": "Time Manipulators", + "display_name": "The University of Tokyo", + "public_description": "" + }, + { + "location": null, + "organization_id": "2006", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "Tokyo Institute of Technology", + "teamid": 51, + "id": "47094", + "icpc_id": "870584", + "label": "47094", + "name": "tonosama", + "display_name": "Tokyo Institute of Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "1303", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "National Taiwan University", + "teamid": 52, + "id": "47066", + "icpc_id": "870585", + "label": "47066", + "name": "8BQube", + "display_name": "National Taiwan University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1308", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "National Tsing Hua University", + "teamid": 53, + "id": "47067", + "icpc_id": "870587", + "label": "47067", + "name": "KiminoShiranaiMonogatari", + "display_name": "National Tsing Hua University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1316", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "National University of Singapore", + "teamid": 54, + "id": "47068", + "icpc_id": "870588", + "label": "47068", + "name": "The Spiders from Mars", + "display_name": "National University of Singapore", + "public_description": "" + }, + { + "location": null, + "organization_id": "3663", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "University of Engineering and Technology - VNU", + "teamid": 55, + "id": "47123", + "icpc_id": "870589", + "label": "47123", + "name": "mongoDB", + "display_name": "University of Engineering and Technology - VNU", + "public_description": "" + }, + { + "location": null, + "organization_id": "3705", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "University of Science, VNU-HCM", + "teamid": 56, + "id": "47130", + "icpc_id": "870590", + "label": "47130", + "name": "HCMUS-BurnedTomatoes", + "display_name": "University of Science, VNU-HCM", + "public_description": "" + }, + { + "location": null, + "organization_id": "2366", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "University of Dhaka", + "teamid": 57, + "id": "47121", + "icpc_id": "870591", + "label": "47121", + "name": "DU_Kronos", + "display_name": "University of Dhaka", + "public_description": "" + }, + { + "location": null, + "organization_id": "725", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Indian Institute of Technology - Delhi", + "teamid": 58, + "id": "47040", + "icpc_id": "870627", + "label": "47040", + "name": "AuditPass", + "display_name": "Indian Institute of Technology - Delhi", + "public_description": "" + }, + { + "location": null, + "organization_id": "5640", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Indian Institute of Technology - Indore", + "teamid": 59, + "id": "47041", + "icpc_id": "870628", + "label": "47041", + "name": "Paradigm Shift", + "display_name": "Indian Institute of Technology - Indore", + "public_description": "" + }, + { + "location": null, + "organization_id": "6685", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Ghulam Ishaq Khan Institute of Engineering Sciences and Technology", + "teamid": 60, + "id": "47033", + "icpc_id": "870629", + "label": "47033", + "name": "AC47", + "display_name": "Ghulam Ishaq Khan Institute of Engineering Sciences and Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "727", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Indian Institute of Technology - Kanpur", + "teamid": 61, + "id": "47042", + "icpc_id": "870630", + "label": "47042", + "name": "facelessmen3.0", + "display_name": "Indian Institute of Technology - Kanpur", + "public_description": "" + }, + { + "location": null, + "organization_id": "728", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Indian Institute of Technology - Kharagpur", + "teamid": 62, + "id": "47043", + "icpc_id": "870631", + "label": "47043", + "name": "Ab_Ki_Baar", + "display_name": "Indian Institute of Technology - Kharagpur", + "public_description": "" + }, + { + "location": null, + "organization_id": "723", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Indian Institute of Technology - Madras", + "teamid": 63, + "id": "47044", + "icpc_id": "870632", + "label": "47044", + "name": "Yorozuya Forever", + "display_name": "Indian Institute of Technology - Madras", + "public_description": "" + }, + { + "location": null, + "organization_id": "6908", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Indian Institute of Technology - Patna", + "teamid": 64, + "id": "47045", + "icpc_id": "870633", + "label": "47045", + "name": "Maanzar", + "display_name": "Indian Institute of Technology - Patna", + "public_description": "" + }, + { + "location": null, + "organization_id": "9636", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Indian Institute Of Technology Jodhpur", + "teamid": 65, + "id": "47046", + "icpc_id": "870634", + "label": "47046", + "name": "Pratyahara", + "display_name": "Indian Institute Of Technology Jodhpur", + "public_description": "" + }, + { + "location": null, + "organization_id": "114", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Bangladesh University of Engineering and Technology", + "teamid": 66, + "id": "47008", + "icpc_id": "870635", + "label": "47008", + "name": "BUET Sommohito", + "display_name": "Bangladesh University of Engineering and Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "5628", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "BITS-Pilani, Hyderabad Campus", + "teamid": 67, + "id": "47015", + "icpc_id": "870636", + "label": "47015", + "name": ":(){ :|:& };:", + "display_name": "BITS-Pilani, Hyderabad Campus", + "public_description": "" + }, + { + "location": null, + "organization_id": "3945", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Jadavpur University, Kolkata", + "teamid": 68, + "id": "47050", + "icpc_id": "870637", + "label": "47050", + "name": "BForBruteForce", + "display_name": "Jadavpur University, Kolkata", + "public_description": "" + }, + { + "location": null, + "organization_id": "181", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "BRAC University", + "teamid": 69, + "id": "47016", + "icpc_id": "870638", + "label": "47016", + "name": "BRACU_Crows", + "display_name": "BRAC University", + "public_description": "" + }, + { + "location": null, + "organization_id": "724", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Indian Institute of Technology - Bombay", + "teamid": 70, + "id": "47039", + "icpc_id": "870639", + "label": "47039", + "name": "Leaf Papad", + "display_name": "Indian Institute of Technology - Bombay", + "public_description": "" + }, + { + "location": null, + "organization_id": "4223", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Birla Institute of Technology and Science, Pilani Campus", + "teamid": 71, + "id": "47013", + "icpc_id": "870640", + "label": "47013", + "name": "jaggu's bois", + "display_name": "Birla Institute of Technology and Science, Pilani Campus", + "public_description": "" + }, + { + "location": null, + "organization_id": "3944", + "hidden": false, + "group_ids": [ + "21501" + ], + "affiliation": "Dhirubhai Ambani Institute of Information and Communication Technology, Gandhinagar", + "teamid": 72, + "id": "47025", + "icpc_id": "870641", + "label": "47025", + "name": "Silent 3lers", + "display_name": "Dhirubhai Ambani Institute of Information and Communication Technology, Gandhinagar", + "public_description": "" + }, + { + "location": null, + "organization_id": "3065", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "University of Novi Sad", + "teamid": 73, + "id": "47127", + "icpc_id": "870642", + "label": "47127", + "name": "Infinity", + "display_name": "University of Novi Sad", + "public_description": "" + }, + { + "location": null, + "organization_id": "8667", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Neapolis University Pafos", + "teamid": 74, + "id": "47071", + "icpc_id": "870643", + "label": "47071", + "name": "bird-cherry", + "display_name": "Neapolis University Pafos", + "public_description": "" + }, + { + "location": null, + "organization_id": "3375", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Lviv National University", + "teamid": 75, + "id": "47058", + "icpc_id": "870644", + "label": "47058", + "name": "LNU Stallions", + "display_name": "Lviv National University", + "public_description": "" + }, + { + "location": null, + "organization_id": "196", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "University of Bucharest", + "teamid": 76, + "id": "47114", + "icpc_id": "870645", + "label": "47114", + "name": "Echipa Sarata", + "display_name": "University of Bucharest", + "public_description": "" + }, + { + "location": null, + "organization_id": "931", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Jagiellonian University in Krakow", + "teamid": 77, + "id": "47051", + "icpc_id": "870646", + "label": "47051", + "name": "Jagiellonian 1", + "display_name": "Jagiellonian University in Krakow", + "public_description": "" + }, + { + "location": null, + "organization_id": "2673", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "University of Warsaw", + "teamid": 78, + "id": "47132", + "icpc_id": "870647", + "label": "47132", + "name": "Warsaw Eagles 2023", + "display_name": "University of Warsaw", + "public_description": "" + }, + { + "location": null, + "organization_id": "269", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Charles University", + "teamid": 79, + "id": "47022", + "icpc_id": "870648", + "label": "47022", + "name": "MFF3", + "display_name": "Charles University", + "public_description": "" + }, + { + "location": null, + "organization_id": "2343", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "University of Cambridge", + "teamid": 80, + "id": "47119", + "icpc_id": "870649", + "label": "47119", + "name": "Trinity's Trinity", + "display_name": "University of Cambridge", + "public_description": "" + }, + { + "location": null, + "organization_id": "1112", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Lund University", + "teamid": 81, + "id": "47057", + "icpc_id": "870650", + "label": "47057", + "name": "ezcp", + "display_name": "Lund University", + "public_description": "" + }, + { + "location": null, + "organization_id": "2320", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "University of Bergen", + "teamid": 82, + "id": "47113", + "icpc_id": "870651", + "label": "47113", + "name": "Algos but Greek chars", + "display_name": "University of Bergen", + "public_description": "" + }, + { + "location": null, + "organization_id": "3620", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "University of Oxford", + "teamid": 83, + "id": "47128", + "icpc_id": "870652", + "label": "47128", + "name": "lamelame", + "display_name": "University of Oxford", + "public_description": "" + }, + { + "location": null, + "organization_id": "2267", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Saarland University", + "teamid": 84, + "id": "47077", + "icpc_id": "870653", + "label": "47077", + "name": "\u229b\u02ef\u26d2", + "display_name": "Saarland University", + "public_description": "" + }, + { + "location": null, + "organization_id": "3436", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "\u00c9cole Normale Sup\u00e9rieure de Paris", + "teamid": 85, + "id": "47027", + "icpc_id": "870654", + "label": "47027", + "name": "ENS Ulm 1", + "display_name": "\u00c9cole Normale Sup\u00e9rieure de Paris", + "public_description": "" + }, + { + "location": null, + "organization_id": "451", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "ETH Z\u00fcrich", + "teamid": 86, + "id": "47029", + "icpc_id": "870656", + "label": "47029", + "name": "gETHyped", + "display_name": "ETH Z\u00fcrich", + "public_description": "" + }, + { + "location": null, + "organization_id": "8869", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Universit\u00e0 di Pisa", + "teamid": 87, + "id": "47110", + "icpc_id": "870657", + "label": "47110", + "name": "flag[10]", + "display_name": "Universit\u00e0 di Pisa", + "public_description": "" + }, + { + "location": null, + "organization_id": "478", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Universidade do Porto", + "teamid": 88, + "id": "47106", + "icpc_id": "870658", + "label": "47106", + "name": "Heroes of the C", + "display_name": "Universidade do Porto", + "public_description": "" + }, + { + "location": null, + "organization_id": "2112", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad de La Habana", + "teamid": 89, + "id": "47100", + "icpc_id": "870659", + "label": "47100", + "name": "UH Top", + "display_name": "Universidad de La Habana", + "public_description": "" + }, + { + "location": null, + "organization_id": "8140", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad de Guanajuato - DCNE", + "teamid": 90, + "id": "47099", + "icpc_id": "870660", + "label": "47099", + "name": "OWO", + "display_name": "Universidad de Guanajuato - DCNE", + "public_description": "" + }, + { + "location": null, + "organization_id": "3372", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad Panamericana Campus Bonaterra", + "teamid": 91, + "id": "47104", + "icpc_id": "870661", + "label": "47104", + "name": "UPsolving", + "display_name": "Universidad Panamericana Campus Bonaterra", + "public_description": "" + }, + { + "location": null, + "organization_id": "7138", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Facultad de Ciencias-Universidad Nacional Aut\u00f3noma de M\u00e9xico", + "teamid": 92, + "id": "47030", + "icpc_id": "870662", + "label": "47030", + "name": "Cagua++", + "display_name": "Facultad de Ciencias-Universidad Nacional Aut\u00f3noma de M\u00e9xico", + "public_description": "" + }, + { + "location": null, + "organization_id": "467", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Escuela Superior De Computo Instituto Politecnico Nacional", + "teamid": 93, + "id": "47028", + "icpc_id": "870663", + "label": "47028", + "name": "Exceso de F\u00e9", + "display_name": "Escuela Superior De Computo Instituto Politecnico Nacional", + "public_description": "" + }, + { + "location": null, + "organization_id": "3424", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad de Guadalajara CUCEI", + "teamid": 94, + "id": "47098", + "icpc_id": "870664", + "label": "47098", + "name": "Almost Retired", + "display_name": "Universidad de Guadalajara CUCEI", + "public_description": "" + }, + { + "location": null, + "organization_id": "2242", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidade Federal do Rio de Janeiro", + "teamid": 95, + "id": "47109", + "icpc_id": "870665", + "label": "47109", + "name": "Lebenslangerschicksalsschatz", + "display_name": "Universidade Federal do Rio de Janeiro", + "public_description": "" + }, + { + "location": null, + "organization_id": "2230", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidade Federal de Minas Gerais", + "teamid": 96, + "id": "47108", + "icpc_id": "870666", + "label": "47108", + "name": "Humuhumunukunukuapua'a", + "display_name": "Universidade Federal de Minas Gerais", + "public_description": "" + }, + { + "location": null, + "organization_id": "2216", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidade Estadual de Campinas", + "teamid": 97, + "id": "47107", + "icpc_id": "870667", + "label": "47107", + "name": "Voc\u00ea beijaria Matheus Leal Viana?", + "display_name": "Universidade Estadual de Campinas", + "public_description": "" + }, + { + "location": null, + "organization_id": "2951", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Instituto Militar de Engenharia", + "teamid": 98, + "id": "47048", + "icpc_id": "870668", + "label": "47048", + "name": "12k Club", + "display_name": "Instituto Militar de Engenharia", + "public_description": "" + }, + { + "location": null, + "organization_id": "2195", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidade de Bras\u00edlia", + "teamid": 99, + "id": "47105", + "icpc_id": "870669", + "label": "47105", + "name": "FLAMENGO", + "display_name": "Universidade de Bras\u00edlia", + "public_description": "" + }, + { + "location": null, + "organization_id": "3683", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad de Buenos Aires - FCEN", + "teamid": 100, + "id": "47096", + "icpc_id": "870670", + "label": "47096", + "name": "una ma y no inchamo ma", + "display_name": "Universidad de Buenos Aires - FCEN", + "public_description": "" + }, + { + "location": null, + "organization_id": "2154", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad Nacional de Rosario", + "teamid": 101, + "id": "47103", + "icpc_id": "870671", + "label": "47103", + "name": "Don Gato", + "display_name": "Universidad Nacional de Rosario", + "public_description": "" + }, + { + "location": null, + "organization_id": "1480", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Pontificia Universidad Cat\u00f3lica de Chile", + "teamid": 102, + "id": "47075", + "icpc_id": "870672", + "label": "47075", + "name": "Laranjas.clear()", + "display_name": "Pontificia Universidad Cat\u00f3lica de Chile", + "public_description": "" + }, + { + "location": null, + "organization_id": "3272", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad Nacional de Colombia - Bogot\u00e1", + "teamid": 103, + "id": "47102", + "icpc_id": "870673", + "label": "47102", + "name": "phiUN", + "display_name": "Universidad Nacional de Colombia - Bogot\u00e1", + "public_description": "" + }, + { + "location": null, + "organization_id": "3257", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "EAFIT University", + "teamid": 104, + "id": "47026", + "icpc_id": "870674", + "label": "47026", + "name": "Fast and Fourier", + "display_name": "EAFIT University", + "public_description": "" + }, + { + "location": null, + "organization_id": "5716", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad de Costa Rica", + "teamid": 105, + "id": "47097", + "icpc_id": "870675", + "label": "47097", + "name": "UCR Mix", + "display_name": "Universidad de Costa Rica", + "public_description": "" + }, + { + "location": null, + "organization_id": "3503", + "hidden": false, + "group_ids": [ + "21514" + ], + "affiliation": "Universidad Mayor de San Sim\u00f3n", + "teamid": 106, + "id": "47101", + "icpc_id": "870676", + "label": "47101", + "name": "Club de Front\u00f3n 2880", + "display_name": "Universidad Mayor de San Sim\u00f3n", + "public_description": "" + }, + { + "location": null, + "organization_id": "1220", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Moscow Institute of Physics and Technology", + "teamid": 107, + "id": "47060", + "icpc_id": "870678", + "label": "47060", + "name": "Yolki-palki", + "display_name": "Moscow Institute of Physics and Technology", + "public_description": "" + }, + { + "location": null, + "organization_id": "3534", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "National Research University Higher School of Economics", + "teamid": 108, + "id": "47065", + "icpc_id": "870679", + "label": "47065", + "name": "FFTilted", + "display_name": "National Research University Higher School of Economics", + "public_description": "" + }, + { + "location": null, + "organization_id": "1802", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "St. Petersburg State University", + "teamid": 109, + "id": "47088", + "icpc_id": "870680", + "label": "47088", + "name": "Urgant Team", + "display_name": "St. Petersburg State University", + "public_description": "" + }, + { + "location": null, + "organization_id": "144", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Belarusian State University", + "teamid": 110, + "id": "47010", + "icpc_id": "870681", + "label": "47010", + "name": "BelarusianSU 1: Dungeon Thread", + "display_name": "Belarusian State University", + "public_description": "" + }, + { + "location": null, + "organization_id": "3295", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "St. Petersburg ITMO University", + "teamid": 111, + "id": "47087", + "icpc_id": "870683", + "label": "47087", + "name": "Cataleptodius", + "display_name": "St. Petersburg ITMO University", + "public_description": "" + }, + { + "location": null, + "organization_id": "989", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Kazakh-British Technical University", + "teamid": 112, + "id": "47055", + "icpc_id": "870685", + "label": "47055", + "name": "DeoxyriboNucleic Acid", + "display_name": "Kazakh-British Technical University", + "public_description": "" + }, + { + "location": null, + "organization_id": "9606", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "St. Petersburg Campus of Higher School of Economics", + "teamid": 113, + "id": "47086", + "icpc_id": "870686", + "label": "47086", + "name": "Just3Keks", + "display_name": "St. Petersburg Campus of Higher School of Economics", + "public_description": "" + }, + { + "location": null, + "organization_id": "6514", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Nazarbayev University", + "teamid": 114, + "id": "47070", + "icpc_id": "870687", + "label": "47070", + "name": "wf or gf?", + "display_name": "Nazarbayev University", + "public_description": "" + }, + { + "location": null, + "organization_id": "10637", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Astana IT University", + "teamid": 115, + "id": "47007", + "icpc_id": "870688", + "label": "47007", + "name": "AITU 1", + "display_name": "Astana IT University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1237", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Moscow State University", + "teamid": 116, + "id": "47061", + "icpc_id": "870689", + "label": "47061", + "name": "apes together strong", + "display_name": "Moscow State University", + "public_description": "" + }, + { + "location": null, + "organization_id": "140", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Belarusian State University of Informatics and Radioelectronics", + "teamid": 117, + "id": "47011", + "icpc_id": "870690", + "label": "47011", + "name": "Belarusian SUIR #1: So stuffy", + "display_name": "Belarusian State University of Informatics and Radioelectronics", + "public_description": "" + }, + { + "location": null, + "organization_id": "1372", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Novosibirsk State University", + "teamid": 118, + "id": "47073", + "icpc_id": "870691", + "label": "47073", + "name": "Novosibirsk SU 4: MathWay", + "display_name": "Novosibirsk State University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1623", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "Saratov State University", + "teamid": 119, + "id": "47078", + "icpc_id": "870692", + "label": "47078", + "name": "Saratov SU K", + "display_name": "Saratov State University", + "public_description": "" + }, + { + "location": null, + "organization_id": "1215", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "National Research Nuclear University MEPhI (Moscow Engineering Physics Institute)", + "teamid": 120, + "id": "47064", + "icpc_id": "870693", + "label": "47064", + "name": "Useless but powerful", + "display_name": "National Research Nuclear University MEPhI (Moscow Engineering Physics Institute)", + "public_description": "" + }, + { + "location": null, + "organization_id": "4330", + "hidden": false, + "group_ids": [ + "21502" + ], + "affiliation": "International IT University", + "teamid": 121, + "id": "47049", + "icpc_id": "870694", + "label": "47049", + "name": "IITU: Defective Memory", + "display_name": "International IT University", + "public_description": "" + }, + { + "location": null, + "organization_id": "2449", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "UNSW Sydney", + "teamid": 122, + "id": "47134", + "icpc_id": "870696", + "label": "47134", + "name": "Hell Hunt", + "display_name": "UNSW Sydney", + "public_description": "" + }, + { + "location": null, + "organization_id": "1978", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "University of Melbourne", + "teamid": 123, + "id": "47126", + "icpc_id": "870697", + "label": "47126", + "name": "Arts Students", + "display_name": "University of Melbourne", + "public_description": "" + }, + { + "location": null, + "organization_id": "1663", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Shanghai Jiao Tong University", + "teamid": 124, + "id": "47082", + "icpc_id": "870874", + "label": "47082", + "name": "Silver Bullet", + "display_name": "Shanghai Jiao Tong University", + "public_description": "" + }, + { + "location": null, + "organization_id": "230", + "hidden": false, + "group_ids": [ + "21497" + ], + "affiliation": "Carleton College", + "teamid": 125, + "id": "47019", + "icpc_id": "871347", + "label": "47019", + "name": "Chinela++", + "display_name": "Carleton College", + "public_description": "" + }, + { + "location": null, + "organization_id": "2751", + "hidden": false, + "group_ids": [ + "21517" + ], + "affiliation": "Zhejiang University", + "teamid": 126, + "id": "47136", + "icpc_id": "871349", + "label": "47136", + "name": "Solitary Dream", + "display_name": "Zhejiang University", + "public_description": "" + }, + { + "location": null, + "organization_id": "2390", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "Universitas Indonesia", + "teamid": 127, + "id": "47111", + "icpc_id": "871379", + "label": "47111", + "name": "Bingung weh", + "display_name": "Universitas Indonesia", + "public_description": "" + }, + { + "location": null, + "organization_id": "9745", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "Harbour.Space University - Barcelona Campus", + "teamid": 128, + "id": "47034", + "icpc_id": "873624", + "label": "47034", + "name": "P+P+P", + "display_name": "Harbour.Space University - Barcelona Campus", + "public_description": "" + }, + { + "location": null, + "organization_id": "275", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "National Yang Ming Chiao Tung University", + "teamid": 129, + "id": "47069", + "icpc_id": "873768", + "label": "47069", + "name": "NYCU_13", + "display_name": "National Yang Ming Chiao Tung University", + "public_description": "" + }, + { + "location": null, + "organization_id": "2316", + "hidden": false, + "group_ids": [ + "21496" + ], + "affiliation": "University of Belgrade", + "teamid": 130, + "id": "47112", + "icpc_id": "881825", + "label": "47112", + "name": "UoB R-Shuf", + "display_name": "University of Belgrade", + "public_description": "" + }, + { + "location": null, + "organization_id": "1636", + "hidden": false, + "group_ids": [ + "21500" + ], + "affiliation": "Seoul National University", + "teamid": 131, + "id": "47080", + "icpc_id": "928309", + "label": "47080", + "name": "HappyLastDance", + "display_name": "Seoul National University", + "public_description": "" + } +] diff --git a/webapp/tests/Unit/GeneralAvailabilityTest.php b/webapp/tests/Unit/GeneralAvailabilityTest.php index 082ef5a0d6..e9698fbcb1 100644 --- a/webapp/tests/Unit/GeneralAvailabilityTest.php +++ b/webapp/tests/Unit/GeneralAvailabilityTest.php @@ -11,11 +11,6 @@ class GeneralAvailabilityTest extends BaseTestCase */ public function testPageIsSuccessful(string $url, int $code): void { - if ($url === '/api/contests/1' && !$this->dataSourceIsLocal()) { - // Use external ID for contest. - $url = '/api/contests/demo'; - } - $this->client->request('GET', $url); $response = $this->client->getResponse(); @@ -32,7 +27,7 @@ public function urlProvider(): Generator yield ['/api', 301]; // Gets redirected to /api/ yield ['/api/', 200]; yield ['/api/contests', 200]; - yield ['/api/contests/1', 200]; + yield ['/api/contests/demo', 200]; // Note that the individual API endpoints are tested with check-api // and cannot easily be tested here since phpunit doesn't provide a // fully featured server environment. diff --git a/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php b/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php index cbd2d0b6cf..7285d35b21 100644 --- a/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php +++ b/webapp/tests/Unit/Integration/QueuetaskIntegrationTest.php @@ -55,7 +55,7 @@ protected function setUp(): void 'compile_penalty' => false, 'penalty_time' => 20, 'score_in_seconds' => false, - 'data_source' => 0, + 'shadow_mode' => 0, 'sourcefiles_limit' => 1, 'sourcesize_limit' => 1024*256, ]; @@ -71,8 +71,7 @@ protected function setUp(): void $this->scoreboardService = new ScoreboardService( $this->em, $dj, $this->config, - self::getContainer()->get(LoggerInterface::class), - self::getContainer()->get(EventLogService::class) + self::getContainer()->get(LoggerInterface::class) ); $this->submissionService = new SubmissionService( $this->em, diff --git a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php index 847a79a3a6..7833e99abe 100644 --- a/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php +++ b/webapp/tests/Unit/Integration/ScoreboardIntegrationTest.php @@ -69,7 +69,7 @@ protected function setUp(): void 'compile_penalty' => false, 'penalty_time' => 20, 'score_in_seconds' => false, - 'data_source' => 0, + 'shadow_mode' => 0, 'show_teams_on_scoreboard' => 0, ]; @@ -83,8 +83,7 @@ protected function setUp(): void $this->em = self::getContainer()->get('doctrine')->getManager(); $this->ss = new ScoreboardService( $this->em, $this->dj, $this->config, - self::getContainer()->get(LoggerInterface::class), - self::getContainer()->get(EventLogService::class) + self::getContainer()->get(LoggerInterface::class) ); // Create a contest, problems and teams for which to test the diff --git a/webapp/tests/Unit/Service/AwardServiceTest.php b/webapp/tests/Unit/Service/AwardServiceTest.php index f0bb1dc432..4db426ad21 100644 --- a/webapp/tests/Unit/Service/AwardServiceTest.php +++ b/webapp/tests/Unit/Service/AwardServiceTest.php @@ -174,12 +174,7 @@ protected function setUp(): void protected function getAwardService(): AwardService { - // Always use external IDs so we also test that those are used in the correct spot - $eventLogService = $this->createMock(EventLogService::class); - $eventLogService->expects(self::any()) - ->method('apiIdFieldForEntity') - ->willReturn('externalId'); - return new AwardService($eventLogService); + return new AwardService(); } protected function getAward(string $label): ?Award diff --git a/webapp/tests/Unit/Service/Compare/AwardCompareServiceTest.php b/webapp/tests/Unit/Service/Compare/AwardCompareServiceTest.php new file mode 100644 index 0000000000..55f7b03277 --- /dev/null +++ b/webapp/tests/Unit/Service/Compare/AwardCompareServiceTest.php @@ -0,0 +1,67 @@ +createMock(SerializerInterface::class); + $compareService = new AwardCompareService($serializer); + + $compareService->compare($awards1, $awards2); + $messages = $compareService->getMessages(); + + self::assertEquals($expectedMessages, $messages); + } + + public function provideCompare(): Generator + { + yield [[], [], []]; + yield [ + [new Award('award1', null, [])], + [], + [new Message(MessageType::INFO, 'Award "award1" not found in second file, but has no team ID\'s in first file', null, null)], + ]; + yield [ + [], + [new Award('award2', null, [])], + [new Message(MessageType::INFO, 'Award "award2" not found in first file, but has no team ID\'s in second file', null, null)], + ]; + yield [ + [new Award('award3', null, ["1", "2", "3"])], + [], + [new Message(MessageType::ERROR, 'Award "award3" not found in second file', null, null)], + ]; + yield [ + [], + [new Award('award4', null, ["1", "2", "3"])], + [new Message(MessageType::ERROR, 'Award "award4" not found in first file', null, null)], + ]; + yield [ + [new Award('award1', 'citation1', [])], + [new Award('award1', 'citation2', [])], + [new Message(MessageType::WARNING, 'Award "award1" has different citation', 'citation1', 'citation2')], + ]; + yield [ + [new Award('award1', 'citation1', ["1", "2"])], + [new Award('award1', 'citation1', ["2", "3"])], + [new Message(MessageType::ERROR, 'Award "award1" has different team ID\'s', '1, 2', '2, 3')], + ]; + } +} diff --git a/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php b/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php new file mode 100644 index 0000000000..4c5124ef01 --- /dev/null +++ b/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php @@ -0,0 +1,77 @@ +createMock(SerializerInterface::class); + $compareService = new ResultsCompareService($serializer); + + $compareService->compare($results1, $results2); + $messages = $compareService->getMessages(); + + self::assertEquals($expectedMessages, $messages); + } + + public function provideCompare(): Generator + { + yield [[], [], []]; + yield [ + [new ResultRow('team1', 1, '', 0, 0, 0)], + [], + [new Message(MessageType::ERROR, 'Team "team1" not found in second file', null, null)], + ]; + yield [ + [], + [new ResultRow('team2', 1, '', 0, 0, 0)], + [new Message(MessageType::ERROR, 'Team "team2" not found in first file', null, null)], + ]; + yield [ + [new ResultRow('team3', 1, '', 0, 0, 0)], + [new ResultRow('team3', 2, '', 0, 0, 0)], + [new Message(MessageType::ERROR, 'Team "team3" has different rank', '1', '2')], + ]; + yield [ + [new ResultRow('team4', 1, 'award1', 0, 0, 0)], + [new ResultRow('team4', 1, 'award2', 0, 0, 0)], + [new Message(MessageType::ERROR, 'Team "team4" has different award', 'award1', 'award2')], + ]; + yield [ + [new ResultRow('team5', 1, 'award3', 1, 0, 0)], + [new ResultRow('team5', 1, 'award3', 2, 0, 0)], + [new Message(MessageType::ERROR, 'Team "team5" has different num solved', '1', '2')], + ]; + yield [ + [new ResultRow('team6', 1, 'award4', 1, 100, 0)], + [new ResultRow('team6', 1, 'award4', 1, 200, 0)], + [new Message(MessageType::ERROR, 'Team "team6" has different total time', '100', '200')], + ]; + yield [ + [new ResultRow('team7', 1, 'award4', 1, 100, 10)], + [new ResultRow('team7', 1, 'award4', 1, 100, 20)], + [new Message(MessageType::ERROR, 'Team "team7" has different last time', '10', '20')], + ]; + yield [ + [new ResultRow('team8', 1, 'award4', 1, 100, 10, 'winner1')], + [new ResultRow('team8', 1, 'award4', 1, 100, 10, 'winner2')], + [new Message(MessageType::WARNING, 'Team "team8" has different group winner', 'winner1', 'winner2')], + ]; + } +} diff --git a/webapp/tests/Unit/Service/Compare/ScoreboardCompareServiceTest.php b/webapp/tests/Unit/Service/Compare/ScoreboardCompareServiceTest.php new file mode 100644 index 0000000000..84879451e8 --- /dev/null +++ b/webapp/tests/Unit/Service/Compare/ScoreboardCompareServiceTest.php @@ -0,0 +1,188 @@ +createMock(SerializerInterface::class); + $compareService = new ScoreboardCompareService($serializer); + + $compareService->compare($scoreboard1, $scoreboard2); + $messages = $compareService->getMessages(); + + self::assertEquals($expectedMessages, $messages); + } + + public function provideCompare(): Generator + { + yield [new Scoreboard(), new Scoreboard(), []]; + yield [ + new Scoreboard('123'), + new Scoreboard('456'), + [new Message(MessageType::INFO, 'Event ID does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456'), + new Scoreboard('123', '123'), + [new Message(MessageType::INFO, 'Time does not match', '456', '123')], + ]; + yield [ + new Scoreboard('123', '456', '111'), + new Scoreboard('123', '456', '222'), + [new Message(MessageType::INFO, 'Contest time does not match', '111', '222')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(started: '123')), + new Scoreboard('123', '456', '111', new ContestState(started: '456')), + [new Message(MessageType::WARNING, 'State started does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(ended: '123')), + new Scoreboard('123', '456', '111', new ContestState(ended: '456')), + [new Message(MessageType::WARNING, 'State ended does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(frozen: '123')), + new Scoreboard('123', '456', '111', new ContestState(frozen: '456')), + [new Message(MessageType::WARNING, 'State frozen does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(thawed: '123')), + new Scoreboard('123', '456', '111', new ContestState(thawed: '456')), + [new Message(MessageType::WARNING, 'State thawed does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(finalized: '123')), + new Scoreboard('123', '456', '111', new ContestState(finalized: '456')), + [new Message(MessageType::WARNING, 'State finalized does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(endOfUpdates: '123')), + new Scoreboard('123', '456', '111', new ContestState(endOfUpdates: '456')), + [new Message(MessageType::WARNING, 'State end of updates does not match', '123', '456')], + ]; + yield [ + new Scoreboard(rows: []), + new Scoreboard(rows: [new Row(1, '123', new Score(0), [])]), + [new Message(MessageType::ERROR, 'Number of rows does not match', '0', '1')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(0), [])]), + new Scoreboard(rows: [new Row(1, '456', new Score(0), [])]), + [new Message(MessageType::ERROR, 'Row 0: team ID does not match', '123', '456')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(0), [])]), + new Scoreboard(rows: [new Row(2, '123', new Score(0), [])]), + [new Message(MessageType::ERROR, 'Row 0: rank does not match', '1', '2')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [])]), + new Scoreboard(rows: [new Row(1, '123', new Score(2), [])]), + [new Message(MessageType::ERROR, 'Row 0: num solved does not match', '1', '2')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1, 123), [])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1, 456), [])]), + [new Message(MessageType::ERROR, 'Row 0: total time does not match', '123', '456')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + [], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, false), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a solved does not match', '1', '')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a solved in first file, but not found in second file')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a solved in second file, but not found in first file')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 2, 0, true), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a num judged does not match', '1', '2')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 3, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 4, true), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a num pending does not match', '3', '4')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 3, true, 123), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 3, true, 456), + ])]), + [new Message(MessageType::INFO, 'Row 0: Problem a time does not match', '123', '456')], + ]; + // PC^2 uses different problem ID's. Also test on `Id = {problemId}-{digits}` + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'Id = a-123', 1, 0, true), + ])]), + [], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'Id = a-123', 1, 0, true), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem Id = a-123 solved in second file, but not found in first file')], + ]; + } +} diff --git a/webapp/tests/Unit/Service/ImportExportServiceTest.php b/webapp/tests/Unit/Service/ImportExportServiceTest.php index 1c478db4f3..1808ccb316 100644 --- a/webapp/tests/Unit/Service/ImportExportServiceTest.php +++ b/webapp/tests/Unit/Service/ImportExportServiceTest.php @@ -2,8 +2,13 @@ namespace App\Tests\Unit\Service; +use App\DataFixtures\Test\TeamWithExternalIdEqualsOneFixture; +use App\DataFixtures\Test\TeamWithExternalIdEqualsTwoFixture; +use App\DataTransferObject\ResultRow; use App\Entity\Contest; use App\Entity\ContestProblem; +use App\Entity\Language; +use App\Entity\Problem; use App\Entity\Team; use App\Entity\TeamAffiliation; use App\Entity\TeamCategory; @@ -11,14 +16,26 @@ use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\ImportExportService; +use App\Service\ScoreboardService; +use App\Tests\Unit\BaseTestCase; +use App\Utils\Utils; +use Collator; +use DateInterval; use DateTime; +use DateTimeImmutable; +use DateTimeInterface; use Doctrine\ORM\EntityManagerInterface; use Generator; +use Ramsey\Uuid\Uuid; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Serializer\Encoder\CsvEncoder; +use Symfony\Component\Serializer\SerializerInterface; -class ImportExportServiceTest extends KernelTestCase +class ImportExportServiceTest extends BaseTestCase { protected function setUp(): void { @@ -332,6 +349,8 @@ public function provideImportProblemsDataSuccess(): Generator public function testImportAccountsTsvSuccess(): void { + $this->loadFixtures([TeamWithExternalIdEqualsOneFixture::class, TeamWithExternalIdEqualsTwoFixture::class]); + // We test all account types twice: // - Team without postfix // - Team with postfix @@ -382,13 +401,13 @@ public function testImportAccountsJsonSuccess(): void name: Team 1 password: password1 type: team - team_id: 1 + team_id: domjudge - id: team2 username: team2 name: Team 2 password: password2 type: team - team_id: 2 + team_id: exteam ip: 1.2.3.4 - id: judge1 username: judge1 @@ -452,6 +471,43 @@ public function testImportAccountsJsonSuccess(): void $this->testImportAccounts($importCount, $message, false); } + public function testImportAccountsJsonError(): void + { + $accounts = <<get(EntityManagerInterface::class); + $preCount = $em->getRepository(User::class)->count([]); + + $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'accounts-yaml'); + file_put_contents($fileName, $accounts); + $file = new UploadedFile($fileName, 'accounts.yaml'); + /** @var ImportExportService $importExportService */ + $importExportService = static::getContainer()->get(ImportExportService::class); + $importCount = $importExportService->importJson('accounts', $file, $message); + // Remove the file, we don't need it anymore. + unlink($fileName); + + self::assertEquals(0, $importCount); + self::assertMatchesRegularExpression('/Only alphanumeric characters and _-@. are allowed/', $message); + + $postCount = $em->getRepository(User::class)->count([]); + self::assertEquals($preCount, $postCount); + } + protected function testImportAccounts(int $importCount, ?string $message, bool $forTsv): void { $expectedUsers = [ @@ -784,6 +840,81 @@ public function testImportTeamsJson(): void } } + public function testImportTeamsJsonError(): void + { + $teamsData = <<get(EntityManagerInterface::class); + $preCount = $em->getRepository(Team::class)->count([]); + + $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'teams-json'); + file_put_contents($fileName, $teamsData); + $file = new UploadedFile($fileName, 'teams.json'); + /** @var ImportExportService $importExportService */ + $importExportService = static::getContainer()->get(ImportExportService::class); + $importCount = $importExportService->importJson('teams', $file, $message); + // Remove the file, we don't need it anymore. + unlink($fileName); + + self::assertMatchesRegularExpression('/name: This value should not be blank./', $message); + self::assertEquals(0, $importCount); + + $postCount = $em->getRepository(Team::class)->count([]); + self::assertEquals($preCount, $postCount); + } + + public function testImportTeamsJsonErrorEmptyString(): void + { + $teamsData = <<get(EntityManagerInterface::class); + $preCount = $em->getRepository(Team::class)->count([]); + + $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'teams-json'); + file_put_contents($fileName, $teamsData); + $file = new UploadedFile($fileName, 'teams.json'); + /** @var ImportExportService $importExportService */ + $importExportService = static::getContainer()->get(ImportExportService::class); + $importCount = $importExportService->importJson('teams', $file, $message); + // Remove the file, we don't need it anymore. + unlink($fileName); + + self::assertMatchesRegularExpression('/name: This value should not be blank./', $message); + self::assertEquals(0, $importCount); + + $postCount = $em->getRepository(Team::class)->count([]); + self::assertEquals($preCount, $postCount); + } + public function testImportGroupsTsv(): void { // Example from the manual @@ -892,6 +1023,41 @@ public function testImportGroupsJson(): void } } + public function testImportGroupsJsonError(): void + { + // Example from the manual + $groupsData = <<get(EntityManagerInterface::class); + $preCount = $em->getRepository(TeamCategory::class)->count([]); + + $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'groups-json'); + file_put_contents($fileName, $groupsData); + $file = new UploadedFile($fileName, 'groups.json'); + /** @var ImportExportService $importExportService */ + $importExportService = static::getContainer()->get(ImportExportService::class); + $importCount = $importExportService->importJson('groups', $file, $message); + // Remove the file, we don't need it anymore. + unlink($fileName); + + self::assertMatchesRegularExpression('/name: This value should not be blank/', $message); + self::assertEquals(0, $importCount); + + $postCount = $em->getRepository(TeamCategory::class)->count([]); + self::assertEquals($preCount, $postCount); + } + + public function testImportOrganizationsJson(): void { // Example from the manual @@ -952,17 +1118,212 @@ public function testImportOrganizationsJson(): void } } + public function testImportOrganizationsErrorJson(): void + { + // Example from the manual + $organizationsData = <<get(EntityManagerInterface::class); + $preCount = $em->getRepository(TeamAffiliation::class)->count([]); + + $fileName = tempnam(static::getContainer()->get(DOMJudgeService::class)->getDomjudgeTmpDir(), 'organizations-json'); + file_put_contents($fileName, $organizationsData); + $file = new UploadedFile($fileName, 'organizations.json'); + /** @var ImportExportService $importExportService */ + $importExportService = static::getContainer()->get(ImportExportService::class); + $importCount = $importExportService->importJson('organizations', $file, $message); + // Remove the file, we don't need it anymore. + unlink($fileName); + + self::assertMatchesRegularExpression('/ISO3166-1 alpha-3 values are allowed/', $message); + self::assertEquals(0, $importCount); + + $postCount = $em->getRepository(TeamAffiliation::class)->count([]); + self::assertEquals($preCount, $postCount); + } + protected function getContest(int|string $cid): Contest { // First clear the entity manager to have all data. static::getContainer()->get(EntityManagerInterface::class)->clear(); - $config = static::getContainer()->get(ConfigurationService::class); - $dataSource = $config->get('data_source'); - if ($dataSource === DOMJudgeService::DATA_SOURCE_LOCAL) { - return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Contest::class)->find($cid); - } else { - return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Contest::class)->findOneBy(['externalid' => $cid]); + return static::getContainer()->get(EntityManagerInterface::class)->getRepository(Contest::class)->findOneBy(['externalid' => $cid]); + } + + /** + * @dataProvider provideGetResultsData + */ + public function testGetResultsData(bool $full, bool $honors, string $dataSet, string $expectedResultsFile): void + { + // Set up some results we can test with + // This data is based on the ICPC World Finals 47 + /** @var EntityManagerInterface $em */ + $em = static::getContainer()->get(EntityManagerInterface::class); + + $startTime = new DateTimeImmutable('2023-05-01 08:00:00'); + + $medalData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/' . $dataSet . '/sample-medals.json'), true); + + $contest = (new Contest()) + ->setName('ICPC World Finals 47') + ->setShortname('wf47') + ->setStarttimeString($startTime->format(DateTimeInterface::ATOM)) + ->setEndtimeString($startTime->add(new DateInterval('PT5H'))->format(DateTimeInterface::ATOM)) + ->setMedalsEnabled(true) + ->setGoldMedals($medalData['medals']['gold']) + ->SetSilverMedals($medalData['medals']['silver']) + ->setBronzeMedals($medalData['medals']['bronze']); + + $groupsById = []; + $groupsData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/' . $dataSet . '/sample-groups.json'), true); + foreach ($groupsData as $groupData) { + $group = (new TeamCategory()) + ->setExternalid($groupData['id']) + ->setName($groupData['name']) + ->setSortorder(37); + $em->persist($group); + $em->flush(); + $groupsById[$group->getExternalid()] = $group; + if (in_array($group->getExternalid(), $medalData['medal_categories'], true)) { + $contest->addMedalCategory($group); + } + } + + $em->persist($contest); + $em->flush(); + + $teamsData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/'. $dataSet . '/sample-teams.json'), true); + /** @var array $teamsById */ + $teamsById = []; + /** @var array $teamsByIcpcId */ + $teamsByIcpcId = []; + foreach ($teamsData as $teamData) { + $team = (new Team()) + ->setExternalid($teamData['id']) + ->setIcpcid($teamData['icpc_id']) + ->setName($teamData['name']) + ->setDisplayName($teamData['display_name']) + ->setCategory($groupsById[$teamData['group_ids'][0]]); + $em->persist($team); + $em->flush(); + $teamsById[$team->getExternalid()] = $team; + $teamsByIcpcId[$team->getIcpcId()] = $team; + } + + $problemsData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/'. $dataSet . '/sample-problems.json'), true); + $contestProblemsById = []; + foreach ($problemsData as $problemData) { + $problem = (new Problem()) + ->setExternalid($problemData['id']) + ->setName($problemData['name']); + $contestProblem = (new ContestProblem()) + ->setProblem($problem) + ->setContest($contest) + ->setColor($problemData['rgb']) + ->setShortname($problemData['label']); + $em->persist($problem); + $em->persist($contestProblem); + $em->flush(); + $contestProblemsById[$contestProblem->getExternalid()] = $contestProblem; } + + $cpp = $em->getRepository(Language::class)->find('cpp'); + + // We use direct queries here to speed this up + $submissionInsertQuery = $em->getConnection()->prepare('INSERT INTO submission (teamid, cid, probid, langid, submittime) VALUES (:teamid, :cid, :probid, :langid, :submittime)'); + $judgingInsertQuery = $em->getConnection()->prepare('INSERT INTO judging (uuid, submitid, result) VALUES (:uuid, :submitid, :result)'); + + $submissionInsertQuery->bindValue('cid', $contest->getCid()); + $submissionInsertQuery->bindValue('langid', $cpp->getLangid()); + + $scoreboardData = json_decode(file_get_contents(__DIR__ . '/../Fixtures/'. $dataSet . '/sample-scoreboard.json'), true); + foreach ($scoreboardData['rows'] as $scoreboardRow) { + $team = $teamsById[$scoreboardRow['team_id']]; + $submissionInsertQuery->bindValue('teamid', $team->getTeamid()); + foreach ($scoreboardRow['problems'] as $problemData) { + if ($problemData['solved']) { + $contestProblem = $contestProblemsById[$problemData['problem_id']]; + // Add fake submission for this problem. First add wrong ones + for ($i = 0; $i < $problemData['num_judged'] - 1; $i++) { + $submissionInsertQuery->bindValue('probid', $contestProblem->getProbid()); + $submissionInsertQuery->bindValue('submittime', $startTime + ->add(new DateInterval('PT' . $problemData['time'] . 'M')) + ->sub(new DateInterval('PT1M')) + ->getTimestamp()); + $submissionInsertQuery->executeQuery(); + $submitId = $em->getConnection()->lastInsertId(); + $judgingInsertQuery->bindValue('uuid', Uuid::uuid4()->toString()); + $judgingInsertQuery->bindValue('submitid', $submitId); + $judgingInsertQuery->bindValue('result', 'wrong-awnser'); + $judgingInsertQuery->executeQuery(); + } + // Add correct submission + $submissionInsertQuery->bindValue('probid', $contestProblem->getProbid()); + $submissionInsertQuery->bindValue('submittime', $startTime + ->add(new DateInterval('PT' . $problemData['time'] . 'M')) + ->getTimestamp()); + $submissionInsertQuery->executeQuery(); + $submitId = $em->getConnection()->lastInsertId(); + $judgingInsertQuery->bindValue('uuid', Uuid::uuid4()->toString()); + $judgingInsertQuery->bindValue('submitid', $submitId); + $judgingInsertQuery->bindValue('result', 'correct'); + $judgingInsertQuery->executeQuery(); + } + } + } + + /** @var ScoreboardService $scoreboardService */ + $scoreboardService = static::getContainer()->get(ScoreboardService::class); + $scoreboardService->refreshCache($contest); + + /** @var ImportExportService $importExportService */ + $importExportService = static::getContainer()->get(ImportExportService::class); + + /** @var RequestStack $requestStack */ + $requestStack = static::getContainer()->get(RequestStack::class); + $request = new Request(); + $request->cookies->set('domjudge_cid', (string)$contest->getCid()); + $requestStack->push($request); + + $results = $importExportService->getResultsData(37, $full, $honors); + + $resultsContents = file_get_contents(__DIR__ . '/../Fixtures/' . $dataSet . '/' . $expectedResultsFile); + $resultsContents = substr($resultsContents, strpos($resultsContents, "\n") + 1); + // Prefix file with a fake header, so we can deserialize them + $resultsContents = "team_id\trank\taward\tnum_solved\ttotal_time\ttime_of_last_submission\tgroup_winner\n" . $resultsContents; + + $serializer = static::getContainer()->get(SerializerInterface::class); + + $expectedResults = $serializer->deserialize($resultsContents, ResultRow::class . '[]', 'csv', [ + CsvEncoder::DELIMITER_KEY => "\t", + ]); + + self::assertEquals($expectedResults, $results); + } + + public function provideGetResultsData(): Generator + { + yield [true, true, 'wf', 'results-full-honors.tsv']; + yield [false, true, 'wf', 'results-wf-honors.tsv']; + yield [true, false, 'wf', 'results-full-ranked.tsv']; + yield [false, false, 'wf', 'results-wf-ranked.tsv']; + yield [true, true, 'sample', 'results-full-honors.tsv']; + yield [false, true, 'sample', 'results-wf-honors.tsv']; + yield [true, false, 'sample', 'results-full-ranked.tsv']; + yield [false, true, 'sample', 'results-wf-honors.tsv']; } } diff --git a/webapp/tests/bootstrap.php b/webapp/tests/bootstrap.php index 0b7f3aa422..e828dea351 100644 --- a/webapp/tests/bootstrap.php +++ b/webapp/tests/bootstrap.php @@ -2,7 +2,7 @@ use Symfony\Component\Dotenv\Dotenv; -require dirname(__DIR__, 2) . '/lib/vendor/autoload.php'; +require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/config/load_db_secrets.php'; if (file_exists(dirname(__DIR__) . '/config/bootstrap.php')) {