Skip to content

Commit 9c8549e

Browse files
committed
add Paste code to submit feature
1 parent bd5f9e5 commit 9c8549e

File tree

5 files changed

+286
-23
lines changed

5 files changed

+286
-23
lines changed

etc/db-config.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@
201201
- category: Display
202202
description: Options related to the DOMjudge user interface.
203203
items:
204+
- name: default_submission_code_mode
205+
type: int
206+
default_value: 0
207+
public: true
208+
description: Select the default submission method for the team
209+
options:
210+
0: Paste
211+
1: Upload
204212
- name: output_display_limit
205213
type: int
206214
default_value: 2000

webapp/src/Controller/Team/SubmissionController.php

+94-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Entity\Submission;
1010
use App\Entity\Testcase;
1111
use App\Form\Type\SubmitProblemType;
12+
use App\Form\Type\SubmitProblemPasteType;
1213
use App\Service\ConfigurationService;
1314
use App\Service\DOMJudgeService;
1415
use App\Service\EventLogService;
@@ -60,34 +61,111 @@ public function createAction(Request $request, ?Problem $problem = null): Respon
6061
if ($problem !== null) {
6162
$data['problem'] = $problem;
6263
}
63-
$form = $this->formFactory
64+
$formUpload = $this->formFactory
6465
->createBuilder(SubmitProblemType::class, $data)
6566
->setAction($this->generateUrl('team_submit'))
6667
->getForm();
6768

68-
$form->handleRequest($request);
69+
$formPaste = $this->formFactory
70+
->createBuilder(SubmitProblemPasteType::class, $data)
71+
->setAction($this->generateUrl('team_submit'))
72+
->getForm();
6973

70-
if ($form->isSubmitted() && $form->isValid()) {
74+
$formUpload->handleRequest($request);
75+
$formPaste->handleRequest($request);
76+
77+
if ($formUpload->isSubmitted() && $formUpload->isValid()) {
7178
if ($contest === null) {
7279
$this->addFlash('danger', 'No active contest');
7380
} elseif (!$this->dj->checkrole('jury') && !$contest->getFreezeData()->started()) {
7481
$this->addFlash('danger', 'Contest has not yet started');
7582
} else {
7683
/** @var Problem $problem */
77-
$problem = $form->get('problem')->getData();
84+
$problem = $formUpload->get('problem')->getData();
7885
/** @var Language $language */
79-
$language = $form->get('language')->getData();
86+
$language = $formUpload->get('language')->getData();
8087
/** @var UploadedFile[] $files */
81-
$files = $form->get('code')->getData();
88+
$files = $formUpload->get('code')->getData();
8289
if (!is_array($files)) {
8390
$files = [$files];
8491
}
85-
$entryPoint = $form->get('entry_point')->getData() ?: null;
92+
$entryPoint = $formUpload->get('entry_point')->getData() ?: null;
8693
$submission = $this->submissionService->submitSolution(
87-
$team, $this->dj->getUser(), $problem->getProbid(), $contest, $language, $files, 'team page', null,
88-
null, $entryPoint, null, null, $message
94+
$team,
95+
$this->dj->getUser(),
96+
$problem->getProbid(),
97+
$contest,
98+
$language,
99+
$files,
100+
'team page',
101+
null,
102+
null,
103+
$entryPoint,
104+
null,
105+
null,
106+
$message
107+
);
108+
109+
if ($submission) {
110+
$this->addFlash(
111+
'success',
112+
'Submission done! Watch for the verdict in the list below.'
113+
);
114+
} else {
115+
$this->addFlash('danger', $message);
116+
}
117+
return $this->redirectToRoute('team_index');
118+
}
119+
} elseif ($formPaste->isSubmitted() && $formPaste->isValid()) {
120+
if ($contest === null) {
121+
$this->addFlash('danger', 'No active contest');
122+
} elseif (!$this->dj->checkrole('jury') && !$contest->getFreezeData()->started()) {
123+
$this->addFlash('danger', 'Contest has not yet started');
124+
} else {
125+
$problem = $formPaste->get('problem')->getData();
126+
$language = $formPaste->get('language')->getData();
127+
$codeContent = $formPaste->get('code_content')->getData();
128+
if($codeContent == null || empty(trim($codeContent))) {
129+
$this->addFlash('danger','No code content provided.');
130+
return $this->redirectToRoute('team_index');
131+
}
132+
$tempDir = sys_get_temp_dir();
133+
$tempFileName = sprintf(
134+
'submission_%s_%s_%s.%s',
135+
$user->getUsername(),
136+
$problem->getName(),
137+
date('Y-m-d_H-i-s'),
138+
$language->getExtensions()[0]
89139
);
140+
$tempFileName = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $tempFileName);
141+
$tempFilePath = $tempDir . DIRECTORY_SEPARATOR . $tempFileName;
142+
file_put_contents($tempFilePath, $codeContent);
90143

144+
$uploadedFile = new UploadedFile(
145+
$tempFilePath,
146+
$tempFileName,
147+
'application/octet-stream',
148+
null,
149+
true
150+
);
151+
152+
$files = [$uploadedFile];
153+
$entryPoint = $formPaste->get('entry_point')->getData() ?: null;
154+
$submission = $this->submissionService->submitSolution(
155+
$team,
156+
$this->dj->getUser(),
157+
$problem,
158+
$contest,
159+
$language,
160+
$files,
161+
'team page',
162+
null,
163+
null,
164+
$entryPoint,
165+
null,
166+
null,
167+
$message
168+
);
91169
if ($submission) {
92170
$this->addFlash(
93171
'success',
@@ -96,11 +174,17 @@ public function createAction(Request $request, ?Problem $problem = null): Respon
96174
} else {
97175
$this->addFlash('danger', $message);
98176
}
177+
99178
return $this->redirectToRoute('team_index');
100179
}
101180
}
102181

103-
$data = ['form' => $form->createView(), 'problem' => $problem];
182+
$data = [
183+
'formupload' => $formUpload->createView(),
184+
'formpaste' => $formPaste->createView(),
185+
'problem' => $problem,
186+
'defaultSubmissionCodeMode' => (bool) $this->config->get('default_submission_code_mode'),
187+
];
104188
$data['validFilenameRegex'] = SubmissionService::FILENAME_REGEX;
105189

106190
if ($request->isXmlHttpRequest()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Form\Type;
4+
5+
use App\Entity\Language;
6+
use App\Entity\Problem;
7+
use App\Service\ConfigurationService;
8+
use App\Service\DOMJudgeService;
9+
use Doctrine\ORM\EntityManagerInterface;
10+
use Doctrine\ORM\EntityRepository;
11+
use Doctrine\ORM\Query\Expr\Join;
12+
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
13+
use Symfony\Component\Form\AbstractType;
14+
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
15+
use Symfony\Component\Form\Extension\Core\Type\TextType;
16+
use Symfony\Component\Form\Form;
17+
use Symfony\Component\Form\FormBuilderInterface;
18+
use Symfony\Component\Form\FormEvent;
19+
use Symfony\Component\Form\FormEvents;
20+
use Symfony\Component\Validator\Constraints\Callback;
21+
use Symfony\Component\Validator\Context\ExecutionContextInterface;
22+
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
23+
24+
class SubmitProblemPasteType extends AbstractType
25+
{
26+
public function __construct(
27+
protected readonly DOMJudgeService $dj,
28+
protected readonly ConfigurationService $config,
29+
protected readonly EntityManagerInterface $em
30+
) {
31+
}
32+
33+
public function buildForm(FormBuilderInterface $builder, array $options): void
34+
{
35+
$user = $this->dj->getUser();
36+
$contest = $this->dj->getCurrentContest($user->getTeam()->getTeamid());
37+
38+
$builder->add('code_content', HiddenType::class, [
39+
'required' => true,
40+
]);
41+
$problemConfig = [
42+
'class' => Problem::class,
43+
'query_builder' => fn(EntityRepository $er) => $er->createQueryBuilder('p')
44+
->join('p.contest_problems', 'cp', Join::WITH, 'cp.contest = :contest')
45+
->select('p', 'cp')
46+
->andWhere('cp.allowSubmit = 1')
47+
->setParameter('contest', $contest)
48+
->addOrderBy('cp.shortname'),
49+
'choice_label' => fn(Problem $problem) => sprintf(
50+
'%s - %s',
51+
$problem->getContestProblems()->first()->getShortName(),
52+
$problem->getName()
53+
),
54+
'placeholder' => 'Select a problem',
55+
];
56+
$builder->add('problem', EntityType::class, $problemConfig);
57+
58+
$builder->add('language', EntityType::class, [
59+
'class' => Language::class,
60+
'query_builder' => fn(EntityRepository $er) => $er
61+
->createQueryBuilder('l')
62+
->andWhere('l.allowSubmit = 1'),
63+
'choice_label' => 'name',
64+
'placeholder' => 'Select a language',
65+
]);
66+
67+
$builder->add('entry_point', TextType::class, [
68+
'label' => 'Entry point',
69+
'required' => false,
70+
'help' => 'The entry point for your code.',
71+
'row_attr' => ['data-entry-point' => ''],
72+
'constraints' => [
73+
new Callback(function ($value, ExecutionContextInterface $context) {
74+
/** @var Form $form */
75+
$form = $context->getRoot();
76+
/** @var Language $language */
77+
$language = $form->get('language')->getData();
78+
if ($language && $language->getRequireEntryPoint() && empty($value)) {
79+
$message = sprintf(
80+
'%s required, but not specified',
81+
$language->getEntryPointDescription() ?: 'Entry point'
82+
);
83+
$context
84+
->buildViolation($message)
85+
->atPath('entry_point')
86+
->addViolation();
87+
}
88+
}),
89+
]
90+
]);
91+
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($problemConfig) {
92+
$data = $event->getData();
93+
if (isset($data['problem'])) {
94+
$problemConfig += ['row_attr' => ['class' => 'd-none']];
95+
$event->getForm()->add('problem', EntityType::class, $problemConfig);
96+
}
97+
});
98+
}
99+
}

webapp/templates/base.html.twig

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
<script src="{{ asset("js/bootstrap.bundle.min.js") }}"></script>
1515

1616
<script src="{{ asset("js/domjudge.js") }}"></script>
17+
<script src="{{ asset('js/ace/ace.js') }}"></script>
18+
1719
{% for file in customAssetFiles('js') %}
1820
<script src="{{ asset('js/custom/' ~ file) }}"></script>
1921
{% endfor %}

webapp/templates/team/submit_modal.html.twig

+83-13
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,64 @@
1919
{% include 'partials/alert.html.twig' with {'type': 'danger', 'message': 'Submissions (temporarily) disabled.'} %}
2020
</div>
2121
{% else %}
22-
{{ form_start(form) }}
22+
{% set active_tab = defaultSubmissionCodeMode == 0 ? 'paste' : 'upload' %}
23+
24+
<!-- Bootstrap Nav Tabs for Switching -->
2325
<div class="modal-body">
24-
{{ form_row(form.code) }}
25-
<div class="alert d-none" id="files_selected"></div>
26-
{{ form_row(form.problem) }}
27-
{{ form_row(form.language) }}
28-
{{ form_row(form.entry_point) }}
29-
</div>
30-
<div class="modal-footer">
31-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
32-
<button type="submit" class="btn-success btn">
33-
<i class="fas fa-cloud-upload-alt"></i> Submit
34-
</button>
26+
<ul class="nav nav-tabs container text-center" id="submissionTabs" role="tablist" style="width: 100%">
27+
<li class="nav-item" role="presentation">
28+
<a class="nav-link {% if active_tab == 'upload' %}active{% endif %}" id="upload-tab" data-bs-toggle="tab" href="#upload" role="tab" aria-controls="upload" aria-selected="{% if active_tab == 'upload' %}true{% else %}false{% endif %}">Upload File</a>
29+
</li>
30+
<li class="nav-item text-center" role="presentation">
31+
<a class="nav-link {% if active_tab == 'paste' %}active{% endif %}" id="paste-tab" data-bs-toggle="tab" href="#paste" role="tab" aria-controls="paste" aria-selected="{% if active_tab == 'paste' %}true{% else %}false{% endif %}">Paste Code</a>
32+
</li>
33+
</ul>
34+
<div class="tab-content" id="submissionTabsContent" style="margin-top: 20px;">
35+
<!-- File Upload Tab -->
36+
<div class="tab-pane fade {% if active_tab == 'upload' %}show active{% endif %}" id="upload" role="tabpanel" aria-labelledby="upload-tab">
37+
{{ form_start(formupload) }}
38+
{{ form_row(formupload.code) }}
39+
<div class="alert d-none" id="files_selected"></div>
40+
{{ form_row(formupload.problem) }}
41+
{{ form_row(formupload.language) }}
42+
{{ form_row(formupload.entry_point) }}
43+
<div class="modal-footer">
44+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
45+
<button type="submit" class="btn btn-success">
46+
<i class="fas fa-cloud-upload-alt"></i> Submit Upload
47+
</button>
48+
</div>
49+
{{ form_end(formupload) }}
50+
</div>
51+
52+
<!-- Paste Code Tab -->
53+
<div class="tab-pane fade {% if active_tab == 'paste' %}show active{% endif %}" id="paste" role="tabpanel" aria-labelledby="paste-tab">
54+
{{ form_start(formpaste) }}
55+
{{ form_widget(formpaste.code_content) }}
56+
<label for="codeInput">Paste your code here:</label>
57+
<div class="editor-container">
58+
{{ "" | codeEditor(
59+
"_team_submission_code",
60+
"c_cpp",
61+
true,
62+
formpaste.code_content.vars.id,
63+
null,
64+
formpaste.language.vars.value
65+
) }}
66+
</div>
67+
{{ form_row(formpaste.problem) }}
68+
{{ form_row(formpaste.language) }}
69+
{{ form_row(formpaste.entry_point) }}
70+
<div class="modal-footer">
71+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
72+
<button type="submit" class="btn btn-primary">
73+
<i class="fas fa-paste"></i> Submit Paste
74+
</button>
75+
</div>
76+
{{ form_end(formpaste) }}
77+
</div>
78+
</div>
3579
</div>
36-
{{ form_end(form) }}
3780
{% endif %}
3881
</div>
3982
</div>
@@ -80,4 +123,31 @@
80123
filesSelected.removeClass('d-none');
81124
});
82125
</script>
126+
<style>
127+
.container {
128+
display: grid;
129+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
130+
gap: 10px;
131+
}
132+
133+
.text-center {
134+
text-align: center;
135+
}
136+
137+
.editor-container {
138+
border: 1px solid #ddd;
139+
border-radius: 4px;
140+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
141+
padding: 10px;
142+
margin-top: 10px;
143+
margin-bottom: 10px;
144+
background-color: #fafafa;
145+
max-height: 400px;
146+
overflow: auto;
147+
}
148+
149+
.editor-container:hover {
150+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
151+
}
152+
</style>
83153
</div>

0 commit comments

Comments
 (0)