Coverage for model/submission.py: 72%
290 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-11-05 04:22 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2024-11-05 04:22 +0000
1import io
2from typing import Optional
3import requests as rq
4import random
5import secrets
6import json
7from flask import (
8 Blueprint,
9 send_file,
10 request,
11 current_app,
12)
13from datetime import datetime, timedelta
14from mongo import *
15from mongo import engine
16from mongo.utils import (
17 RedisCache,
18 drop_none,
19)
20from .utils import *
21from .auth import *
23__all__ = ['submission_api']
24submission_api = Blueprint('submission_api', __name__)
27@submission_api.route('/', methods=['POST'])
28@login_required
29@Request.json('language_type: int', 'problem_id: int')
30def create_submission(user, language_type, problem_id):
31 # the user reach the rate limit for submitting
32 now = datetime.now()
33 delta = timedelta.total_seconds(now - user.last_submit)
34 if delta <= Submission.config().rate_limit:
35 wait_for = Submission.config().rate_limit - delta
36 return HTTPError(
37 'Submit too fast!\n'
38 f'Please wait for {wait_for:.2f} seconds to submit.',
39 429,
40 data={
41 'waitFor': wait_for,
42 },
43 ) # Too many request
44 # check for fields
45 if problem_id is None:
46 return HTTPError(
47 'problemId is required!',
48 400,
49 )
50 # search for problem
51 problem = Problem(problem_id)
52 if not problem:
53 return HTTPError('Unexisted problem id.', 404)
54 # problem permissoion
55 if not problem.permission(user, Problem.Permission.VIEW):
56 return HTTPError('problem permission denied!', 403)
57 # check deadline
58 for homework in problem.obj.homeworks:
59 if now < homework.duration.start:
60 return HTTPError('this homework hasn\'t start.', 403)
61 # ip validation
62 if not problem.is_valid_ip(get_ip()):
63 return HTTPError('Invalid IP address.', 403)
64 # handwritten problem doesn't need language type
65 if language_type is None:
66 if problem.problem_type != 2:
67 return HTTPError(
68 'post data missing!',
69 400,
70 data={
71 'languageType': language_type,
72 'problemId': problem_id
73 },
74 )
75 language_type = 3
76 # not allowed language
77 if not problem.allowed(language_type):
78 return HTTPError(
79 'not allowed language',
80 403,
81 data={
82 'allowed': problem.obj.allowed_language,
83 'got': language_type
84 },
85 )
86 # check if the user has used all his quota
87 if problem.obj.quota != -1:
88 no_grade_permission = not any(
89 c.permission(user=user, req=Course.Permission.GRADE)
90 for c in map(Course, problem.courses))
92 run_out_of_quota = problem.submit_count(user) >= problem.quota
93 if no_grade_permission and run_out_of_quota:
94 return HTTPError('you have used all your quotas', 403)
95 user.problem_submission[str(problem_id)] = problem.submit_count(user) + 1
96 user.save()
97 # insert submission to DB
98 try:
99 submission = Submission.add(problem_id=problem_id,
100 username=user.username,
101 lang=language_type,
102 timestamp=now,
103 ip_addr=request.remote_addr)
104 except ValidationError:
105 return HTTPError('invalid data!', 400)
106 except engine.DoesNotExist as e:
107 return HTTPError(str(e), 404)
108 except TestCaseNotFound as e:
109 return HTTPError(str(e), 403)
110 # update user
111 user.update(
112 last_submit=now,
113 push__submissions=submission.obj,
114 )
115 # update problem
116 submission.problem.update(inc__submitter=1)
117 return HTTPResponse(
118 'submission recieved.\n'
119 'please send source code with given submission id later.',
120 data={
121 'submissionId': submission.id,
122 },
123 )
126@submission_api.route('/', methods=['GET'])
127@login_required
128@Request.args('offset', 'count', 'problem_id', 'username', 'status',
129 'language_type', 'course', 'before', 'after', 'ip_addr')
130def get_submission_list(
131 user,
132 offset,
133 count,
134 problem_id,
135 username,
136 status,
137 course,
138 before,
139 after,
140 language_type,
141 ip_addr,
142):
143 '''
144 get the list of submission data
145 '''
147 def parse_int(val: Optional[int], name: str):
148 if val is None:
149 return None
150 try:
151 return int(val)
152 except ValueError:
153 raise ValueError(f'can not convert {name} to integer')
155 def parse_str(val: Optional[str], name: str):
156 if val is None:
157 return None
158 try:
159 return str(val)
160 except ValueError:
161 raise ValueError(f'can not convert {name} to string')
163 def parse_timestamp(val: Optional[int], name: str):
164 if val is None:
165 return None
166 try:
167 return datetime.fromtimestamp(val)
168 except ValueError:
169 raise ValueError(f'can not convert {name} to timestamp')
171 cache_key = (
172 'SUBMISSION_LIST_API',
173 user,
174 problem_id,
175 username,
176 status,
177 language_type,
178 course,
179 offset,
180 count,
181 before,
182 after,
183 )
185 cache_key = '_'.join(map(str, cache_key))
186 cache = RedisCache()
187 # check cache
188 if cache.exists(cache_key):
189 submissions = json.loads(cache.get(cache_key))
190 submission_count = submissions['submission_count']
191 submissions = submissions['submissions']
192 else:
193 # convert args
194 offset = parse_int(offset, 'offset')
195 count = parse_int(count, 'count')
196 problem_id = parse_int(problem_id, 'problemId')
197 status = parse_int(status, 'status')
198 before = parse_timestamp(before, 'before')
199 after = parse_timestamp(after, 'after')
200 ip_addr = parse_str(ip_addr, 'ip_addr')
202 if language_type is not None:
203 try:
204 language_type = list(map(int, language_type.split(',')))
205 except ValueError as e:
206 return HTTPError(
207 'cannot parse integers from languageType',
208 400,
209 )
210 # students can only get their own submissions
211 if user.role == User.engine.Role.STUDENT:
212 username = user.username
213 try:
214 params = drop_none({
215 'user': user,
216 'offset': offset,
217 'count': count,
218 'problem': problem_id,
219 'q_user': username,
220 'status': status,
221 'language_type': language_type,
222 'course': course,
223 'before': before,
224 'after': after,
225 'ip_addr': ip_addr
226 })
227 submissions, submission_count = Submission.filter(
228 **params,
229 with_count=True,
230 )
231 submissions = [s.to_dict() for s in submissions]
232 cache.set(
233 cache_key,
234 json.dumps({
235 'submissions': submissions,
236 'submission_count': submission_count,
237 }), 15)
238 except ValueError as e:
239 return HTTPError(str(e), 400)
240 # unicorn gifs
241 unicorns = [
242 'https://media.giphy.com/media/xTiTnLmaxrlBHxsMMg/giphy.gif',
243 'https://media.giphy.com/media/26AHG5KGFxSkUWw1i/giphy.gif',
244 'https://media.giphy.com/media/g6i1lEax9Pa24/giphy.gif',
245 'https://media.giphy.com/media/tTyTbFF9uEbPW/giphy.gif',
246 ]
247 ret = {
248 'unicorn': random.choice(unicorns),
249 'submissions': submissions,
250 'submissionCount': submission_count,
251 }
252 return HTTPResponse(
253 'here you are, bro',
254 data=ret,
255 )
258@submission_api.route('/<submission>', methods=['GET'])
259@login_required
260@Request.doc('submission', Submission)
261def get_submission(user, submission: Submission):
262 user_feedback_perm = submission.permission(user,
263 Submission.Permission.FEEDBACK)
264 # check permission
265 if submission.handwritten and not user_feedback_perm:
266 return HTTPError('forbidden.', 403)
267 # ip validation
268 problem = Problem(submission.problem_id)
269 if not problem.is_valid_ip(get_ip()):
270 return HTTPError('Invalid IP address.', 403)
271 if not all(submission.timestamp in hw.duration
272 for hw in problem.running_homeworks() if hw.ip_filters):
273 return HTTPError('You cannot view this submission during quiz.', 403)
274 # serialize submission
275 has_code = not submission.handwritten and user_feedback_perm
276 has_output = submission.problem.can_view_stdout
277 ret = submission.to_dict()
278 if has_code:
279 try:
280 ret['code'] = submission.get_main_code()
281 except UnicodeDecodeError:
282 ret['code'] = False
283 if has_output:
284 ret['tasks'] = submission.get_detailed_result()
285 else:
286 ret['tasks'] = submission.get_result()
287 return HTTPResponse(data=ret)
290@submission_api.get('/<submission>/output/<int:task_no>/<int:case_no>')
291@login_required
292@Request.doc('submission', Submission)
293def get_submission_output(
294 user,
295 submission: Submission,
296 task_no: int,
297 case_no: int,
298):
299 if not submission.permission(user, Submission.Permission.VIEW_OUTPUT):
300 return HTTPError('permission denied', 403)
301 try:
302 output = submission.get_single_output(task_no, case_no)
303 except FileNotFoundError as e:
304 return HTTPError(str(e), 400)
305 except AttributeError as e:
306 return HTTPError(str(e), 102)
307 return HTTPResponse('ok', data=output)
310@submission_api.route('/<submission>/pdf/<item>', methods=['GET'])
311@login_required
312@Request.doc('submission', Submission)
313def get_submission_pdf(user, submission: Submission, item):
314 # check the permission
315 if not submission.permission(user, Submission.Permission.FEEDBACK):
316 return HTTPError('forbidden.', 403)
317 # non-handwritten submissions have no pdf file
318 if not submission.handwritten:
319 return HTTPError('it is not a handwritten submission.', 400)
320 if item not in ['comment', 'upload']:
321 return HTTPError('/<submission_id>/pdf/<"upload" or "comment">', 400)
322 try:
323 if item == 'comment':
324 data = submission.get_comment()
325 else:
326 data = submission.get_code('main.pdf', binary=True)
327 except FileNotFoundError as e:
328 return HTTPError('File not found.', 404)
329 return send_file(
330 io.BytesIO(data),
331 mimetype='application/pdf',
332 as_attachment=True,
333 max_age=0,
334 download_name=f'{item}-{submission.id[-6:] or "missing-id"}.pdf',
335 )
338@submission_api.route('/<submission>/complete', methods=['PUT'])
339@Request.json('tasks: list', 'token: str')
340@Request.doc('submission', Submission)
341def on_submission_complete(submission: Submission, tasks, token):
342 if not Submission.verify_token(submission.id, token):
343 return HTTPError('i don\'t know you', 403)
344 try:
345 submission.process_result(tasks)
346 except (ValidationError, KeyError) as e:
347 return HTTPError(
348 'invalid data!\n'
349 f'{type(e).__name__}: {e}',
350 400,
351 )
352 return HTTPResponse(f'{submission} result recieved.')
355@submission_api.route('/<submission>', methods=['PUT'])
356@login_required
357@Request.doc('submission', Submission)
358@Request.files('code')
359def update_submission(user, submission: Submission, code):
360 # validate this reques
361 if submission.status >= 0:
362 return HTTPError(
363 f'{submission} has finished judgement.',
364 403,
365 )
366 # if user not equal, reject
367 if not secrets.compare_digest(submission.user.username, user.username):
368 return HTTPError('user not equal!', 403)
369 # if source code not found
370 if code is None:
371 return HTTPError(
372 f'can not find the source file',
373 400,
374 )
375 # or empty file
376 if len(code.read()) == 0:
377 return HTTPError('empty file', 400)
378 code.seek(0)
379 # has been uploaded
380 if submission.code:
381 return HTTPError(
382 f'{submission} has been uploaded source file!',
383 403,
384 )
385 try:
386 success = submission.submit(code)
387 except FileExistsError:
388 exit(10086)
389 except ValueError as e:
390 return HTTPError(str(e), 400)
391 except JudgeQueueFullError as e:
392 return HTTPResponse(str(e), 202)
393 except ValidationError as e:
394 return HTTPError(str(e), 400, data=e.to_dict())
395 except TestCaseNotFound as e:
396 return HTTPError(str(e), 403)
397 if success:
398 return HTTPResponse(
399 f'{submission} {"is finished." if submission.handwritten else "send to judgement."}'
400 )
401 else:
402 return HTTPError('Some error occurred, please contact the admin', 500)
405@submission_api.route('/<submission>/grade', methods=['PUT'])
406@login_required
407@Request.json('score: int')
408@Request.doc('submission', Submission)
409def grade_submission(user: User, submission: Submission, score: int):
410 if not submission.permission(user, Submission.Permission.GRADE):
411 return HTTPError('forbidden.', 403)
413 if score < 0 or score > 100:
414 return HTTPError('score must be between 0 to 100.', 400)
416 # AC if the score is 100, WA otherwise
417 submission.update(score=score, status=(0 if score == 100 else 1))
418 submission.finish_judging()
419 return HTTPResponse(f'{submission} score recieved.')
422@submission_api.route('/<submission>/comment', methods=['PUT'])
423@login_required
424@Request.files('comment')
425@Request.doc('submission', Submission)
426def comment_submission(user, submission: Submission, comment):
427 if not submission.permission(user, Submission.Permission.COMMENT):
428 return HTTPError('forbidden.', 403)
430 if comment is None:
431 return HTTPError(
432 f'can not find the comment',
433 400,
434 )
435 try:
436 submission.add_comment(comment)
437 except ValueError as e:
438 return HTTPError(str(e), 400)
439 return HTTPResponse(f'{submission} comment recieved.')
442@submission_api.route('/<submission>/rejudge', methods=['GET'])
443@login_required
444@Request.doc('submission', Submission)
445def rejudge(user, submission: Submission):
446 if submission.status == -2 or (
447 submission.status == -1 and
448 (datetime.now() - submission.last_send).seconds < 300):
449 return HTTPError(f'{submission} haven\'t be judged', 403)
450 if not submission.permission(user, Submission.Permission.REJUDGE):
451 return HTTPError('forbidden.', 403)
452 try:
453 success = submission.rejudge()
454 except FileExistsError:
455 exit(10086)
456 except ValueError as e:
457 return HTTPError(str(e), 400)
458 except JudgeQueueFullError as e:
459 return HTTPResponse(str(e), 202)
460 except ValidationError as e:
461 return HTTPError(str(e), data=e.to_dict())
462 if success:
463 return HTTPResponse(f'{submission} is sent to judgement.')
464 else:
465 return HTTPError('Some error occurred, please contact the admin', 500)
468@submission_api.route('/config', methods=['GET', 'PUT'])
469@login_required
470@identity_verify(0)
471def config(user):
472 config = Submission.config()
474 def get_config():
475 ret = config.to_mongo()
476 del ret['_cls']
477 del ret['_id']
478 return HTTPResponse('success.', data=ret)
480 @Request.json('rate_limit: int', 'sandbox_instances: list')
481 def modify_config(rate_limit, sandbox_instances):
482 # try to convert json object to Sandbox instance
483 try:
484 sandbox_instances = [
485 *map(
486 lambda s: engine.Sandbox(**s),
487 sandbox_instances,
488 )
489 ]
490 except engine.ValidationError as e:
491 return HTTPError(
492 'wrong Sandbox schema',
493 400,
494 data=e.to_dict(),
495 )
496 # skip if during testing
497 if not current_app.config['TESTING']:
498 resps = []
499 # check sandbox status
500 for sb in sandbox_instances:
501 resp = rq.get(f'{sb.url}/status')
502 if not resp.ok:
503 resps.append((sb.name, resp))
504 # some exception occurred
505 if len(resps) != 0:
506 return HTTPError(
507 'some error occurred when check sandbox status',
508 400,
509 data=[{
510 'name': name,
511 'statusCode': resp.status_code,
512 'response': resp.text,
513 } for name, resp in resps],
514 )
515 try:
516 config.update(
517 rate_limit=rate_limit,
518 sandbox_instances=sandbox_instances,
519 )
520 except ValidationError as e:
521 return HTTPError(str(e), 400)
523 return HTTPResponse('success.')
525 methods = {'GET': get_config, 'PUT': modify_config}
526 return methods[request.method]()