diff --git a/etc/db-config.yaml b/etc/db-config.yaml index 665a48a6e8..d5bec41e6c 100644 --- a/etc/db-config.yaml +++ b/etc/db-config.yaml @@ -201,6 +201,14 @@ - category: Display description: Options related to the DOMjudge user interface. items: + - name: default_submission_code_mode + type: array_val + default_value: [upload] + public: true + description: Select the default submission method for the team in the webinterface + options: + - paste + - upload - name: output_display_limit type: int default_value: 2000 diff --git a/webapp/public/style_domjudge.css b/webapp/public/style_domjudge.css index e443f7271c..f23bcac05b 100644 --- a/webapp/public/style_domjudge.css +++ b/webapp/public/style_domjudge.css @@ -699,3 +699,30 @@ blockquote { padding: 3px; border-radius: 5px; } + +#submissionTabs.container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; +} + +.editor-container { + border: 1px solid #ddd; + border-radius: 4px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + padding: 10px; + margin-top: 10px; + margin-bottom: 10px; + background-color: #fafafa; + max-height: 400px; + overflow: auto; +} + +.editor-container:hover { + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); +} + +.single-tab { + width: 100%; + display: inline-block; +} diff --git a/webapp/src/Controller/Team/SubmissionController.php b/webapp/src/Controller/Team/SubmissionController.php index 35e1a382e7..ba4ba7df86 100644 --- a/webapp/src/Controller/Team/SubmissionController.php +++ b/webapp/src/Controller/Team/SubmissionController.php @@ -9,6 +9,7 @@ use App\Entity\Submission; use App\Entity\Testcase; use App\Form\Type\SubmitProblemType; +use App\Form\Type\SubmitProblemPasteType; use App\Service\ConfigurationService; use App\Service\DOMJudgeService; use App\Service\EventLogService; @@ -60,47 +61,135 @@ public function createAction(Request $request, ?Problem $problem = null): Respon if ($problem !== null) { $data['problem'] = $problem; } - $form = $this->formFactory + $formUpload = $this->formFactory ->createBuilder(SubmitProblemType::class, $data) ->setAction($this->generateUrl('team_submit')) ->getForm(); - $form->handleRequest($request); + $formPaste = $this->formFactory + ->createBuilder(SubmitProblemPasteType::class, $data) + ->setAction($this->generateUrl('team_submit')) + ->getForm(); - if ($form->isSubmitted() && $form->isValid()) { + $formUpload->handleRequest($request); + $formPaste->handleRequest($request); + if ($formUpload->isSubmitted() || $formPaste->isSubmitted()) { if ($contest === null) { $this->addFlash('danger', 'No active contest'); } elseif (!$this->dj->checkrole('jury') && !$contest->getFreezeData()->started()) { $this->addFlash('danger', 'Contest has not yet started'); } else { - /** @var Problem $problem */ - $problem = $form->get('problem')->getData(); - /** @var Language $language */ - $language = $form->get('language')->getData(); - /** @var UploadedFile[] $files */ - $files = $form->get('code')->getData(); - if (!is_array($files)) { - $files = [$files]; + $problem = null; + $language = null; + $files = []; + $entryPoint = null; + $message = ''; + + if ($formUpload->isSubmitted() && $formUpload->isValid()) { + $problem = $formUpload->get('problem')->getData(); + $language = $formUpload->get('language')->getData(); + $files = $formUpload->get('code')->getData(); + if (!is_array($files)) { + $files = [$files]; + } + $entryPoint = $formUpload->get('entry_point')->getData() ?: null; + } elseif ($formPaste->isSubmitted() && $formPaste->isValid()) { + $problem = $formPaste->get('problem')->getData(); + $language = $formPaste->get('language')->getData(); + $codeContent = $formPaste->get('code_content')->getData(); + $problemShortName = $problem->getContestProblems()->first()->getShortName(); + + if ($codeContent == null || empty(trim($codeContent))) { + $this->addFlash('danger', 'No code content provided.'); + return $this->redirectToRoute('team_index'); + } + + $saveFileDir = sys_get_temp_dir(); + $saveFileName = sprintf( + '%s.%s', + $problemShortName, + $language->getExtensions()[0] + ); + $saveFileName = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $saveFileName); + + if ($language->getExtensions()[0] == 'java' || $language->getExtensions()[0] == 'kt') { + $entryPoint = $formPaste->get('entry_point')->getData() ?: null; + // Check for invalid characters in entry point name + $invalidChars = '/[<>:"\/\\|?*]/'; + if (preg_match($invalidChars, $entryPoint)) { + $this->addFlash('danger', 'Invalid entry point name.'); + return $this->redirectToRoute('team_index'); + } + $saveFileName = $entryPoint . '.' . $language->getExtensions()[0]; + } else { + $entryPoint = $saveFileName; + } + + $saveFilePath = $saveFileDir . DIRECTORY_SEPARATOR . $saveFileName; + file_put_contents($saveFilePath, $codeContent); + + $uploadedFile = new UploadedFile( + $saveFilePath, + $saveFileName, + 'application/octet-stream', + null, + true + ); + $files = [$uploadedFile]; } - $entryPoint = $form->get('entry_point')->getData() ?: null; - $submission = $this->submissionService->submitSolution( - $team, $this->dj->getUser(), $problem->getProbid(), $contest, $language, $files, 'team page', null, - null, $entryPoint, null, null, $message - ); - - if ($submission) { - $this->addFlash( - 'success', - 'Submission done! Watch for the verdict in the list below.' + + if ($problem && $language && !empty($files)) { + $submission = $this->submissionService->submitSolution( + $team, + $this->dj->getUser(), + $problem->getProbid(), + $contest, + $language, + $files, + 'team page', + null, + null, + $entryPoint, + null, + null, + $message ); - } else { - $this->addFlash('danger', $message); + + if ($submission) { + $this->addFlash('success', 'Submission done! Watch for the verdict in the list below.'); + } else { + $this->addFlash('danger', $message); + } + + return $this->redirectToRoute('team_index'); } - return $this->redirectToRoute('team_index'); + } + } + + $active_tab_array = $this->config->get('default_submission_code_mode'); + $active_tab = ""; + if (count($active_tab_array) == 1) { + $active_tab = reset($active_tab_array); + $this->dj->setCookie('active_tab', $active_tab); + } + else if ($this->dj->getCookie('active_tab') != null) { + $cookie_active_tab = $this->dj->getCookie('active_tab'); + if(in_array($cookie_active_tab, $active_tab_array)) { + $active_tab = $cookie_active_tab; + } + else { + $active_tab = reset($active_tab_array); + $this->dj->setCookie('active_tab', $active_tab); } } - $data = ['form' => $form->createView(), 'problem' => $problem]; + $data = [ + 'formupload' => $formUpload->createView(), + 'formpaste' => $formPaste->createView(), + 'active_tab' => $active_tab, + 'active_tab_array' => $active_tab_array, + 'problem' => $problem, + ]; $data['validFilenameRegex'] = SubmissionService::FILENAME_REGEX; if ($request->isXmlHttpRequest()) { diff --git a/webapp/src/Form/Type/SubmitProblemPasteType.php b/webapp/src/Form/Type/SubmitProblemPasteType.php new file mode 100644 index 0000000000..c4195b7421 --- /dev/null +++ b/webapp/src/Form/Type/SubmitProblemPasteType.php @@ -0,0 +1,108 @@ +dj->getUser(); + $contest = $this->dj->getCurrentContest($user->getTeam()->getTeamid()); + + $builder->add('code_content', HiddenType::class, [ + 'required' => true, + ]); + $problemConfig = [ + 'class' => Problem::class, + 'query_builder' => fn(EntityRepository $er) => $er->createQueryBuilder('p') + ->join('p.contest_problems', 'cp', Join::WITH, 'cp.contest = :contest') + ->select('p', 'cp') + ->andWhere('cp.allowSubmit = 1') + ->setParameter('contest', $contest) + ->addOrderBy('cp.shortname'), + 'choice_label' => fn(Problem $problem) => sprintf( + '%s - %s', + $problem->getContestProblems()->first()->getShortName(), + $problem->getName() + ), + 'placeholder' => 'Select a problem', + ]; + $builder->add('problem', EntityType::class, $problemConfig); + + $builder->add('language', EntityType::class, [ + 'class' => Language::class, + 'query_builder' => fn(EntityRepository $er) => $er + ->createQueryBuilder('l') + ->andWhere('l.allowSubmit = 1'), + 'choice_label' => 'name', + 'placeholder' => 'Select a language', + ]); + + $builder->add('entry_point', TextType::class, [ + 'label' => 'Entry point', + 'required' => false, + 'help' => 'The entry point for your code.', + 'row_attr' => ['data-entry-point' => ''], + 'constraints' => [ + new Callback(function ($value, ExecutionContextInterface $context) { + /** @var Form $form */ + $form = $context->getRoot(); + /** @var Language $language */ + $language = $form->get('language')->getData(); + $langId = strtolower($language->getExtensions()[0]); + if ($language->getRequireEntryPoint() && empty($value)) { + $message = sprintf('%s required, but not specified', + $language->getEntryPointDescription() ?: 'Entry point'); + $context + ->buildViolation($message) + ->atPath('entry_point') + ->addViolation(); + } + + if (in_array($langId, ['java', 'kt']) && empty($value)) { + $message = sprintf('%s is required for %s language, but not specified', + $language->getEntryPointDescription() ?: 'Entry point', + ucfirst($langId)); + $context + ->buildViolation($message) + ->atPath('entry_point') + ->addViolation(); + } + }), + ] + ]); + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($problemConfig) { + $data = $event->getData(); + if (isset($data['problem'])) { + $problemConfig += ['row_attr' => ['class' => 'd-none']]; + $event->getForm()->add('problem', EntityType::class, $problemConfig); + } + }); + } +} diff --git a/webapp/templates/base.html.twig b/webapp/templates/base.html.twig index 895750ede8..6ccf9845de 100644 --- a/webapp/templates/base.html.twig +++ b/webapp/templates/base.html.twig @@ -14,6 +14,8 @@ + + {% for file in customAssetFiles('js') %} {% endfor %} diff --git a/webapp/templates/team/partials/submit_scripts.html.twig b/webapp/templates/team/partials/submit_scripts.html.twig index bf0bdb1af6..f408729e8f 100644 --- a/webapp/templates/team/partials/submit_scripts.html.twig +++ b/webapp/templates/team/partials/submit_scripts.html.twig @@ -55,6 +55,43 @@ } } + function maybeShowEntryPointByPasteCode() { + var langid = $('#submit_problem_paste_language').val(); + console.log($('#submit_problem_paste_problem')); + if (langid === "") { + return; + } + + var $entryPoint = $('[data-entry-point]'); + var $entryPointLabel = $entryPoint.find('label'); + var $entryPointInput = $entryPoint.find('input'); + var entryPointDescription = getEntryPoint(langid); + + if (langid === 'java' || langid === 'kt') { + $entryPoint.show(); + $entryPointInput.attr('required', 'required'); + } else if (entryPointDescription) { + $entryPoint.show(); + $entryPointInput.attr('required', 'required'); + } else { + $entryPoint.hide(); + $entryPointInput.attr('required', null); + } + + if (entryPointDescription) { + var $labelChildren = $entryPointLabel.children(); + $entryPointLabel.text(entryPointDescription).append($labelChildren); + } + + if (langid === 'java') { + $entryPointInput.val(entryPointDetectJava('main.java')); + } else if (langid === 'kt') { + $entryPointInput.val(entryPointDetectKt('main.kt')); + } else { + $entryPointInput.val(''); + } + } + $(function () { var $entryPoint = $('[data-entry-point]'); $entryPoint.hide(); @@ -104,6 +141,14 @@ maybeShowEntryPoint(); }); + $body.on('change', '#submit_problem_paste_language', function () { + maybeShowEntryPointByPasteCode(); + }); + + $body.on('change', '#submit_problem_paste_problem', function () { + maybeShowEntryPointByPasteCode(); + }); + $body.on('submit', 'form[name=submit_problem]', function () { var langelt = document.getElementById("submit_problem_language"); var language = langelt.options[langelt.selectedIndex].value; diff --git a/webapp/templates/team/submit.html.twig b/webapp/templates/team/submit.html.twig index af6ce61722..be969d98e2 100644 --- a/webapp/templates/team/submit.html.twig +++ b/webapp/templates/team/submit.html.twig @@ -14,16 +14,16 @@ {% else %} - {{ form_start(form) }} - {{ form_row(form.code) }} - {{ form_row(form.problem) }} - {{ form_row(form.language) }} - {{ form_row(form.entry_point) }} + {{ form_start(formupload) }} + {{ form_row(formupload.code) }} + {{ form_row(formupload.problem) }} + {{ form_row(formupload.language) }} + {{ form_row(formupload.entry_point) }}