Coverage for model/submission.py: 72%

290 statements  

« 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 * 

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

124 

125 

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

146 

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

154 

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

162 

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

170 

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 ) 

184 

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

201 

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 ) 

256 

257 

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) 

288 

289 

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) 

308 

309 

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 ) 

336 

337 

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

353 

354 

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) 

403 

404 

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) 

412 

413 if score < 0 or score > 100: 

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

415 

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

420 

421 

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) 

429 

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

440 

441 

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) 

466 

467 

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

469@login_required 

470@identity_verify(0) 

471def config(user): 

472 config = Submission.config() 

473 

474 def get_config(): 

475 ret = config.to_mongo() 

476 del ret['_cls'] 

477 del ret['_id'] 

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

479 

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) 

522 

523 return HTTPResponse('success.') 

524 

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

526 return methods[request.method]()