Skip to content

Commit

Permalink
Add option to upload contest text/statement.
Browse files Browse the repository at this point in the history
  • Loading branch information
nickygerritsen committed Mar 22, 2024
1 parent ac40ef6 commit 487b253
Show file tree
Hide file tree
Showing 23 changed files with 636 additions and 50 deletions.
19 changes: 19 additions & 0 deletions misc-tools/import-contest.in
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ def import_contest_banner(cid: str):
else:
print('Skipping contest banner import.')

def import_contest_text(cid: str):
"""Import the contest text"""

files = ['contest.pdf', 'contest-web.pdf', 'contest.html', 'contest.txt']

text_file = None
for file in files:
if os.path.isfile(file):
text_file = file
break

if text_file:
if dj_utils.confirm(f'Import {text_file} for contest?', False):
dj_utils.upload_file(f'contests/{cid}/text', 'text', text_file)
print('Contest text imported.')
else:
print('Skipping contest text import.')

if len(sys.argv) == 1:
dj_utils.domjudge_webapp_folder_or_api_url = webappdir
elif len(sys.argv) == 2:
Expand Down Expand Up @@ -154,6 +172,7 @@ else:
if cid is not None:
print(f' -> cid={cid}')
import_contest_banner(cid)
import_contest_text(cid)

# Problem import is also special: we need to upload each individual problem and detect what they are
if os.path.exists('problems.yaml') or os.path.exists('problems.json') or os.path.exists('problemset.yaml'):
Expand Down
40 changes: 40 additions & 0 deletions webapp/migrations/Version20240322100827.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240322100827 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add contest text table and type to contests.';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE contest_text_content (cid INT UNSIGNED NOT NULL COMMENT \'Contest ID\', content LONGBLOB NOT NULL COMMENT \'Text content(DC2Type:blobtext)\', PRIMARY KEY(cid)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'Stores contents of contest texts\' ');
$this->addSql('ALTER TABLE contest_text_content ADD CONSTRAINT FK_6680FE6A4B30D9C4 FOREIGN KEY (cid) REFERENCES contest (cid) ON DELETE CASCADE');
$this->addSql('ALTER TABLE contest ADD contest_text_type VARCHAR(4) DEFAULT NULL COMMENT \'File type of contest text\'');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE contest_text_content DROP FOREIGN KEY FK_6680FE6A4B30D9C4');
$this->addSql('DROP TABLE contest_text_content');
$this->addSql('ALTER TABLE contest DROP contest_text_type');
}

public function isTransactional(): bool
{
return false;
}
}
115 changes: 115 additions & 0 deletions webapp/src/Controller/API/ContestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,121 @@ public function setBannerAction(Request $request, string $cid, ValidatorInterfac
return new Response('', Response::HTTP_NO_CONTENT);
}

/**
* Delete the text for the given contest.
*/
#[IsGranted('ROLE_ADMIN')]
#[Rest\Delete('/{cid}/text', name: 'delete_contest_text')]
#[OA\Response(response: 204, description: 'Deleting text succeeded')]
#[OA\Parameter(ref: '#/components/parameters/cid')]
public function deleteTextAction(Request $request, string $cid): Response
{
$contest = $this->getContestAndCheckIfLocked($request, $cid);
$contest->setClearContestText(true);
$contest->processContestText();
$this->em->flush();

$this->eventLogService->log('contests', $contest->getCid(), EventLogService::ACTION_UPDATE,
$contest->getCid());

return new Response('', Response::HTTP_NO_CONTENT);
}

/**
* Set the text for the given contest.
*/
#[IsGranted('ROLE_ADMIN')]
#[Rest\Post("/{cid}/text", name: 'post_contest_text')]
#[Rest\Put("/{cid}/text", name: 'put_contest_text')]
#[OA\RequestBody(
required: true,
content: new OA\MediaType(
mediaType: 'multipart/form-data',
schema: new OA\Schema(
required: ['text'],
properties: [
new OA\Property(
property: 'text',
description: 'The text to use, as either text/html, text/plain or application/pdf.',
type: 'string',
format: 'binary'
),
]
)
)
)]
#[OA\Response(response: 204, description: 'Setting text succeeded')]
#[OA\Parameter(ref: '#/components/parameters/cid')]
public function setTextAction(Request $request, string $cid, ValidatorInterface $validator): Response
{
$contest = $this->getContestAndCheckIfLocked($request, $cid);

/** @var UploadedFile|null $text */
$text = $request->files->get('text');
if (!$text) {
return new JsonResponse(['title' => 'Validation failed', 'errors' => ['Please supply a text']], Response::HTTP_BAD_REQUEST);
}
if (!in_array($text->getMimeType(), ['text/html', 'text/plain', 'application/pdf'])) {
return new JsonResponse(['title' => 'Validation failed', 'errors' => ['Invalid text type']], Response::HTTP_BAD_REQUEST);
}

$contest->setContestTextFile($text);

if ($errorResponse = $this->responseForErrors($validator->validate($contest), true)) {
return $errorResponse;
}

$contest->processContestText();
$this->em->flush();

$this->eventLogService->log('contests', $contest->getCid(), EventLogService::ACTION_UPDATE,
$contest->getCid());

return new Response('', Response::HTTP_NO_CONTENT);
}

/**
* Get the text for the given contest.
*/
#[Rest\Get('/{cid}/text', name: 'contest_text')]
#[OA\Response(
response: 200,
description: 'Returns the given contest text in PDF, HTML or TXT format',
content: [
new OA\MediaType(mediaType: 'application/pdf'),
new OA\MediaType(mediaType: 'text/plain'),
new OA\MediaType(mediaType: 'text/html'),
]
)]
#[OA\Parameter(ref: '#/components/parameters/cid')]
public function textAction(Request $request, string $cid): Response
{
/** @var Contest|null $contest */
$contest = $this->getQueryBuilder($request)
->andWhere(sprintf('%s = :id', $this->getIdField()))
->setParameter('id', $cid)
->getQuery()
->getOneOrNullResult();

$hasAccess = $this->dj->checkrole('jury') ||
$this->dj->checkrole('api_reader') ||
$contest->getFreezeData()->started();

if (!$hasAccess) {
throw new AccessDeniedHttpException();
}

if ($contest === null) {
throw new NotFoundHttpException(sprintf('Object with ID \'%s\' not found', $cid));
}

if (!$contest->getContestTextType()) {
throw new NotFoundHttpException(sprintf('Contest with ID \'%s\' has no text', $cid));
}

return $contest->getContestTextStreamedResponse();
}

/**
* Change the start time or unfreeze (thaw) time of the given contest.
* @throws NonUniqueResultException
Expand Down
8 changes: 8 additions & 0 deletions webapp/src/Controller/BaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ protected function saveEntity(
): void {
$auditLogType = Utils::tableForEntity($entity);

// 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);
foreach ($metadata->lifecycleCallbacks['prePersist'] ?? [] as $prePersistMethod) {
$entity->$prePersistMethod();
}

$entityManager->persist($entity);
$entityManager->flush();

Expand Down
25 changes: 25 additions & 0 deletions webapp/src/Controller/Jury/ContestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Expand Down Expand Up @@ -169,6 +170,7 @@ public function indexAction(Request $request): Response
return $this->redirectToRoute('jury_contests');
}

/** @var Contest[] $contests */
$contests = $em->createQueryBuilder()
->select('c')
->from(Contest::class, 'c')
Expand Down Expand Up @@ -244,6 +246,18 @@ public function indexAction(Request $request): Response
}
}

// Create action links
if ($contest->getContestTextType()) {
$contestactions[] = [
'icon' => 'file-' . $contest->getContestTextType(),
'title' => 'view contest description',
'link' => $this->generateUrl('jury_contest_text', [
'cid' => $contest->getCid(),
])
];
} else {
$contestactions[] = [];
}
if ($this->isGranted('ROLE_ADMIN') && !$contest->isLocked()) {
$contestactions[] = [
'icon' => 'edit',
Expand Down Expand Up @@ -987,4 +1001,15 @@ public function publicScoreboardDataZipAction(
}
return $this->dj->getScoreboardZip($request, $requestStack, $contest, $scoreboardService, $type === 'unfrozen');
}

#[Route(path: '/{cid<\d+>}/text', name: 'jury_contest_text')]
public function viewTextAction(int $cid): StreamedResponse
{
$contest = $this->em->getRepository(Contest::class)->find($cid);
if (!$contest) {
throw new NotFoundHttpException(sprintf('Contest with ID %s not found', $cid));
}

return $contest->getContestTextStreamedResponse();
}
}
2 changes: 1 addition & 1 deletion webapp/src/Controller/Jury/ProblemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public function indexAction(): Response
if ($p->getProblemtextType()) {
$problemactions[] = [
'icon' => 'file-' . $p->getProblemtextType(),
'title' => 'view problem description',
'title' => 'view all problem statements of the contest',
'link' => $this->generateUrl('jury_problem_text', [
'probId' => $p->getProbid(),
])
Expand Down
10 changes: 10 additions & 0 deletions webapp/src/Controller/PublicController.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ public function problemTextAction(int $probId): StreamedResponse
});
}

#[Route(path: '/contest-text', name: 'public_contest_text')]
public function contestTextAction(): StreamedResponse
{
$contest = $this->dj->getCurrentContest(onlyPublic: true);
if (!$contest->getFreezeData()->started()) {
throw new NotFoundHttpException('Contest text not found or not available');
}
return $contest->getContestTextStreamedResponse();
}

/**
* @throws NonUniqueResultException
*/
Expand Down
16 changes: 16 additions & 0 deletions webapp/src/Controller/Team/MiscController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use App\Controller\BaseController;
use App\DataTransferObject\SubmissionRestriction;
use App\Entity\Clarification;
use App\Entity\Contest;
use App\Entity\ContestProblem;
use App\Entity\Language;
use App\Form\Type\PrintType;
use App\Service\ConfigurationService;
Expand All @@ -16,6 +18,9 @@
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\HttpFoundation\File\UploadedFile;
Expand Down Expand Up @@ -211,4 +216,15 @@ public function docsAction(): Response
{
return $this->render('team/docs.html.twig');
}

#[Route(path: '/contest-text', name: 'team_contest_text')]
public function contestTextAction(): StreamedResponse
{
$user = $this->dj->getUser();
$contest = $this->dj->getCurrentContest($user->getTeam()->getTeamid());
if (!$contest->getFreezeData()->started()) {
throw new NotFoundHttpException('Contest text not found or not available');
}
return $contest->getContestTextStreamedResponse();
}
}
Loading

0 comments on commit 487b253

Please sign in to comment.