563 lines
23 KiB
HTML
563 lines
23 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block js_media %}
|
|
<script type="text/javascript">
|
|
window.valid_files = {{valid_files_json}};
|
|
window.big_input = (window.valid_files.length > 100);
|
|
</script>
|
|
<script type="text/javascript" src="{{ static('jquery-ui.min.js') }}"></script>
|
|
<script type="text/javascript" src="{{ static('libs/featherlight/featherlight.min.js') }}"></script>
|
|
<script type="text/javascript" src="{{ static('fine-uploader/jquery.fine-uploader.js') }}"></script>
|
|
<script type="text/javascript">
|
|
$(function () {
|
|
$("#problem-data-zipfile_fine_uploader").fineUploader({
|
|
request: {
|
|
endpoint: "{{url('problem_zip_upload', problem.code)}}",
|
|
params: {
|
|
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
|
}
|
|
},
|
|
chunking: {
|
|
enabled: true,
|
|
partSize: 40000000,
|
|
},
|
|
resume: {
|
|
enabled: true
|
|
},
|
|
validation: {
|
|
allowedExtensions: ['zip'],
|
|
},
|
|
}).on('complete', function (event, id, name, responseJSON) {
|
|
console.log(responseJSON);
|
|
if (!responseJSON.success) {
|
|
alert('Fail to upload: ' + responseJSON.error);
|
|
}
|
|
else {
|
|
$('#submit-button').click();
|
|
}
|
|
});;
|
|
|
|
function update_select2() {
|
|
$('tbody:not(.extra-row-body) .type-column select').select2({
|
|
minimumResultsForSearch: -1
|
|
});
|
|
}
|
|
|
|
$("#id_problem-data-checker").select2();
|
|
update_select2();
|
|
|
|
function autofill_if_exists($select, file) {
|
|
if (!$select.val() && ~window.valid_files.indexOf(file))
|
|
$select.val(file).trigger('change');
|
|
}
|
|
|
|
var $table = $('#case-table');
|
|
$table.on('add-row', function (e, $tr) {
|
|
// update_select2();
|
|
$tr.find('input').filter('[id$=file]').each(function () {
|
|
var $select, val = $(this).replaceWith($select = $('<select>').attr({
|
|
id: $(this).attr('id'),
|
|
name: $(this).attr('name'),
|
|
style: 'width: 100%'
|
|
})).val();
|
|
$select.select2({
|
|
data: window.big_input ? [val] : window.valid_files,
|
|
allowClear: true,
|
|
placeholder: ''
|
|
}).val(val).trigger('change').on('change', function () {
|
|
var val = $select.val();
|
|
if (val) {
|
|
if ($select.attr('id').endsWith('input_file'))
|
|
autofill_if_exists($tr.find('select[id$=output_file]'), val.replace(/in(?!.*?in)/, 'out'));
|
|
else
|
|
autofill_if_exists($tr.find('select[id$=input_file]'), val.replace(/out(?!.*?out)/, 'in'));
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
var order = 0;
|
|
|
|
function handle_table_reorder() {
|
|
var in_batch = false;
|
|
$table.find('tbody:first tr').each(function () {
|
|
switch ($(this).attr('data-type')) {
|
|
case 'C':
|
|
$(this).find('input[id$=pretest]').toggle(!in_batch);
|
|
break;
|
|
case 'S':
|
|
in_batch = true;
|
|
break;
|
|
case 'E':
|
|
in_batch = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
function try_parse_json(json) {
|
|
try {
|
|
return JSON.parse(json);
|
|
} catch (e) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function swap_row($a, $b) {
|
|
var $a_order = $a.find('input[id$=order]'), $b_order = $b.find('input[id$=order]');
|
|
var order = $a_order.val();
|
|
$a_order.val($b_order.val());
|
|
$b_order.val(order);
|
|
$b.after($a);
|
|
$a.find('span.order').text($a_order.val());
|
|
$b.find('span.order').text($b_order.val());
|
|
handle_table_reorder();
|
|
}
|
|
|
|
function checker_precision($checker) {
|
|
var $td = $checker.parent();
|
|
var $args = $td.closest('table').find('#id_problem-data-checker_args');
|
|
var $precision = $('<input>', {
|
|
type: 'number',
|
|
value: try_parse_json($args.val()).precision || 6,
|
|
title: 'precision (decimal digits)',
|
|
style: 'width: 4em'
|
|
}).change(function () {
|
|
if ($checker.val().startsWith('floats'))
|
|
$args.val(JSON.stringify({precision: parseInt($(this).val())}));
|
|
else
|
|
$args.val('');
|
|
}).appendTo($td);
|
|
|
|
$checker.change(function () {
|
|
$precision.toggle($checker.val().startsWith('floats')).change();
|
|
}).change();
|
|
}
|
|
|
|
(function toggle_custom() {
|
|
let $checker = $('#id_problem-data-checker')
|
|
|
|
let $custom_checker = $('#id_problem-data-custom_checker')
|
|
let $validator = $('#id_problem-data-custom_validator')
|
|
let $interactive = $('#id_problem-data-interactive_judge')
|
|
|
|
$tr_checker = $custom_checker.parent().parent();
|
|
$tr_validator = $validator.parent().parent()
|
|
$tr_interactive = $interactive.parent().parent()
|
|
|
|
$td = $checker.parent();
|
|
var $sample = $("<a/>",{
|
|
text: "{{_('Instruction')}}",
|
|
style: "margin-left:3em;",
|
|
target: "_blank",
|
|
href: "{{url('custom_checker_sample')}}"
|
|
}).appendTo($td);
|
|
|
|
$checker.change(function () {
|
|
$tr_checker.toggle($checker.val() == 'custom').change();
|
|
$tr_validator.toggle($checker.val() == 'customval' || $checker.val() == 'testlib').change();
|
|
$tr_interactive.toggle($checker.val() == 'interact').change();
|
|
|
|
$sample.toggle(['custom', 'customval', 'interact'].includes($checker.val())).change();
|
|
}).change();
|
|
})();
|
|
|
|
checker_precision($('#id_problem-data-checker'));
|
|
|
|
$table.on('add-row', function (e, $tr) {
|
|
var $order = $tr.find('input').filter('[id$=order]').attr('type', 'hidden').val(++order);
|
|
$order.after($('<span>', {'class': 'order'}).text($order.val()))
|
|
.after($('<i>', {'class': 'fa fa-fw fa-lg fa-ellipsis-v'}));
|
|
|
|
var $opts = $tr.find('input').slice(2, 6);
|
|
var $files = $tr.find('select').slice(1, 3);
|
|
var $delete = $files.end().last();
|
|
$tr.find('select[id$=type]').change(function () {
|
|
var $this = $(this), val = $this.val(), disabled;
|
|
switch (val) {
|
|
case 'S':
|
|
case 'E':
|
|
disabled = val == 'S';
|
|
$opts.toggle(val == 'S');
|
|
$files.siblings('.select2').hide();
|
|
$delete.toggle(val == 'S');
|
|
break;
|
|
default:
|
|
$opts.toggle(val == 'C');
|
|
$files.siblings('.select2').toggle(val == 'C');
|
|
$delete.toggle(val == 'C');
|
|
var $prevs = $tr.prevAll('tr[data-type=S], tr[data-type=E]');
|
|
disabled = $prevs.length && $prevs.get(0).getAttribute('data-type') == 'S';
|
|
$tr.find('input[id$=pretest]').toggle(val == 'C' && !disabled);
|
|
}
|
|
$tr.attr('data-type', val).nextUntil('tr[data-type=S], tr[data-type=E], tr[data-type=""]')
|
|
.find('input[id$=pretest]').toggle(!disabled);
|
|
}).change();
|
|
|
|
var tooltip_classes = 'tooltipped tooltipped-s';
|
|
$tr.find('a.edit-generator-args').mouseover(function () {
|
|
switch ($tr.attr('data-type')) {
|
|
case 'C':
|
|
case 'S':
|
|
var $this = $(this).addClass(tooltip_classes);
|
|
$this.attr('aria-label', $this.prev().val() || '(none)');
|
|
}
|
|
}).mouseout(function () {
|
|
$(this).removeClass(tooltip_classes).removeAttr('aria-label');
|
|
}).featherlight($('.generator-args-editor'), {
|
|
beforeOpen: function () {
|
|
switch ($tr.attr('data-type')) {
|
|
case 'C':
|
|
case 'S':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
},
|
|
afterOpen: function () {
|
|
var $input = this.$currentTarget.prev();
|
|
this.$instance.find('.generator-args-editor')
|
|
.find('textarea').val($input.val()).end()
|
|
.find('.button').click(function () {
|
|
$input.val($(this).prev().val());
|
|
$.featherlight.current().close();
|
|
}).end()
|
|
.show();
|
|
}
|
|
});
|
|
|
|
checker_precision($tr.find('select[id$=checker]'));
|
|
}).find('tbody:first').find('tr').each(function () {
|
|
$table.trigger('add-row', [$(this)]);
|
|
});
|
|
|
|
$('form').submit(function () {
|
|
$table.find('tbody:first').find('tr').each(function () {
|
|
var filled = false;
|
|
$(this).find('input, select').each(function () {
|
|
var $this = $(this);
|
|
if (!$this.attr('name'))
|
|
return;
|
|
if ($this.attr('type') === 'checkbox')
|
|
filled |= $this.is(':checked');
|
|
else if (!$this.attr('name').endsWith('order'))
|
|
filled |= !!$this.val();
|
|
});
|
|
if (!filled)
|
|
$(this).find('input[id$=order]').val('');
|
|
});
|
|
});
|
|
|
|
var $total = $('#id_cases-TOTAL_FORMS');
|
|
|
|
$('a#add-case-row').click(function () {
|
|
var $tr;
|
|
$table.find('tbody:first').append($tr = $($table.find('.extra-row-body').html()
|
|
.replace(/__prefix__/g, $total.val())));
|
|
$tr.find('.type-column select option[value="C"]').attr('selected', true);
|
|
$total.val(parseInt($total.val()) + 1);
|
|
$table.trigger('add-row', [$tr]);
|
|
window.scrollBy(0, $tr.height());
|
|
return false;
|
|
});
|
|
|
|
function isInpFile(x) {
|
|
let tail = ['.in', '.inp', '.IN', '.INP']
|
|
for (let i of tail) {
|
|
if (x.endsWith(i)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isOutFile(x) {
|
|
let tail = ['.out', '.OUT', '.ans', '.ANS']
|
|
for (let i of tail) {
|
|
if (x.endsWith(i)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
$('a#fill-testcases').click(function (e) {
|
|
e.preventDefault();
|
|
var inFiles = [], outFiles = [];
|
|
for (let file of window.valid_files) {
|
|
if (isInpFile(file)) {
|
|
inFiles.push(file);
|
|
}
|
|
if (isOutFile(file)) {
|
|
outFiles.push(file);
|
|
}
|
|
}
|
|
if (inFiles.length == 0) {
|
|
alert("No input/output files. Make sure your files' suffices are .in(p)/.out");
|
|
return false;
|
|
}
|
|
if (inFiles.length != outFiles.length) {
|
|
alert("The input files do not match the output files!");
|
|
return false;
|
|
}
|
|
|
|
inFiles.sort();
|
|
outFiles.sort();
|
|
|
|
// big number of input
|
|
if (inFiles.length > 100) {
|
|
window.big_input = true;
|
|
}
|
|
|
|
var batch_starts = $('#batch_starts').val();
|
|
batch_starts = batch_starts.split(',');
|
|
batch_starts = batch_starts.filter(e => {
|
|
return e.length && e == +e;
|
|
});
|
|
batch_starts = new Set(batch_starts.map(x => Math.min(Math.max(1, parseInt(x)), inFiles.length - 1)));
|
|
batch_starts.add(inFiles.length + 1);
|
|
|
|
var numRows = inFiles.length + 2 * batch_starts.size - 2;
|
|
while ($total.val() < numRows) {
|
|
$('a#add-case-row').click();
|
|
}
|
|
|
|
// fill cases
|
|
var row = 0;
|
|
for (var i = 0; i < inFiles.length; i++) {
|
|
if (batch_starts.has(i + 1)) {
|
|
$("#id_cases-"+row+"-type").val('S').change();
|
|
row += 1;
|
|
}
|
|
var $input = $("#id_cases-"+row+"-input_file");
|
|
$input.select2('destroy').empty().select2({ data: [inFiles[i]] });
|
|
$input.val(inFiles[i]).change();
|
|
var $output = $("#id_cases-"+row+"-output_file");
|
|
$output.select2('destroy').empty().select2({ data: [outFiles[i]] });
|
|
$output.val(outFiles[i]).change();
|
|
|
|
if ($("#problem-type").val() == "ICPC") {
|
|
$("#id_cases-"+row+"-points").val('0').change();
|
|
}
|
|
else {
|
|
$("#id_cases-"+row+"-points").val('1').change();
|
|
}
|
|
row += 1;
|
|
|
|
if (batch_starts.has(i + 2)) {
|
|
$("#id_cases-"+(row-1)+"-points").val('1').change();
|
|
$("#id_cases-"+row+"-type").val('E').change();
|
|
row += 1;
|
|
}
|
|
}
|
|
update_select2();
|
|
return false;
|
|
});
|
|
|
|
var oldIndex;
|
|
$('#case-table tbody').sortable({
|
|
itemSelector: 'tr',
|
|
handle: 'i.fa-ellipsis-v',
|
|
start: function (event, ui) {
|
|
$item = ui.item;
|
|
oldIndex = $item.index();
|
|
},
|
|
stop: function (event, ui) {
|
|
$item = ui.item;
|
|
var newIndex = $item.index();
|
|
if (newIndex > oldIndex) {
|
|
var order = parseInt($item.parent().children().slice(oldIndex, newIndex).each(function () {
|
|
var $order = $(this).find('input[id$=order]');
|
|
$order.val(parseInt($order.val()) - 1).siblings('span.order').text($order.val());
|
|
}).last().after($item).find('input[id$=order]').val());
|
|
$item.find('input[id$=order]').val(order + 1).siblings('span.order').text(order + 1);
|
|
} else if (newIndex < oldIndex) {
|
|
var order = parseInt($item.parent().children().slice(newIndex + 1, oldIndex + 1).each(function () {
|
|
var $order = $(this).find('input[id$=order]');
|
|
$order.val(parseInt($order.val()) + 1).siblings('span.order').text($order.val());
|
|
}).first().before($item).find('input[id$=order]').val());
|
|
$item.find('input[id$=order]').val(order - 1).siblings('span.order').text(order - 1);
|
|
}
|
|
if (newIndex != oldIndex)
|
|
handle_table_reorder();
|
|
}
|
|
});
|
|
|
|
$('input#delete-all').change(function() {
|
|
if (this.checked) {
|
|
$("input[name$='DELETE']").attr('checked', true);
|
|
}
|
|
else {
|
|
$("input[name$='DELETE']").attr('checked', false);
|
|
}
|
|
});
|
|
|
|
// Change to OI if the first row point > 0
|
|
if($("#id_cases-0-points").val() != '0') $('#problem-type').val('OI');
|
|
$("#problem-type").select2();
|
|
|
|
// Change batch_starts based on current tests
|
|
function update_batch_starts() {
|
|
var numBatches = 0;
|
|
var batchStarts = [];
|
|
$("#case-table tbody:first tr").each(function(idx) {
|
|
$select = $('#id_cases-' + idx + '-type');
|
|
if ($select.val() == 'S') {
|
|
batchStarts.push(idx + 1 - 2 * numBatches);
|
|
numBatches++;
|
|
}
|
|
});
|
|
if (batchStarts) {
|
|
$("#batch_starts").val(batchStarts.join(', '));
|
|
}
|
|
}
|
|
update_batch_starts();
|
|
|
|
}).change();
|
|
</script>
|
|
{% include 'fine_uploader/script.html' %}
|
|
{% endblock %}
|
|
|
|
{% block media %}
|
|
<link href="{{ static ('fine-uploader/fine-uploader.css') }}" rel="stylesheet">
|
|
<style>
|
|
#case-table .select2 {
|
|
text-align: initial;
|
|
}
|
|
|
|
.order-column {
|
|
width: 1em;
|
|
}
|
|
|
|
span.order {
|
|
padding-right: 0.5em;
|
|
}
|
|
|
|
body.dragging, body.dragging * {
|
|
cursor: move !important;
|
|
}
|
|
|
|
.dragged {
|
|
position: absolute;
|
|
opacity: 0.5;
|
|
z-index: 2000;
|
|
}
|
|
|
|
i.fa-ellipsis-v {
|
|
cursor: move;
|
|
}
|
|
|
|
#case-table tbody td {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.type-column {
|
|
width: 8em;
|
|
}
|
|
|
|
ul.errorlist {
|
|
border: 3px red solid;
|
|
border-radius: 5px;
|
|
padding-top: 1em;
|
|
padding-bottom: 1em;
|
|
background: #e99;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block header %}
|
|
{% if data_form.instance.has_yml %}
|
|
<div class="title-line-action">
|
|
[<a href="{{ url('problem_data_init', problem.code) }}">{{ _('View YAML') }}</a>]
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block body %}
|
|
{% if data_form.instance.feedback %}
|
|
<ul class="errorlist">
|
|
<li>{{ data_form.instance.feedback }}</li>
|
|
</ul>
|
|
{% endif %}
|
|
<form class="problem-data-form" action="" method="POST" enctype="multipart/form-data">
|
|
{% csrf_token %}
|
|
{{ cases_formset.management_form }}
|
|
<table class="table">
|
|
{{ data_form.as_table() }}
|
|
<th>
|
|
<label>{{_('Autofill testcases')}}:</label>
|
|
</th>
|
|
<td>
|
|
<div>
|
|
{{_('Problem type')}}:
|
|
<select id="problem-type" style="width: 5em">
|
|
<option value="ICPC">ICPC</option>
|
|
<option value="OI">OI</option>
|
|
</select>
|
|
<a id="fill-testcases" href="#">
|
|
<i class="fa fa-circle"></i>
|
|
{{_('Fill testcases')}}
|
|
</a>
|
|
</div>
|
|
<div style="margin-top: 1em;">
|
|
{{_("Batch start positions")}}:
|
|
<input id="batch_starts">
|
|
</div>
|
|
<div>
|
|
{{_("Leave empty if not use batch. If you want to divide to three batches [1, 4], [5, 8], [9, 10], enter: 1, 5, 9")}}
|
|
</div>
|
|
</td>
|
|
</table>
|
|
<input type="submit" value="{{ _('Apply!') }}" class="button" id="submit-button">
|
|
<table id="case-table" class="table">
|
|
<thead>
|
|
<tr>
|
|
<th class="order-column"></th>
|
|
<th class="type-column">{{ _('Type') }}</th>
|
|
<th>{{ _('Input file') }}</th>
|
|
<th>{{ _('Output file') }}</th>
|
|
<th>{{ _('Points') }}</th>
|
|
<th>{{ _('Pretest?') }}</th>
|
|
{% if cases_formset.can_delete %}
|
|
<th>{{ _('Delete?') }}
|
|
<br>
|
|
<input type="checkbox" name="delete-all" id="delete-all">
|
|
</th>
|
|
{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for form in all_case_forms %}
|
|
{% if form.non_field_errors() %}
|
|
<tr>
|
|
<td colspan="{{ 9 + cases_formset.can_delete }}">{{ form.non_field_errors() }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% if form.prefix and '__prefix__' in form.prefix %}
|
|
</tbody>
|
|
<tbody class="extra-row-body" style="display: none">
|
|
{% endif %}
|
|
<tr data-type="{{ form['type'].value() }}">
|
|
<td>
|
|
{{ form.id }}{{ form.order.errors }}{{ form.order }}
|
|
</td>
|
|
<td class="type-column">{{ form.type.errors }}{{ form.type }}</td>
|
|
<td{% if not (form.empty_permitted or form['type'].value() != 'C' or
|
|
form['input_file'].value() in valid_files) %} class="bad-file"{% endif %}>
|
|
{{ form.input_file.errors }}{{ form.input_file }}
|
|
</td>
|
|
<td{% if not (form.empty_permitted or form['type'].value() != 'C' or
|
|
form['output_file'].value() in valid_files) %} class="bad-file"{% endif %}>
|
|
{{ form.output_file.errors }}{{ form.output_file }}
|
|
</td>
|
|
<td>{{ form.points.errors }}{{ form.points }}</td>
|
|
<td>{{ form.is_pretest.errors }}{{ form.is_pretest }}</td>
|
|
{% if cases_formset.can_delete %}
|
|
<td>{{ form.DELETE }}</td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
<input type="submit" value="{{ _('Apply!') }}" class="button" id="submit-button">
|
|
<a id="add-case-row" href="#"><i class="fa fa-plus"></i> {{ _('Add new case') }}</a>
|
|
</form>
|
|
{% endblock %}
|