diff --git a/.github/codecov.yml b/.github/codecov.yml index c636c12..b42482e 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,26 +1,29 @@ -codecov: - require_ci_to_pass: true - -coverage: - precision: 2 - round: down - range: "75...95" - status: - project: - default: - target: auto - threshold: 3% - base: auto - -parsers: - gcov: - branch_detection: - conditional: true - loop: true - method: false - macro: false - -comment: - layout: "reach,diff,flags,tree" - behavior: default - require_changes: false +codecov: + require_ci_to_pass: true + +coverage: + precision: 2 + round: down + range: "75...95" + status: + project: + default: + target: auto + threshold: 3% + base: auto + patch: + default: + target: 50% + +parsers: + gcov: + branch_detection: + conditional: true + loop: true + method: false + macro: false + +comment: + layout: "reach,diff,flags,tree" + behavior: default + require_changes: false diff --git a/Dockerfile b/Dockerfile index 01f3e8f..68600de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,8 @@ RUN pip3 install \ requests \ markdown \ python-markdown-math \ - flask-cors + flask-cors \ + pytz # Copy local code to the container image ENV APP_HOME /app diff --git a/TODOs.md b/TODOs.md index 7b236e4..6b73aec 100644 --- a/TODOs.md +++ b/TODOs.md @@ -1,17 +1,15 @@ -## TODOs - - Stop the submit form from giving weird errors when code file is changed - - Limit the frequency of submission by a single user (at least make it so they can't submit multiple at once) - - Make a front-end web interface to create and edit problems - - Make sure generated YAML file uses strings as subtask names - - Decorate the front-end interface a bit (look into Bootstrap?) - - Make a way to view all past submissions in a table +## TODOs (pre-V1) + - Make a logo for JudgeLite + - Update the wiki / add documentation + - Continue getting feedback from people + - Make some stronger tests -### Less important TODOs +## TODOs (post-V1) + - Limit the frequency of submission by a single user (at least make it so they can't submit multiple at once) - Allow custom checkers (other than diff, as a standalone binary) - Allow custom graders for interactive problems! - Maybe make two modes: One where your program is directly linked to the submitted code via stdin and stdout, and one where your program's stdout is used (to allow for completely custom problem setup) - Add support for partial scoring (fractional scores for each test case allowed, instead of just right or wrong) #### Other stuff - - See if there's some way to automate the 'swapoff -a' command (maybe replace with swap accounting?) - Disk quota is not enabled, only fsize is. That means that you could bypass the max file size limit by creating tons of small files, overloading the system. No one would do this by accident though, so it's not something that is too important to fix (plus, who would even think to try this?). diff --git a/app.py b/app.py index 2bee653..3e26457 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,6 @@ """ This python script contains the main Flask app. """ -import logging as logg import tempfile import yaml import markdown @@ -23,7 +22,6 @@ app.config['MAX_CONTENT_LENGTH'] = (MAX_CODE_SIZE + 1) * 1024 q = Queue(connection=REDIS_CONN) -logg.getLogger('flask_cors').level = logg.DEBUG """ @@ -33,13 +31,15 @@ @app.route('/favicon.ico', methods=['GET']) def favicon(): - return send_from_directory('images', 'favicon.ico', mimetype='image/vnd.microsoft.icon') + return send_from_directory('media', 'favicon.ico', mimetype='image/vnd.microsoft.icon') @app.route('/', methods=['GET']) def show_index(): - is_admin = 'admin' in session - return render_template('index.html', is_admin=is_admin) + timestamp = pytz.timezone('US/Pacific').localize(datetime.now()) + timestamp = timestamp.strftime("%m/%d/%Y %I:%M %p") + return render_template('index.html', num_problems=get_num_problems(), num_submissions=redis_get_num_submissions(), + curr_time=timestamp) @app.route('/problem_list', methods=['GET']) @@ -47,15 +47,37 @@ def show_problem_list(): return render_template('problem_list.html', problem_list=_get_problem_list()) +@app.route('/submission_list', methods=['GET']) +def show_submission_list(): + if 'admin' not in session: + return render_template('login.html', error='Please login to access this page.') + page = 1 + if 'page' in request.args: + page = int(request.args['page']) + return render_template('submission_list.html', submissions=_get_submissions(page), page=page, + num_pages=(redis_get_num_submissions() + PAGE_SIZE - 1) // PAGE_SIZE) + + +@app.route('/submission_details/', methods=['GET']) +def show_submission_details(i): + if 'job_id' not in request.args: + return json_error('No job id provided!') + try: + Job.fetch(request.args['job_id'], connection=REDIS_CONN) + except NoSuchJobError: + return json_error('Job not found!') + return render_template('submission_details.html', submission_source=_get_submission_source(i), + submission=redis_get_submission(i), job_id=request.args['job_id']) + + @app.route('/view_problem/', methods=['GET']) def view_problem(problem_id): return render_template('view_problem.html', problem=_get_problem_info(problem_id)) -@app.route('/test_form', methods=['GET']) -def show_form(): - # Return a simple testing form for the GET method - return render_template('form.html', filesize_limit=round(app.config['MAX_CONTENT_LENGTH'] / 1024 - 1)) +@app.route('/api', methods=['GET']) +def show_api_reference(): + return render_template('api_reference.html') @app.route('/status', methods=['GET']) @@ -82,7 +104,7 @@ def login_form(): return render_template('login.html', error='Incorrect secret key!') # Login successful session['admin'] = True - return redirect('/') + return redirect('/submission_list') @app.route('/logout', methods=['GET']) @@ -110,6 +132,15 @@ def is_valid_problem_id(problem_id): return valid_problem +def get_num_problems(): + problem_list = _get_problem_list() + num_problems = 0 + for group in problem_list['groups']: + for _ in group['problems']: + num_problems += 1 + return num_problems + + def change_md_to_html(md_file, default): raw: str if os.path.isfile(md_file): @@ -127,25 +158,37 @@ def change_md_to_html(md_file, default): return raw -@app.route('/api/get_submissions', methods=['GET']) +@app.route('/api/get_submissions/', methods=['GET']) @cross_origin() -def get_submissions(): - return str(redis_get_all_submissions()) +def get_submissions(page): + if 'secret_key' not in request.args: + return 'Missing secret key in GET parameters!' + elif request.args['secret_key'] != SECRET_KEY: + return 'Invalid secret key!' + return str(_get_submissions(int(page))) + + +def _get_submissions(page=1): + return redis_get_submissions(page) @app.route('/api/get_submission_source/', methods=['GET']) @cross_origin() def get_submission_source(i): + if 'secret_key' not in request.args: + return 'Missing secret key in GET parameters!' + elif request.args['secret_key'] != SECRET_KEY: + return 'Invalid secret key!' return _get_submission_source(i) def _get_submission_source(i): source_code = redis_get_submission_source(i) if source_code is None: - return 'Invalid submission index!' + return 'Invalid submission index!' else: - source_code = source_code.decode('utf-8').replace('<', '<').replace('>', '>') - return '
{}
'.format(source_code) + source_code = source_code.decode('utf-8') + return source_code @app.route('/api/get_problem_list', methods=['GET']) @@ -213,15 +256,19 @@ def _get_problem_info(problem_id): 'difficulty': pinfo['difficulty'] if 'difficulty' in pinfo else ''} -@app.route('/api/submit', methods=['POST']) +@app.route('/api/submit', methods=['GET', 'POST']) @cross_origin() def handle_submission(): + if request.method == 'GET': + # Return a simple testing form + return render_template('test_submit_form.html') + # Validate request if not request.form: return json_error('Empty request form (maybe invalid code file?)') # Secret key needed if not admin if 'admin' not in session and ('secret_key' not in request.form or request.form['secret_key'] != SECRET_KEY): - return json_error('You are not authorized to make submissions (must be admin or provide the right secret key)!') + return json_error('Invalid secret key!') if 'problem_id' not in request.form or not is_valid_problem_id(request.form['problem_id']): return json_error('Invalid problem ID!') if 'type' not in request.form: @@ -232,6 +279,9 @@ def handle_submission(): return json_error('No code file submitted!') if 'username' not in request.form or request.form['username'] == '': return json_error('No username!') + run_bonus = True + if 'run_bonus' in request.form and (request.form['run_bonus'] == 'off' or not request.form['run_bonus']): + run_bonus = False sec_filename = secure_filename(request.files['code'].filename) if sec_filename in ['', 'input.in.txt', 'output.out.txt', 'answer.ans.txt', 'code.new.py']: @@ -252,7 +302,7 @@ def handle_submission(): job = q.enqueue_call(func=judge_submission, timeout=60, ttl=RESULT_TTL, result_ttl=RESULT_TTL, failure_ttl=RESULT_TTL, args=(tempdir, request.form['problem_id'], sec_filename, - request.form['type'], request.form['username'])) + request.form['type'], request.form['username'], run_bonus)) job.meta['status'] = 'queued' job.save_meta() if DEBUG_LOWEST: diff --git a/env_vars.py b/env_vars.py index 7047038..4f27104 100644 --- a/env_vars.py +++ b/env_vars.py @@ -63,9 +63,9 @@ These settings deal with the web server (gunicorn). """ # Number of workers for gunicorn to use. -WORKER_COUNT = os.environ.get('WORKER_COUNT', 1) +WORKER_COUNT = int(os.environ.get('WORKER_COUNT', 1)) # Number of threads for gunicorn to use. -THREAD_COUNT = os.environ.get('THREAD_COUNT', 2) +THREAD_COUNT = int(os.environ.get('THREAD_COUNT', 2)) # Max code file size allowed, in KILOBYTES (KB)!!!!! Must be an integer. MAX_CODE_SIZE = int(os.environ.get('MAX_CODE_SIZE', 256)) @@ -81,6 +81,9 @@ PROBLEM_INFO_PATH = "/problem_info" PROBLEM_INFO_PATH = os.environ.get('PROBLEM_INFO_PATH', PROBLEM_INFO_PATH) -SECRET_KEY = os.environ.get('SECRET_KEY', ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(24))) +default_secret_key = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(24)) +SECRET_KEY = os.environ.get('SECRET_KEY', default_secret_key) + +PAGE_SIZE = int(os.environ.get('PAGE_SIZE', 50)) REDIS_CONN = redis.Redis() diff --git a/judge_submission.py b/judge_submission.py index 64129a6..2e60fa3 100644 --- a/judge_submission.py +++ b/judge_submission.py @@ -11,7 +11,6 @@ import glob import requests -from env_vars import * from logger import * from manage_redis import * @@ -361,7 +360,7 @@ def run_subtask(isolate_dir, problem_info, problem_folder, subtask_info, compile return verdict_subtask(final_verdict, final_score, max_time, max_memory, testcase) -def judge_submission(tempdir, problem_id, code_filename, code_type, username): +def judge_submission(tempdir, problem_id, code_filename, code_type, username, run_bonus=True): global job job = rq.get_current_job() if DEBUG_LOWEST: @@ -413,27 +412,30 @@ def judge_submission(tempdir, problem_id, code_filename, code_type, username): subtask = problem_info['subtasks'][i] # Should this subtask actually be run? + should_run = True if 'depends_on' in subtask and subtask['depends_on'] is not None: - should_run = True for required in subtask['depends_on']: if required not in subtask_results: log_error(subtask['name'] + ' has an invalid depends_on list! (' + required + ' not yet evaluated)') if subtask_results[required]['score'] == 0: should_run = False break - if not should_run: - subtask_results[subtask['name']] = verdict_subtask('SK', 0) - if DEBUG_LOW: - log('Skipping subtask ' + subtask['name']) - if DEBUG: - log('Subtask ' + subtask['name'] + ': ' + str(subtask_results[subtask['name']])) - num_tests = len(glob.glob1(problem_folder + '/subtasks/' + subtask['name'], '*.in')) - test_num += num_tests - # Update job meta - for j in range(num_tests): - job.meta['subtasks'][i][j][0] = 'SK' - job.save_meta() - continue + # Honor the "run_bonus" setting + if 'is_bonus' in subtask and subtask['is_bonus'] and not run_bonus: + should_run = False + if not should_run: + subtask_results[subtask['name']] = verdict_subtask('SK', 0) + if DEBUG_LOW: + log('Skipping subtask ' + subtask['name']) + if DEBUG: + log('Subtask ' + subtask['name'] + ': ' + str(subtask_results[subtask['name']])) + num_tests = len(glob.glob1(problem_folder + '/subtasks/' + subtask['name'], '*.in')) + test_num += num_tests + # Update job meta + for j in range(num_tests): + job.meta['subtasks'][i][j][0] = 'SK' + job.save_meta() + continue subtask_result = run_subtask(isolate_dir, problem_info, problem_folder, subtask, compiled_filename, code_type, i) @@ -482,7 +484,8 @@ def judge_submission(tempdir, problem_id, code_filename, code_type, username): # Add submission result to Redis database with open(isolate_dir + '/' + code_filename, 'r') as fcode: source_code = fcode.read() - redis_add_submission(problem_info['problem_id'], username, final_score, job.get_id(), source_code) + redis_add_submission(problem_info['problem_id'], username, final_score, job.get_id(), + source_code, final_verdict) # Send POST request to webhook URL if WEBHOOK_URL is not None: @@ -490,12 +493,12 @@ def judge_submission(tempdir, problem_id, code_filename, code_type, username): if DEBUG_LOW: log('Sending POST request to ' + WEBHOOK_URL) req = requests.post(WEBHOOK_URL, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246'}, - data={'problem_id': problem_info['problem_id'], 'username': username, - 'score': final_score, 'job_id': job.get_id(), - 'secret_key': SECRET_KEY}, timeout=10) + data={'problem_id': problem_info['problem_id'], 'username': username, + 'score': final_score, 'job_id': job.get_id(), + 'secret_key': SECRET_KEY}, timeout=10) if DEBUG_LOW: log('Response code: ' + str(req)) - except Exception as e: + except requests.exceptions: return verdict_error('WEBHOOK_FAIL') # Finally, return the result. :) diff --git a/manage_redis.py b/manage_redis.py index 57904cc..88c9f40 100644 --- a/manage_redis.py +++ b/manage_redis.py @@ -1,20 +1,36 @@ import json +import pytz from datetime import datetime from env_vars import * -def redis_add_submission(problem_id: str, username: str, score: float, job_id: str, source_code: str): +def redis_add_submission(problem_id: str, username: str, score: float, job_id: str, source_code: str, verdict: str): + timestamp = pytz.timezone('US/Pacific').localize(datetime.now()) + timestamp = timestamp.strftime("%m/%d/%Y %I:%M:%S %p") submission_json = json.dumps({'problem_id': problem_id, 'username': username, 'score': score, 'job_id': job_id, - 'timestamp': datetime.now().strftime("%m/%d/%Y %H:%M:%S")}) + 'verdict': verdict, 'timestamp': timestamp}) REDIS_CONN.lpush('submissions', submission_json) REDIS_CONN.lpush('submission_source', source_code) -def redis_get_all_submissions(): - return REDIS_CONN.lrange('submissions', 0, -1) +def redis_get_submissions(page=1): + submissions = REDIS_CONN.lrange('submissions', (page-1) * PAGE_SIZE, page * PAGE_SIZE) + for i in range(len(submissions)): + submissions[i] = json.loads(submissions[i]) + return submissions + + +def redis_get_submission(i): + submission = REDIS_CONN.lindex('submissions', i) + submission = json.loads(submission) + return submission def redis_get_submission_source(i): return REDIS_CONN.lindex('submission_source', i) + + +def redis_get_num_submissions(): + return REDIS_CONN.llen('submissions') diff --git a/images/favicon.ico b/media/favicon.ico similarity index 100% rename from images/favicon.ico rename to media/favicon.ico diff --git a/media/particles.mp4 b/media/particles.mp4 new file mode 100644 index 0000000..8784413 Binary files /dev/null and b/media/particles.mp4 differ diff --git a/run_judgelite.sh b/run_judgelite.sh index 8739b53..3b666dc 100644 --- a/run_judgelite.sh +++ b/run_judgelite.sh @@ -6,7 +6,7 @@ if [ "$(docker ps -q -a -f name=judgelite)" ]; then fi echo "Pulling latest JudgeLite docker image from Docker Hub..." -sudo docker pull giantpizzahead/judgelite:version-0.4.1 +sudo docker pull giantpizzahead/judgelite:version-0.5 echo "Turning swap off..." sudo swapoff -a @@ -17,7 +17,7 @@ sudo docker run --name judgelite --privileged -dit \ -e DEBUG=0 -e DEBUG_LOW=0 -e DEBUG_LOWEST=0 -e PROGRAM_OUTPUT=0 \ -v $PWD/problem_info:/problem_info \ -v $PWD/redis_db:/redis_db \ - giantpizzahead/judgelite:version-0.4.1 + giantpizzahead/judgelite:version-0.5 echo "Sending logs to judgelite.log..." echo "------------------JUDGELITE STARTED------------------" >> judgelite.log diff --git a/static/status.js b/static/status.js index 04b6d2a..ab3bc8e 100644 --- a/static/status.js +++ b/static/status.js @@ -138,7 +138,7 @@ function updateResults(resp) { let subtaskIsBonus = resp["is_bonus"][i] == 1; let atLeast1AC = false; for (let j = 0; j < subtask.length; j++) { - let test = subtask[j] + let test = subtask[j]; if (test[0] == "AC") { atLeast1AC = true; break; diff --git a/static/style.css b/static/style.css index fe65ee0..9e324ad 100644 --- a/static/style.css +++ b/static/style.css @@ -8,13 +8,152 @@ Check it out at http://usaco.org/ * { box-sizing: border-box; + word-wrap: break-word; +} + +header * { + margin: 0; + padding: 0; } body { - /*background-color: #D6FFD4;*/ + margin: 0; + background-color: #d8f5fd; + font-family: 'Open Sans', sans-serif; +} + +#content { margin: auto; max-width: min(1000px, 90%); - font-family: 'Open Sans', sans-serif; +} + +header { + padding: 1em 0; + margin-bottom: 50px; + background-color: #111d4a; +} + +header h1 { + color: white; + font-size: 2.5em; + text-align: center; +} + +#top-navbar ul { + display: flex; + justify-content: center; + margin-top: 1em; + list-style-type: none; +} + +.top-navbar-link { + padding: 1em; + color: #fff; + text-decoration: none; +} + +.top-navbar-link:hover { + color: #a3e7fc; +} + +#background-video { + position: fixed; + top: 0; + left: 0; + min-width: 100%; + min-height: 100%; + z-index: -6; +} + +#center-jumbotron { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: -5; + color: white; + opacity: 0; + transition: opacity 3s ease; +} + +#center-jumbotron h1 { + font-size: 3em; +} + +#center-jumbotron p { + margin: 1em; + font-size: 1.7em; + font-weight: bold; +} + +#center-jumbotron a { + color: #a3e7fc; +} + +#submission-table { + width: 100%; + border: 1px solid black; + border-collapse: collapse; +} + +#submission-table td, #submission-table th { + padding: 8px 0; + text-align: center; +} + +#submission-table th { + background-color: #111d4a; + color: white; +} + +#submission-table tbody tr { + background-color: #c5f0fc; + cursor: pointer; +} + +#submission-table tbody tr:nth-child(odd) { + filter: contrast(107%); +} + +#submission-table tbody tr:hover { + background-color: #9ee6fa; +} + +#submission-table .submission-pass { + color: #009900; + font-weight: bold; +} + +#submission-table .submission-bonus { + color: #000099; + font-weight: bold; +} + +pre { + border: 1px solid black; +} + +code { + padding: 1em !important; +} + +#page-number-controller { + display: flex; + align-items: center; + justify-content: center; + margin: 20px -5px -5px -5px; +} + +#page-number-controller * { + margin: 5px 5px; + text-decoration: none; + font-weight: bold; + font-size: 1.2em; } h1, h2, h3 { @@ -54,6 +193,8 @@ h2 { margin: 20px; font-weight: bold; overflow-wrap: anywhere; + background-color: white; + z-index: -2; } .submission-result-compile-error { @@ -178,3 +319,61 @@ h2 { font-size: 0.675em; text-align: right; } + +@media only screen and (max-width: 570px), screen and (max-height: 730px) { + header h1 { + color: white; + font-size: 1.5em; + text-align: center; + } + + #top-navbar ul { + margin-top: 0.7em; + } + + .top-navbar-link { + padding: 0.7em; + font-size: 0.8em; + } + + #center-jumbotron h1 { + font-size: 2.3em; + } + + #center-jumbotron p { + margin: 0.7em; + font-size: 1.4em; + font-weight: bold; + } +} + +@media only screen and (max-width: 420px), screen and (max-height: 500px) { + #content { + font-size: 0.8em; + } + + header h1 { + color: white; + font-size: 1.2em; + text-align: center; + } + + #top-navbar ul { + margin-top: 0.5em; + } + + .top-navbar-link { + padding: 0.2em 0.35em; + font-size: 0.7em; + } + + #center-jumbotron h1 { + font-size: 1.9em; + } + + #center-jumbotron p { + margin: 0.5em; + font-size: 1.25em; + font-weight: bold; + } +} \ No newline at end of file diff --git a/static/submit.js b/static/submit.js index 6405d86..e283d83 100644 --- a/static/submit.js +++ b/static/submit.js @@ -36,8 +36,10 @@ function sendData() { // Get form data / use an AJAX request to call the submission API let formData = new FormData(submitForm); + if (!formData.has("run_bonus")) formData.append("run_bonus", "off"); let xhr = new XMLHttpRequest(); xhr.onload = function() { + document.getElementById("code").value = ""; let resp = JSON.parse(xhr.response); if (xhr.status == 200) { // Add job_id to URL parameters @@ -55,8 +57,9 @@ function sendData() { document.getElementById("submit-button").innerText = "Submit!"; }; xhr.onerror = function() { + document.getElementById("code").value = ""; // Display error in a very unfriendly way - document.getElementById("response-text").innerHTML = "Submission failed: Unknown error."; + document.getElementById("response-text").innerHTML = "Error occurred while submitting. Maybe try submitting again?"; // Reenable submit button document.getElementById("submit-button").disabled = false; document.getElementById("submit-button").innerText = "Submit!"; diff --git a/templates/api_reference.html b/templates/api_reference.html new file mode 100644 index 0000000..6dee0b6 --- /dev/null +++ b/templates/api_reference.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block content %} +

API Reference

+ +

No Key Required

+

Get Problem List

+

Get Problem Info (change PRB1 to desired problem ID)

+

Get Submission Status (change job_id to desired job ID)

+ +

Secret Key Required

+

Submit Code to JudgeLite

+

Get Evaluated Submissions (change 1 to desired page #)

+

Get Evaluated Submission Source (change 0 to desired index)

+{% endblock %} diff --git a/templates/base.html b/templates/base.html index 7751f36..4ac052d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -10,8 +10,25 @@ -

JudgeLite

- {% block content %}{% endblock %} +
+

JudgeLite

+ +
+
+ {% block content %}{% endblock %} +
diff --git a/templates/index.html b/templates/index.html index 5d1ac22..938be69 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,19 +1,29 @@ {% extends "base.html" %} {% block content %} -

Welcome to JudgeLite

-

Click this link to view the list of problems.

- {% if is_admin %} -

You are logged in as an administrator. Click here to log out.

- {% else %} -

Administrators, click here to log in.

- {% endif %} -

API Stuff

-

Get Evaluated Submissions

-

Get Evaluated Submission Source (change 0 to desired index)

-

Get Submission Status (change job_id to desired job ID)

-

Get Problem List

-

Get Problem Info (change PRB1 to desired problem ID)

-
-

(This front end interface is still a huge WIP)

+ + +
+

Welcome to JudgeLite.

+

Server time: {{ curr_time }}

+ {% if not session['admin'] %} +

Click here to login.

+ {% else %} +

Click here to view recent submissions.

+ {% endif %} +

{{ num_problems }} problems.

+

{{ num_submissions }} submissions.

+
+ {% endblock %} diff --git a/templates/login.html b/templates/login.html index b08275c..e7c2d9a 100644 --- a/templates/login.html +++ b/templates/login.html @@ -4,8 +4,8 @@ {% if error is defined %}

{{ error }}

{% endif %} -

Login Form

-
+

Login Form


+

diff --git a/templates/problem_list.html b/templates/problem_list.html index 0f26b70..7965cfd 100644 --- a/templates/problem_list.html +++ b/templates/problem_list.html @@ -1,7 +1,6 @@ {% extends "base.html" %} {% block content %} -

Back to home page

Problem List

{% for group in problem_list.groups %}

{{ group.name }}

diff --git a/templates/submission_details.html b/templates/submission_details.html new file mode 100644 index 0000000..c0730f7 --- /dev/null +++ b/templates/submission_details.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block content %} +

Back to submission list

+

Submission Details

+

Username: {{ submission['username'] }}

+

Problem: {{ submission['problem_id'] }}

+

Score: {{ submission['score'] }}

+

Time: {{ submission['timestamp'] }}

+ +
+
+
+

Waiting...

+
+
+
+
+ +

+ + + + +

Submission Source:

+
{{ submission_source }}
+{% endblock %} \ No newline at end of file diff --git a/templates/submission_list.html b/templates/submission_list.html new file mode 100644 index 0000000..c71832a --- /dev/null +++ b/templates/submission_list.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +

Submission List

+ + + + + + + + + + + {% for submission in submissions %} + + + + + + + {% endfor %} + +
UsernameProblemScoreTimestamp
{{ submission["username"] }}{{ submission["problem_id"] }}{{ submission["score"] }}{{ submission["timestamp"] }}
+ +
+ << + < +

Page {{ page }} of {{ num_pages }}

+ > + >> +
+{% endblock %} \ No newline at end of file diff --git a/templates/form.html b/templates/test_submit_form.html similarity index 74% rename from templates/form.html rename to templates/test_submit_form.html index cecb1d4..0974d5c 100644 --- a/templates/form.html +++ b/templates/test_submit_form.html @@ -1,17 +1,10 @@ - - - - - - - Submission Form - - - +{% extends "base.html" %} + +{% block content %}

Test Submission Form

+

Note: This form contains all the fields that are needed to send a POST request to /api/submit, along with their correct names and value types. So, to automate the submission of code, all you have to do is copy this form!

@@ -29,12 +22,19 @@

Test Submission Form



+ + +

+ +

+
+
- + - - \ No newline at end of file +{% endblock %} \ No newline at end of file diff --git a/templates/view_problem.html b/templates/view_problem.html index 3937ef9..cfa68f1 100644 --- a/templates/view_problem.html +++ b/templates/view_problem.html @@ -55,11 +55,14 @@

Submission Form



+ +


+
diff --git a/tests/test_flask.py b/tests/test_flask.py index 6468855..55ea5fe 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -59,7 +59,7 @@ def test_submit_no_key(client): code=(io.BytesIO(b'N=int(input())\nprint(N)\n'), 'code.py'), username='test_user' ), follow_redirects=True, content_type='multipart/form-data') - assert b'not authorized' in rv.data + assert b'Invalid secret key' in rv.data def test_submit_no_id(client): diff --git a/tests/test_judge_logic.py b/tests/test_judge_logic.py index 5c00337..f5963f9 100644 --- a/tests/test_judge_logic.py +++ b/tests/test_judge_logic.py @@ -124,3 +124,19 @@ def test_fill_missing_output(tempdir): job = q.enqueue_call(func=judge_submission, args=(tempdir, 'test5', 'sol.py', 'python', 'username')) assert isfile('./sample_problem_info/test5/subtasks/main/01.out') and \ not isfile('./sample_problem_info/test5/subtasks/main/03.out') and job.result['final_score'] == 0 + + +def test_no_run_bonus(tempdir): + """Test that the judge honors 'do not run bonus' requests.""" + copyfile('./sample_problem_info/test2/solutions/wrong.py', tempdir + '/wrong.py') + job = q.enqueue_call(func=judge_submission, args=(tempdir, 'test2', 'wrong.py', 'python', 'username', False)) + + correct_verdicts = ['AC', 'WA', 'SK', 'AC', 'WA', 'SK', 'SK', 'SK', 'SK'] + judge_verdicts = [] + for i in range(3): + judge_verdicts.append(job.result['subtasks'][0][i][0]) + for i in range(3): + judge_verdicts.append(job.result['subtasks'][1][i][0]) + for i in range(3): + judge_verdicts.append(job.result['subtasks'][2][i][0]) + assert correct_verdicts == judge_verdicts