def _clone_solution_comments( from_solution: models.Solution, to_solution: models.Solution, ) -> None: user_comments = models.Comment.by_solution( from_solution.id, ).filter(~models.Comment.is_auto) for comment in user_comments: models.Comment.create_comment( commenter=models.User.get_system_user(), line_number=comment.line_number, comment_text=comment.comment, file=to_solution.solution_files.get(), is_auto=True, ) to_solution.checker = from_solution.checker to_solution.state = from_solution.state to_solution.save() notifications.send( kind=notifications.NotificationKind.CHECKED, user=to_solution.solver, related_id=to_solution, message=_( 'הפתרון שלך לתרגיל %(subject)s נבדק.', subject=to_solution.exercise.subject, ), action_url=f'{routes.SOLUTIONS}/{to_solution.id}', )
def test_mark_as_checked( exercise: Exercise, student_user: User, staff_user: User, solution: Solution, ): # Basic functionality assert solution.state == Solution.STATES.CREATED.name marked = solutions.mark_as_checked(solution.id, staff_user.id) # HELL WITH PEEWEE!!! solution = Solution.get_by_id(solution.id) assert marked assert solution.state == Solution.STATES.DONE.name assert solution.checker == staff_user # Not duplicating things staff_user2 = conftest.create_staff_user(index=1) solution2 = conftest.create_solution(exercise, student_user) marked = solutions.mark_as_checked(solution2.id, staff_user2.id) solution2 = Solution.get_by_id(solution2.id) assert solution2.state == Solution.STATES.DONE.name assert solution2.checker == staff_user2 # Creates notifications assert len(list(notifications.get(student_user))) == 2
def test_valid_solution(self, solution: models.Solution): solution.json_data_str = VALID_CODE solution.save() tasks.run_flake8_on_solution(solution.id) comments = tuple( models.Comment.filter(models.Comment.solution == solution)) assert not comments
def test_solutions_of_user( staff_user: User, student_user: User, course: Course, solution: Solution, _assessments, ): conftest.create_usercourse(student_user, course) client = conftest.get_logged_user(staff_user.username) client.post( f'/assessment/{solution.id}', data=json.dumps({'assessment': 2}), content_type='application/json', ) solution = Solution.get_by_id(solution.id) assert solution.assessment.name == 'Nice' client.post( f'/assessment/{solution.id}', data=json.dumps({'assessment': None}), content_type='application/json', ) exercise2 = conftest.create_exercise(course, 2) solution2 = conftest.create_solution(exercise2, student_user) client.post( f'/assessment/{solution2.id}', data=json.dumps({'assessment': 3}), content_type='application/json', ) solution2 = Solution.get_by_id(solution2.id) exercises = solution.of_user(student_user.id, from_all_courses=True) assert exercises[0].get('assessment') is None assert exercises[1].get('assessment') == 'Try again'
def view( solution_id: int, file_id: Optional[int] = None, shared_url: str = '', ): solution = Solution.get_or_none(Solution.id == solution_id) if solution is None: return fail(404, 'Solution does not exist.') viewer_is_solver = solution.solver.id == current_user.id has_viewer_access = current_user.role.is_viewer if not shared_url and not viewer_is_solver and not has_viewer_access: return fail(403, 'This user has no permissions to view this page.') versions = solution.ordered_versions() test_results = solution.test_results() is_manager = current_user.role.is_manager solution_files = tuple(solution.files) if not solution_files: if not is_manager: return fail(404, 'There are no files in this solution.') return done_checking(solution.exercise.id, solution.id) files = solutions.get_files_tree(solution.files) file_id = file_id or files[0]['id'] file_to_show = next((f for f in solution_files if f.id == file_id), None) if file_to_show is None: return fail(404, 'File does not exist.') view_params = { 'solution': model_to_dict(solution), 'files': files, 'comments': solution.comments_per_file, 'current_file': file_to_show, 'is_manager': is_manager, 'role': current_user.role.name.lower(), 'versions': versions, 'test_results': test_results, 'shared_url': shared_url, } if is_manager: view_params = { **view_params, 'exercise_common_comments': _common_comments(exercise_id=solution.exercise), 'all_common_comments': _common_comments(), 'user_comments': _common_comments(user_id=current_user.id), 'left': Solution.left_in_exercise(solution.exercise), } if viewer_is_solver: notifications.read_related(solution_id, current_user.id) return render_template('view.html', **view_params)
def test_pyflake_wont_execute_code(self, solution: models.Solution): solution.json_data_str = self.execute_script solution.save() tasks.run_flake8_on_solution(solution.id) comments = tuple( models.Comment.filter(models.Comment.solution == solution)) assert not os.listdir(self.test_directory) assert len(comments) == 2 exec(compile(self.execute_script, '', 'exec')) # noqa S102 assert os.listdir(self.test_directory) == ['some-file']
def start_checking(exercise_id): if exercise_id != 0: next_exercise = Solution.next_unchecked_of(exercise_id) else: next_exercise = Solution.next_unchecked() if next_exercise and next_exercise.start_checking(): general_tasks.reset_solution_state_if_needed.apply_async( args=(next_exercise.id, ), countdown=Solution.MAX_CHECK_TIME_SECONDS, ) return redirect(f'/view/{next_exercise.id}') return redirect('/exercises')
def test_simple_course(self): course_1 = list(Solution.status(course_id=self.course1.id).dicts()) course_2 = list(Solution.status(course_id=self.course2.id).dicts()) assert len(course_1) == 2 assert len(course_2) == 1 ex1_1 = next(filter(lambda e: e['id'] == self.ex1_1.id, course_1)) ex1_2 = next(filter(lambda e: e['id'] == self.ex1_2.id, course_1)) ex2_1 = next(filter(lambda e: e['id'] == self.ex2_1.id, course_2)) assert ex1_1['submitted'] == 2 assert ex1_2['submitted'] == 1 assert ex2_1['submitted'] == 1
def upload(): user_id = current_user.id user = User.get_or_none(User.id == user_id) # should never happen if user is None: return fail(404, 'user not found') if request.content_length > MAX_REQUEST_SIZE: return fail(413, 'File is too heavy. 500KB allowed') file: FileStorage = request.files.get('file') if not file: return fail(422, 'No file was given') json_file_data = file.read() try: file_content = json.loads(json_file_data) exercises = list(extract_exercises(file_content)) except (ValueError, json.JSONDecodeError): return fail(422, 'Invalid file format - must be ipynb') if not exercises: msg = 'No exercises were found in the notebook' desc = 'did you use Upload <number of exercise> ? (example: Upload 1)' return fail(422, f'{msg}, {desc}') matches, misses = set(), set() for exercise_id, code in exercises: exercise = Exercise.get_or_none(Exercise.id == exercise_id) if exercise is None: misses.add(exercise_id) continue if not exercise.open_for_new_solutions(): misses.add(exercise_id) continue if Solution.solution_exists( exercise=exercise, solver=user, json_data_str=code, ): continue solution = Solution.create_solution( exercise=exercise, solver=user, json_data_str=code, ) flake8_tasks.run_flake8_on_solution.apply_async(args=(solution.id, )) identical_tests_tasks.solve_solution_with_identical_code.apply_async( args=(solution.id, )) matches.add(exercise_id) return jsonify({ 'exercise_matches': list(matches), 'exercise_misses': list(misses), })
def view( solution_id: int, file_id: Optional[int] = None, shared_url: str = '', ): solution = Solution.get_or_none(Solution.id == solution_id) if solution is None: return fail(404, 'Solution does not exist.') viewer_is_solver = solution.solver.id == current_user.id has_viewer_access = current_user.role.is_viewer if not shared_url and not viewer_is_solver and not has_viewer_access: return fail(403, 'This user has no permissions to view this page.') is_manager = current_user.role.is_manager solution_files = tuple(solution.files) if not solution_files: if not is_manager: return fail(404, 'There are no files in this solution.') return done_checking(solution.exercise.id, solution.id) try: view_params = solutions.get_view_parameters( solution, file_id, shared_url, is_manager, solution_files, viewer_is_solver, ) except LmsError as e: error_message, status_code = e.args return fail(status_code, error_message) return render_template('view.html', **view_params)
def _upload_to_db( exercise_number: int, course_id: int, user_id: int, files: List[File], solution_hash: Optional[str] = None, ) -> Solution: exercise = Exercise.get_or_none(course=course_id, number=exercise_number) user = User.get_by_id(user_id) if exercise is None: raise UploadError(f'No such exercise id: {exercise_number}') elif not user.has_course(course_id): raise UploadError( f'Exercise {exercise_number} is invalid for this user.', ) elif not exercise.open_for_new_solutions(): raise UploadError( f'Exercise {exercise_number} is closed for new solutions.') if solution_hash and _is_uploaded_before(user, exercise, solution_hash): raise AlreadyExists('You try to reupload an old solution.') elif not files: raise UploadError( f'There are no files to upload for {exercise_number}.', ) return Solution.create_solution( exercise=exercise, solver=user, files=files, hash_=solution_hash, )
def download(download_id: str): """Downloading a zip file of the code files. Args: download_id (str): Can be on each side of a solution.id and sharedsolution.shared_url. """ solution = Solution.get_or_none(Solution.id == download_id) shared_solution = SharedSolution.get_or_none( SharedSolution.shared_url == download_id, ) if solution is None and shared_solution is None: return fail(404, 'Solution does not exist.') if shared_solution is None: viewer_is_solver = solution.solver.id == current_user.id has_viewer_access = current_user.role.is_viewer if not viewer_is_solver and not has_viewer_access: return fail(403, 'This user has no permissions to view this page.') files = solution.files filename = solution.exercise.subject else: files = shared_solution.solution.files filename = shared_solution.solution.exercise.subject response = make_response(solutions.create_zip_from_solution(files)) response.headers.set('Content-Type', 'zip') response.headers.set( 'Content-Disposition', 'attachment', filename=f'{filename}.zip', ) return response
def view(solution_id): solution = Solution.get_or_none(Solution.id == solution_id) if solution is None: return fail(404, 'Solution does not exist.') is_manager = current_user.role.is_manager if solution.solver.id != current_user.id and not is_manager: return fail(403, 'This user has no permissions to view this page.') versions = solution.ordered_versions() view_params = { 'solution': model_to_dict(solution), 'is_manager': is_manager, 'role': current_user.role.name.lower(), 'versions': versions, } if is_manager: view_params = { **view_params, 'exercise_common_comments': _common_comments(exercise_id=solution.exercise), 'all_common_comments': _common_comments(), 'user_comments': _common_comments(user_id=current_user.id), } return render_template('view.html', **view_params)
def test_strange_solution_with_no_files( exercise: Exercise, student_user: User, staff_user: User, ): solution = conftest.create_solution( exercise, student_user, files=[], hash_='koko', ) staff_client = conftest.get_logged_user(staff_user.username) view_response = staff_client.get(f'{routes.SOLUTIONS}/{solution.id}') assert view_response.status_code == 200 solution = Solution.get_by_id(solution.id) assert solution.state == Solution.STATES.DONE.name to_show_in_view = { 'solution': solution, 'file_id': None, 'shared_url': '', 'is_manager': False, 'solution_files': (), 'viewer_is_solver': True, } with pytest.raises(ResourceNotFound): assert get_view_parameters(**to_show_in_view) user_client = conftest.get_logged_user(student_user.username) assert len(list(notifications.get(student_user))) == 1 view_response = user_client.get(f'{routes.SOLUTIONS}/{solution.id}') assert view_response.status_code == 404
def get_view_parameters( solution: Solution, file_id: Optional[int], shared_url: str, is_manager: bool, solution_files: Tuple[SolutionFile, ...], viewer_is_solver: bool, ) -> Dict[str, Any]: versions = solution.ordered_versions() test_results = solution.test_results() files = get_files_tree(solution.files) file_id = file_id or (files[0]['id'] if files else None) file_to_show = next((f for f in solution_files if f.id == file_id), None) if file_to_show is None: raise ResourceNotFound('File does not exist.', 404) view_params = { 'solution': model_to_dict(solution), 'files': files, 'comments': solution.comments_per_file, 'current_file': file_to_show, 'is_manager': is_manager, 'role': current_user.role.name.lower(), 'versions': versions, 'test_results': test_results, 'shared_url': shared_url, 'image_extensions': ALLOWED_IMAGES_EXTENSIONS, } if is_manager: view_params = { **view_params, 'exercise_common_comments': comments._common_comments(exercise_id=solution.exercise), 'all_common_comments': comments._common_comments(), 'user_comments': comments._common_comments(user_id=current_user.id), 'left': Solution.left_in_exercise(solution.exercise), } if viewer_is_solver: notifications.read_related(solution.id, current_user.id) return view_params
def test_next_exercise_with_cleanest_code( self, comment: Comment, staff_user: User, ): student_user: User = conftest.create_student_user(index=1) first_solution = comment.solution comment_text = comment.comment second_solution = conftest.create_solution(comment.solution.exercise, student_user) # comment exists on first solution - second one should be the first next_unchecked = Solution.next_unchecked() assert next_unchecked is not None assert next_unchecked.id == second_solution.id # delete the comment should give us back the first solution Comment.delete_by_id(comment.id) next_unchecked = Solution.next_unchecked() assert next_unchecked is not None assert next_unchecked.id == first_solution.id # if second_solution has comments we should get first solution Comment.create_comment( commenter=staff_user, line_number=1, comment_text=comment_text, file=second_solution.solution_files.get(), is_auto=False, ) next_unchecked = Solution.next_unchecked() assert next_unchecked is not None assert next_unchecked.id == first_solution.id # both have comments - the first one should be the first solution Comment.create_comment( commenter=staff_user, line_number=1, comment_text=comment_text, file=first_solution.solution_files.get(), is_auto=False, ) next_unchecked = Solution.next_unchecked() assert next_unchecked is not None assert next_unchecked.id == first_solution.id
def test_last_view_status( solution: Solution, student_user: User, staff_user: User, ): client = conftest.get_logged_user(student_user.username) assert solution.last_status_view == SolutionStatusView.UPLOADED.name client.get(f'/view/{solution.id}') solution = Solution.get_by_id(solution.id) assert solution.last_status_view == SolutionStatusView.NOT_CHECKED.name solutions.mark_as_checked(solution.id, staff_user.id) solution = Solution.get_by_id(solution.id) assert solution.last_status_view == SolutionStatusView.NOT_CHECKED.name client.get(f'/view/{solution.id}') solution = Solution.get_by_id(solution.id) assert solution.last_status_view == SolutionStatusView.CHECKED.name
def exercises_page(): fetch_archived = bool(request.args.get('archived')) exercises = Solution.of_user(current_user.id, fetch_archived) is_manager = current_user.role.is_manager return render_template( 'exercises.html', exercises=exercises, is_manager=is_manager, fetch_archived=fetch_archived, )
def test_all_together(self): all_courses = list(Solution.status().dicts()) assert len(all_courses) == 3 ex1_1 = next(filter(lambda e: e['id'] == self.ex1_1.id, all_courses)) ex1_2 = next(filter(lambda e: e['id'] == self.ex1_2.id, all_courses)) ex2_1 = next(filter(lambda e: e['id'] == self.ex2_1.id, all_courses)) assert ex1_1['submitted'] == 2 assert ex1_2['submitted'] == 1 assert ex2_1['submitted'] == 1
def _is_uploaded_before( user: User, exercise: Exercise, file_hash: str, ) -> bool: return Solution.is_duplicate( file_hash, user, exercise, already_hashed=True, )
def test_invalid_solution(self, solution: models.Solution): solution.json_data_str = INVALID_CODE solution.save() tasks.run_flake8_on_solution(solution.id) comments = tuple( models.Comment.filter(models.Comment.solution == solution)) assert comments assert len(comments) == 1 comment = comments[0].comment assert comment.text == INVALID_CODE_MESSAGE assert comment.flake8_key == INVALID_CODE_KEY user_notifications = notifications.get_notifications_for_user( for_user=solution.solver) assert len(user_notifications) == 1 assert user_notifications parameters = user_notifications[0]['message_parameters'] subject = parameters['exercise_name'] errors = parameters['errors'] assert solution.exercise.subject == subject assert 1 == errors
def test_new_solution_override_old_solutions( self, exercise: Exercise, student_user: User, ): first_solution = conftest.create_solution(exercise, student_user) second_solution = conftest.create_solution(exercise, student_user) assert second_solution.state == self.created_state assert first_solution.refresh().state == self.old_solution_state next_unchecked = Solution.next_unchecked() assert next_unchecked is not None assert next_unchecked.id == second_solution.id next_unchecked = Solution.next_unchecked_of(exercise.id) assert next_unchecked is not None assert next_unchecked.id == second_solution.id assert next_unchecked.start_checking() assert next_unchecked.refresh().state == self.in_checking_state assert Solution.next_unchecked() is None assert Solution.next_unchecked_of(exercise.id) is None general_tasks.reset_solution_state_if_needed(second_solution.id) next_unchecked = Solution.next_unchecked() next_unchecked_by_id = Solution.next_unchecked_of(exercise.id) assert next_unchecked is not None and next_unchecked_by_id is not None assert next_unchecked.id == second_solution.id assert next_unchecked_by_id.id == second_solution.id third_solution = conftest.create_solution( exercise, student_user, code='123', ) fourth_solution = conftest.create_solution( exercise, student_user, code='1234', ) assert not Solution.is_duplicate( third_solution.hashed, student_user, exercise, already_hashed=True, ) assert Solution.is_duplicate( fourth_solution.hashed, student_user, exercise, already_hashed=True, )
def done_checking(exercise_id, solution_id): checked_solution: Solution = Solution.get_by_id(solution_id) is_updated = checked_solution.set_state(new_state=Solution.STATES.DONE) if is_updated: notifications.create_notification( notification_type=( notifications.SolutionCheckedNotification.notification_type()), for_user=checked_solution.solver, solution=checked_solution, ) identical_tests_tasks.check_if_other_solutions_can_be_solved.apply_async( args=(solution_id, )) next_exercise = None solution = Solution.next_unchecked_of(exercise_id) if solution and solution.start_checking(): general_tasks.reset_solution_state_if_needed.apply_async( args=(solution.id, ), countdown=Solution.MAX_CHECK_TIME_SECONDS, ) next_exercise = solution.id return jsonify({'success': is_updated, 'next': next_exercise})
def user(user_id): if user_id != current_user.id and not current_user.role.is_manager: return fail(403, "You aren't allowed to watch this page.") target_user = User.get_or_none(User.id == user_id) if target_user is None: return fail(404, 'There is no such user.') return render_template( 'user.html', solutions=Solution.of_user(target_user.id, with_archived=True), user=target_user, )
def test_new_solution_override_old_solutions( self, exercise: Exercise, student_user: User, ): first_solution = conftest.create_solution(exercise, student_user) second_solution = conftest.create_solution(exercise, student_user) assert second_solution.state == self.created_state assert first_solution.refresh().state == self.old_solution_state next_unchecked = Solution.next_unchecked() assert next_unchecked is not None assert next_unchecked.id == second_solution.id next_unchecked = Solution.next_unchecked_of(exercise.id) assert next_unchecked is not None assert next_unchecked.id == second_solution.id assert next_unchecked.start_checking() assert next_unchecked.refresh().state == self.in_checking_state assert Solution.next_unchecked() is None assert Solution.next_unchecked_of(exercise.id) is None general_tasks.reset_solution_state_if_needed(second_solution.id) next_unchecked = Solution.next_unchecked() next_unchecked_by_id = Solution.next_unchecked_of(exercise.id) assert next_unchecked is not None and next_unchecked_by_id is not None assert next_unchecked.id == second_solution.id assert next_unchecked_by_id.id == second_solution.id
def create_solution( exercise: Exercise, student_user: User, code: Optional[str] = None, ) -> Solution: if code is None: code = ''.join(random.choices(string.printable, k=100)) return Solution.create_solution( exercise=exercise, solver=student_user, files=[File('exercise.py', code)], )
def _clone_solution_comments( from_solution: models.Solution, to_solution: models.Solution, ) -> None: user_comments = models.Comment.by_solution( from_solution.id, ).filter(~models.Comment.is_auto) for comment in user_comments: models.Comment.create_comment( commenter=models.User.get_system_user(), line_number=comment.line_number, comment_text=comment.comment.comment_id, solution=to_solution, is_auto=True, ) to_solution.checker = from_solution.checker to_solution.state = from_solution.state to_solution.save() notifications.create_notification( notification_type=( notifications.SolutionCheckedNotification.notification_type()), for_user=to_solution.solver, solution=to_solution, )
def mark_as_checked(solution_id: int, checker_id: int) -> bool: checked_solution: Solution = Solution.get_by_id(solution_id) is_updated = checked_solution.mark_as_checked(by=checker_id) msg = f'הפתרון שלך לתרגיל "{checked_solution.exercise.subject}" נבדק.' if is_updated: notifications.send( kind=notifications.NotificationKind.CHECKED, user=checked_solution.solver, related_id=solution_id, message=msg, action_url=f'{routes.SOLUTIONS}/{solution_id}', ) if config.FEATURE_FLAG_CHECK_IDENTICAL_CODE_ON: (identical_tests_tasks.check_if_other_solutions_can_be_solved. apply_async(args=(solution_id, ))) return is_updated
def comment(): act = request.args.get('act') or request.json.get('act') if request.method == 'POST': solution_id = int(request.json.get('solutionId', 0)) else: # it's a GET solution_id = int(request.args.get('solutionId', 0)) solution = Solution.get_or_none(Solution.id == solution_id) if solution is None: return fail(404, f'No such solution {solution_id}') solver_id = solution.solver.id if solver_id != current_user.id and not current_user.role.is_manager: return fail(401, "You aren't allowed to watch this page.") if act == 'fetch': return jsonify(Comment.get_solutions(solution_id)) if act == 'delete': comment_id = int(request.args.get('commentId')) comment_ = Comment.get_or_none(Comment.id == comment_id) if comment_ is not None: comment_.delete_instance() return jsonify({'success': 'true'}) if act == 'create': kind = request.json.get('kind', '') comment_id, comment_text = None, None try: line_number = int(request.json.get('line', 0)) except ValueError: line_number = 0 if kind.lower() == 'id': comment_id = int(request.json.get('comment', 0)) if kind.lower() == 'text': comment_text = request.json.get('comment', '') return _create_comment( current_user.id, solution, kind, line_number, comment_text, comment_id, ) return fail(400, f'Unknown or unset act value "{act}"')
def test_start_checking(exercise: Exercise, student_user: User): student_user2 = conftest.create_student_user(index=1) exercise2 = conftest.create_exercise(1) solution1 = conftest.create_solution(exercise, student_user) solution2 = conftest.create_solution(exercise2, student_user) solution3 = conftest.create_solution(exercise, student_user2) is_checking = solutions.start_checking(solution=None) assert not is_checking with no_celery: for solution in (solution1, solution2, solution3): assert solution.state == Solution.STATES.CREATED.name is_checking = solutions.start_checking(solution=solution) assert is_checking the_solution = Solution.get_by_id(solution.id) assert the_solution.state == Solution.STATES.IN_CHECKING.name