Coverage for model/submission.py: 65%

298 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-11 18:37 +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 * 

22 

23__all__ = ['submission_api'] 

24submission_api = Blueprint('submission_api', __name__) 

25 

26 

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)) 

91 

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 ip_addr = request.headers.get('cf-connecting-ip', request.remote_addr) 

99 try: 

100 submission = Submission.add(problem_id=problem_id, 

101 username=user.username, 

102 lang=language_type, 

103 timestamp=now, 

104 ip_addr=ip_addr) 

105 except ValidationError: 

106 return HTTPError('invalid data!', 400) 

107 except engine.DoesNotExist as e: 

108 return HTTPError(str(e), 404) 

109 except TestCaseNotFound as e: 

110 return HTTPError(str(e), 403) 

111 # update user 

112 user.update( 

113 last_submit=now, 

114 push__submissions=submission.obj, 

115 ) 

116 # update problem 

117 submission.problem.update(inc__submitter=1) 

118 return HTTPResponse( 

119 'submission recieved.\n' 

120 'please send source code with given submission id later.', 

121 data={ 

122 'submissionId': submission.id, 

123 }, 

124 ) 

125 

126 

127@submission_api.route('/', methods=['GET']) 

128@login_required 

129@Request.args('offset', 'count', 'problem_id', 'username', 'status', 

130 'language_type', 'course', 'before', 'after', 'ip_addr') 

131def get_submission_list( 

132 user, 

133 offset, 

134 count, 

135 problem_id, 

136 username, 

137 status, 

138 course, 

139 before, 

140 after, 

141 language_type, 

142 ip_addr, 

143): 

144 ''' 

145 get the list of submission data 

146 ''' 

147 

148 def parse_int(val: Optional[int], name: str): 

149 if val is None: 

150 return None 

151 try: 

152 return int(val) 

153 except ValueError: 

154 raise ValueError(f'can not convert {name} to integer') 

155 

156 def parse_str(val: Optional[str], name: str): 

157 if val is None: 

158 return None 

159 try: 

160 return str(val) 

161 except ValueError: 

162 raise ValueError(f'can not convert {name} to string') 

163 

164 def parse_timestamp(val: Optional[int], name: str): 

165 if val is None: 

166 return None 

167 try: 

168 return datetime.fromtimestamp(val) 

169 except ValueError: 

170 raise ValueError(f'can not convert {name} to timestamp') 

171 

172 cache_key = ( 

173 'SUBMISSION_LIST_API', 

174 user, 

175 problem_id, 

176 username, 

177 status, 

178 language_type, 

179 course, 

180 offset, 

181 count, 

182 before, 

183 after, 

184 ) 

185 

186 cache_key = '_'.join(map(str, cache_key)) 

187 cache = RedisCache() 

188 # check cache 

189 if cache.exists(cache_key): 

190 submissions = json.loads(cache.get(cache_key)) 

191 submission_count = submissions['submission_count'] 

192 submissions = submissions['submissions'] 

193 else: 

194 # convert args 

195 offset = parse_int(offset, 'offset') 

196 count = parse_int(count, 'count') 

197 problem_id = parse_int(problem_id, 'problemId') 

198 status = parse_int(status, 'status') 

199 before = parse_timestamp(before, 'before') 

200 after = parse_timestamp(after, 'after') 

201 ip_addr = parse_str(ip_addr, 'ip_addr') 

202 

203 if language_type is not None: 

204 try: 

205 language_type = list(map(int, language_type.split(','))) 

206 except ValueError as e: 

207 return HTTPError( 

208 'cannot parse integers from languageType', 

209 400, 

210 ) 

211 # students can only get their own submissions 

212 if user.role == User.engine.Role.STUDENT: 

213 username = user.username 

214 try: 

215 params = drop_none({ 

216 'user': user, 

217 'offset': offset, 

218 'count': count, 

219 'problem': problem_id, 

220 'q_user': username, 

221 'status': status, 

222 'language_type': language_type, 

223 'course': course, 

224 'before': before, 

225 'after': after, 

226 'ip_addr': ip_addr 

227 }) 

228 submissions, submission_count = Submission.filter( 

229 **params, 

230 with_count=True, 

231 ) 

232 submissions = [s.to_dict() for s in submissions] 

233 cache.set( 

234 cache_key, 

235 json.dumps({ 

236 'submissions': submissions, 

237 'submission_count': submission_count, 

238 }), 15) 

239 except ValueError as e: 

240 return HTTPError(str(e), 400) 

241 # unicorn gifs 

242 unicorns = [ 

243 'https://media.giphy.com/media/xTiTnLmaxrlBHxsMMg/giphy.gif', 

244 'https://media.giphy.com/media/26AHG5KGFxSkUWw1i/giphy.gif', 

245 'https://media.giphy.com/media/g6i1lEax9Pa24/giphy.gif', 

246 'https://media.giphy.com/media/tTyTbFF9uEbPW/giphy.gif', 

247 ] 

248 ret = { 

249 'unicorn': random.choice(unicorns), 

250 'submissions': submissions, 

251 'submissionCount': submission_count, 

252 } 

253 return HTTPResponse( 

254 'here you are, bro', 

255 data=ret, 

256 ) 

257 

258 

259@submission_api.route('/<submission>', methods=['GET']) 

260@login_required 

261@Request.doc('submission', Submission) 

262def get_submission(user, submission: Submission): 

263 user_feedback_perm = submission.permission(user, 

264 Submission.Permission.FEEDBACK) 

265 # check permission 

266 if submission.handwritten and not user_feedback_perm: 

267 return HTTPError('forbidden.', 403) 

268 # ip validation 

269 problem = Problem(submission.problem_id) 

270 if not problem.is_valid_ip(get_ip()): 

271 return HTTPError('Invalid IP address.', 403) 

272 if not all(submission.timestamp in hw.duration 

273 for hw in problem.running_homeworks() if hw.ip_filters): 

274 return HTTPError('You cannot view this submission during quiz.', 403) 

275 # serialize submission 

276 has_code = not submission.handwritten and user_feedback_perm 

277 has_output = submission.problem.can_view_stdout 

278 ret = submission.to_dict() 

279 if has_code: 

280 try: 

281 ret['code'] = submission.get_main_code() 

282 except UnicodeDecodeError: 

283 ret['code'] = False 

284 if has_output: 

285 ret['tasks'] = submission.get_detailed_result() 

286 else: 

287 ret['tasks'] = submission.get_result() 

288 return HTTPResponse(data=ret) 

289 

290 

291@submission_api.get('/<submission>/output/<int:task_no>/<int:case_no>') 

292@login_required 

293@Request.doc('submission', Submission) 

294def get_submission_output( 

295 user, 

296 submission: Submission, 

297 task_no: int, 

298 case_no: int, 

299): 

300 if not submission.permission(user, Submission.Permission.VIEW_OUTPUT): 

301 return HTTPError('permission denied', 403) 

302 try: 

303 output = submission.get_single_output(task_no, case_no) 

304 except FileNotFoundError as e: 

305 return HTTPError(str(e), 400) 

306 except AttributeError as e: 

307 return HTTPError(str(e), 102) 

308 return HTTPResponse('ok', data=output) 

309 

310 

311@submission_api.route('/<submission>/pdf/<item>', methods=['GET']) 

312@login_required 

313@Request.doc('submission', Submission) 

314def get_submission_pdf(user, submission: Submission, item): 

315 # check the permission 

316 if not submission.permission(user, Submission.Permission.FEEDBACK): 

317 return HTTPError('forbidden.', 403) 

318 # non-handwritten submissions have no pdf file 

319 if not submission.handwritten: 

320 return HTTPError('it is not a handwritten submission.', 400) 

321 if item not in ['comment', 'upload']: 

322 return HTTPError('/<submission_id>/pdf/<"upload" or "comment">', 400) 

323 try: 

324 if item == 'comment': 

325 data = submission.get_comment() 

326 else: 

327 data = submission.get_code('main.pdf', binary=True) 

328 except FileNotFoundError as e: 

329 return HTTPError('File not found.', 404) 

330 return send_file( 

331 io.BytesIO(data), 

332 mimetype='application/pdf', 

333 as_attachment=True, 

334 max_age=0, 

335 download_name=f'{item}-{submission.id[-6:] or "missing-id"}.pdf', 

336 ) 

337 

338 

339@submission_api.route('/<submission>/complete', methods=['PUT']) 

340@Request.json('tasks: list', 'token: str') 

341@Request.doc('submission', Submission) 

342def on_submission_complete(submission: Submission, tasks, token): 

343 if not Submission.verify_token(submission.id, token): 

344 return HTTPError('i don\'t know you', 403) 

345 try: 

346 submission.process_result(tasks) 

347 except (ValidationError, KeyError) as e: 

348 return HTTPError( 

349 'invalid data!\n' 

350 f'{type(e).__name__}: {e}', 

351 400, 

352 ) 

353 return HTTPResponse(f'{submission} result recieved.') 

354 

355 

356@submission_api.route('/<submission>', methods=['PUT']) 

357@login_required 

358@Request.doc('submission', Submission) 

359@Request.files('code') 

360def update_submission(user, submission: Submission, code): 

361 # validate this reques 

362 if submission.status >= 0: 

363 return HTTPError( 

364 f'{submission} has finished judgement.', 

365 403, 

366 ) 

367 # if user not equal, reject 

368 if not secrets.compare_digest(submission.user.username, user.username): 

369 return HTTPError('user not equal!', 403) 

370 # if source code not found 

371 if code is None: 

372 return HTTPError( 

373 f'can not find the source file', 

374 400, 

375 ) 

376 # or empty file 

377 if len(code.read()) == 0: 

378 return HTTPError('empty file', 400) 

379 code.seek(0) 

380 # has been uploaded 

381 if submission.has_code(): 

382 return HTTPError( 

383 f'{submission} has been uploaded source file!', 

384 403, 

385 ) 

386 try: 

387 success = submission.submit(code) 

388 except FileExistsError: 

389 exit(10086) 

390 except ValueError as e: 

391 return HTTPError(str(e), 400) 

392 except JudgeQueueFullError as e: 

393 return HTTPResponse(str(e), 202) 

394 except ValidationError as e: 

395 return HTTPError(str(e), 400, data=e.to_dict()) 

396 except TestCaseNotFound as e: 

397 return HTTPError(str(e), 403) 

398 if success: 

399 return HTTPResponse( 

400 f'{submission} {"is finished." if submission.handwritten else "send to judgement."}' 

401 ) 

402 else: 

403 return HTTPError('Some error occurred, please contact the admin', 500) 

404 

405 

406@submission_api.route('/<submission>/grade', methods=['PUT']) 

407@login_required 

408@Request.json('score: int') 

409@Request.doc('submission', Submission) 

410def grade_submission(user: User, submission: Submission, score: int): 

411 if not submission.permission(user, Submission.Permission.GRADE): 

412 return HTTPError('forbidden.', 403) 

413 

414 if score < 0 or score > 100: 

415 return HTTPError('score must be between 0 to 100.', 400) 

416 

417 # AC if the score is 100, WA otherwise 

418 submission.update(score=score, status=(0 if score == 100 else 1)) 

419 submission.finish_judging() 

420 return HTTPResponse(f'{submission} score recieved.') 

421 

422 

423@submission_api.route('/<submission>/comment', methods=['PUT']) 

424@login_required 

425@Request.files('comment') 

426@Request.doc('submission', Submission) 

427def comment_submission(user, submission: Submission, comment): 

428 if not submission.permission(user, Submission.Permission.COMMENT): 

429 return HTTPError('forbidden.', 403) 

430 

431 if comment is None: 

432 return HTTPError( 

433 f'can not find the comment', 

434 400, 

435 ) 

436 try: 

437 submission.add_comment(comment) 

438 except ValueError as e: 

439 return HTTPError(str(e), 400) 

440 return HTTPResponse(f'{submission} comment recieved.') 

441 

442 

443@submission_api.route('/<submission>/rejudge', methods=['GET']) 

444@login_required 

445@Request.doc('submission', Submission) 

446def rejudge(user, submission: Submission): 

447 if submission.status == -2 or (submission.status == -1 and 

448 (datetime.now() - 

449 submission.last_send).seconds < 300): 

450 return HTTPError(f'{submission} haven\'t be judged', 403) 

451 if not submission.permission(user, Submission.Permission.REJUDGE): 

452 return HTTPError('forbidden.', 403) 

453 try: 

454 success = submission.rejudge() 

455 except ValueError as e: 

456 return HTTPError(str(e), 400) 

457 except JudgeQueueFullError as e: 

458 return HTTPResponse(str(e), 202) 

459 except ValidationError as e: 

460 return HTTPError(str(e), 422, data=e.to_dict()) 

461 if success: 

462 return HTTPResponse(f'{submission} is sent to judgement.') 

463 else: 

464 return HTTPError('Some error occurred, please contact the admin', 500) 

465 

466 

467@submission_api.route('/config', methods=['GET', 'PUT']) 

468@login_required 

469@identity_verify(0) 

470def config(user): 

471 config = Submission.config() 

472 

473 def get_config(): 

474 ret = config.to_mongo() 

475 del ret['_cls'] 

476 del ret['_id'] 

477 return HTTPResponse('success.', data=ret) 

478 

479 @Request.json('rate_limit: int', 'sandbox_instances: list') 

480 def modify_config(rate_limit, sandbox_instances): 

481 # try to convert json object to Sandbox instance 

482 try: 

483 sandbox_instances = [ 

484 *map( 

485 lambda s: engine.Sandbox(**s), 

486 sandbox_instances, 

487 ) 

488 ] 

489 except engine.ValidationError as e: 

490 return HTTPError( 

491 'wrong Sandbox schema', 

492 400, 

493 data=e.to_dict(), 

494 ) 

495 # skip if during testing 

496 if not current_app.config['TESTING']: 

497 resps = [] 

498 # check sandbox status 

499 for sb in sandbox_instances: 

500 resp = rq.get(f'{sb.url}/status') 

501 if not resp.ok: 

502 resps.append((sb.name, resp)) 

503 # some exception occurred 

504 if len(resps) != 0: 

505 return HTTPError( 

506 'some error occurred when check sandbox status', 

507 400, 

508 data=[{ 

509 'name': name, 

510 'statusCode': resp.status_code, 

511 'response': resp.text, 

512 } for name, resp in resps], 

513 ) 

514 try: 

515 config.update( 

516 rate_limit=rate_limit, 

517 sandbox_instances=sandbox_instances, 

518 ) 

519 except ValidationError as e: 

520 return HTTPError(str(e), 400) 

521 

522 return HTTPResponse('success.') 

523 

524 methods = {'GET': get_config, 'PUT': modify_config} 

525 return methods[request.method]() 

526 

527 

528@submission_api.post('/<submission>/migrate-code') 

529@login_required 

530@identity_verify(0) 

531@Request.doc('submission', Submission) 

532def migrate_code(user: User, submission: Submission): 

533 if not submission.permission( 

534 user, 

535 Submission.Permission.MANAGER, 

536 ): 

537 return HTTPError('forbidden.', 403) 

538 

539 submission.migrate_code_to_minio() 

540 return HTTPResponse('ok')