Skip to content

Commit

Permalink
pretty front end + admin panel + made bonus tests optional to not con…
Browse files Browse the repository at this point in the history
…fuse people (credit to @frodakcin)
  • Loading branch information
Giantpizzahead committed Aug 8, 2020
1 parent d0487d8 commit 16b5d85
Show file tree
Hide file tree
Showing 24 changed files with 532 additions and 124 deletions.
55 changes: 29 additions & 26 deletions .github/codecov.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 7 additions & 9 deletions TODOs.md
Original file line number Diff line number Diff line change
@@ -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?).
88 changes: 69 additions & 19 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
This python script contains the main Flask app.
"""
import logging as logg
import tempfile
import yaml
import markdown
Expand All @@ -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


"""
Expand All @@ -33,29 +31,53 @@

@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'])
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/<i>', 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/<problem_id>', 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'])
Expand All @@ -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'])
Expand Down Expand Up @@ -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):
Expand All @@ -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/<page>', 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/<i>', 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 '<code>Invalid submission index!</code>'
return 'Invalid submission index!'
else:
source_code = source_code.decode('utf-8').replace('<', '&lt;').replace('>', '&gt;')
return '<code><pre>{}</pre></code>'.format(source_code)
source_code = source_code.decode('utf-8')
return source_code


@app.route('/api/get_problem_list', methods=['GET'])
Expand Down Expand Up @@ -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:
Expand All @@ -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']:
Expand All @@ -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:
Expand Down
9 changes: 6 additions & 3 deletions env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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()
45 changes: 24 additions & 21 deletions judge_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import glob
import requests

from env_vars import *
from logger import *
from manage_redis import *

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -482,20 +484,21 @@ 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:
try:
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. :)
Expand Down
Loading

0 comments on commit 16b5d85

Please sign in to comment.