Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/webstandard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ jobs:
run: .github/jobs/baseinstall.sh ${{ matrix.role }}
- name: Run webstandard tests (W3C, WCAG)
run: .github/jobs/webstandard.sh ${{ matrix.test }} ${{ matrix.role }}
- name: dump the db
if: ${{ !cancelled() }}
run: mysqldump -uroot -proot --quick --max_allowed_packet=1024M domjudge > /tmp/db.sql
- name: Upload database dump for debugging
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: DB-dump
path: /tmp/db.sql
- name: Upload all logs/artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
Expand Down
200 changes: 200 additions & 0 deletions webapp/public/js/domjudge.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ function setDiffMode(value)
localStorage.setItem('domjudge_editor_diff_mode', value);
}

function getDiffTag()
{
let diffTag = localStorage.getItem('domjudge_editor_diff_tag');
if (diffTag === undefined) {
return 'no-diff';
}
return diffTag;
}

function setDiffTag(value)
{
localStorage.setItem('domjudge_editor_diff_tag', value);
}

// 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
Expand Down Expand Up @@ -1290,3 +1304,189 @@ function initScoreboardSubmissions() {
});
});
}

const editors = [];
function initDiffEditor(editorId) {
const wrapper = $(`#${editorId}-wrapper`);

const initialTag = getDiffTag();
const select = wrapper.find(".diff-select");
for (let i = 0; i < select[0].options.length; i++) {
if (select[0].options[i].dataset.tag == initialTag) {
select[0].selectedIndex = i;
break;
}
}
// Fall back to other tagged diff if preferred tag is not available for this submission.
if (initialTag !== "no-diff" && select[0].selectedIndex === 0) {
for (let i = 1; i < select[0].options.length; i++) {
if (select[0].options[i].dataset.tag) {
select[0].selectedIndex = i;
break;
}
}
}

const initialDiffMode = getDiffMode();
const radios = wrapper.find(`.diff-mode > input[type='radio']`);
radios.each((_, radio) => {
radio.checked = radio.value === initialDiffMode
});

const download = wrapper.find(".download")[0];
const edit = wrapper.find(".edit")[0];
const updateTabRank = (rank) => {
if (rank) {
let url = new URL(download.href);
url.searchParams.set("fetch", rank);
download.href = url;

url = new URL(edit.href);
url.searchParams.set("rank", rank);
edit.href = url;
} else {
download.href = "#";
edit.href = "#";
}
};
wrapper.find(".nav").on('show.bs.tab', (e) => {
updateTabRank(e.target.dataset.rank);
})

const editor = {
'getDiffMode': () => {
for (let radio of radios) {
if (radio.checked) {
return radio.value;
}
}
},
'getDiffSelection': () => {
let s = select[0];
return s.options[s.selectedIndex].value;
},
'updateIcon': (rank, icon) => {
const element = wrapper.find(".nav-link[data-rank]")[rank].querySelector('.fa-fw');
element.className = 'fas fa-fw fa-' + icon;
},
'onDiffModeChange': (f) => {
radios.change((e) => {
const diffMode = e.target.value;
f(diffMode);
});
},
'onDiffSelectChange': (f) => {
select.change((e) => {
const submitId = e.target.value;
const noDiff = submitId === "";
f(submitId, noDiff);
});
}
};
editors[editorId] = editor;

const updateMode = (diffMode) => {
setDiffMode(diffMode);
};
updateMode(initialDiffMode);
editor.onDiffModeChange(updateMode);

const updateSelect = (submitId, noDiff) => {
radios.each((_, radio) => {
radio.disabled = noDiff;
});

const selected = select[0].options[select[0].selectedIndex];
if (selected && selected.dataset.tag) {
setDiffTag(selected.dataset.tag);
}

// TODO: add tab panes for deleted source files.
};
updateSelect(select[0].value, select[0].value === "");
editor.onDiffSelectChange(updateSelect);
}

function initDiffEditorTab(editorId, diffId, rank, models, modifiedModel) {
const empty = monaco.editor.getModel(monaco.Uri.file("empty")) ?? monaco.editor.createModel("", undefined, monaco.Uri.file("empty"));

const diffEditor = monaco.editor.createDiffEditor(
document.getElementById(diffId), {
scrollbar: {
alwaysConsumeMouseWheel: false,
vertical: 'auto',
horizontal: 'auto'
},
scrollBeyondLastLine: false,
automaticLayout: true,
readOnly: true,
theme: getCurrentEditorTheme(),
});

const updateMode = (diffMode) => {
diffEditor.updateOptions({
renderSideBySide: diffMode === 'side-by-side',
});
};
editors[editorId].onDiffModeChange(updateMode);

const updateSelect = (submitId, noDiff) => {
if (!noDiff) {
const model = models[submitId];
if (model === undefined) {
models[submitId] = {'model': empty};
} else if (model !== undefined && !model['model']) {
// TODO: show source code instead of diff to empty file?
model['model'] = monaco.editor.createModel(model['source'], undefined, monaco.Uri.file("test/" + submitId + "/" + model['filename']));
}
}

diffEditor.updateOptions({
renderOverviewRuler: !noDiff,
});
if (noDiff) {
diffEditor.updateOptions({
renderSideBySide: false,
});
} else {
// Reset the diff mode to the currently selected mode.
updateMode(editors[editorId].getDiffMode())
}
// TODO: handle single-file submission case with renamed file.
const oldViewState = diffEditor.saveViewState();
diffEditor.setModel({
original: noDiff ? modifiedModel : models[submitId]['model'],
modified: modifiedModel,
});
diffEditor.restoreViewState(oldViewState);

diffEditor.getOriginalEditor().updateOptions({
lineNumbers: !noDiff,
});
diffEditor.getModifiedEditor().updateOptions({
minimap: {
enabled: noDiff,
},
})
};
editors[editorId].onDiffSelectChange(updateSelect);
updateSelect(editors[editorId].getDiffSelection(), editors[editorId].getDiffSelection() === "");

const updateIcon = () => {
const noDiff = editors[editorId].getDiffSelection() === "";
if (noDiff) {
editors[editorId].updateIcon(rank, 'file');
return;
}

const lineChanges = diffEditor.getLineChanges();
if (diffEditor.getModel().original == empty) {
editors[editorId].updateIcon(rank, 'file-circle-plus');
} else if (lineChanges !== null && lineChanges.length > 0) {
editors[editorId].updateIcon(rank, 'file-circle-exclamation');
} else {
editors[editorId].updateIcon(rank, 'file-circle-check');
}
}
diffEditor.onDidUpdateDiff(updateIcon);
}
43 changes: 18 additions & 25 deletions webapp/src/Controller/Jury/SubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -849,22 +849,10 @@ public function sourceAction(
->getQuery()
->getResult();

$originalSubmission = $originalFiles = null;

if ($submission->getOriginalSubmission()) {
/** @var Submission $originalSubmission */
$originalSubmission = $this->em->getRepository(Submission::class)->find($submission->getOriginalSubmission()->getSubmitid());

/** @var SubmissionFile[] $files */
$originalFiles = $this->em->createQueryBuilder()
->from(SubmissionFile::class, 'file')
->select('file')
->andWhere('file.submission = :submission')
->setParameter('submission', $originalSubmission)
->orderBy('file.ranknumber')
->getQuery()
->getResult();

$otherSubmissions = [];
$originalSubmission = $submission->getOriginalSubmission();
if ($originalSubmission) {
$otherSubmissions[] = $originalSubmission;
/** @var Submission $oldSubmission */
$oldSubmission = $this->em->createQueryBuilder()
->from(Submission::class, 's')
Expand Down Expand Up @@ -900,30 +888,35 @@ public function sourceAction(
->getQuery()
->getOneOrNullResult();
}
if ($oldSubmission !== null) {
$otherSubmissions[] = $oldSubmission;
}

/** @var SubmissionFile[] $files */
$oldFiles = $this->em->createQueryBuilder()
->from(SubmissionFile::class, 'file')
->select('file')
->andWhere('file.submission = :submission')
->setParameter('submission', $oldSubmission)
->orderBy('file.ranknumber')
->andWhere('file.submission in (:submissions)')
->setParameter('submissions', array_map(fn($s) => $s->getSubmitid(), $otherSubmissions))
->orderBy('file.submission, file.ranknumber')
->getQuery()
->getResult();

$oldFileStats = $oldFiles !== null ? $this->determineFileChanged($files, $oldFiles) : [];
$originalFileStats = $originalFiles !== null ? $this->determineFileChanged($files, $originalFiles) : [];
$otherFiles = [];
foreach ($oldFiles as $f) {
$submitId = $f->getSubmission()->getSubmitid();
$otherFiles[$submitId] ??= [];
$otherFiles[$submitId][] = $f;
}

return $this->render('jury/submission_source.html.twig', [
'submission' => $submission,
'files' => $files,
'oldSubmission' => $oldSubmission,
'oldFiles' => $oldFiles,
'oldFileStats' => $oldFileStats,
'originalSubmission' => $originalSubmission,
'originalFiles' => $originalFiles,
'originalFileStats' => $originalFileStats,
'allowEdit' => $this->allowEdit(),
'otherSubmissions' => $otherSubmissions,
'otherFiles' => $otherFiles,
]);
}

Expand Down
Loading
Loading