def delete_snippets(snippet_id: int) -> EmptyResponse: """Delete the :class:`.models.Snippet` with the given id. .. :quickref: Snippet; Delete a snippet. :param int snippet_id: The id of the snippet :returns: An empty response with return code 204 :raises APIException: If the snippet with the given id does not exist. (OBJECT_ID_NOT_FOUND) :raises APIException: If the snippet does not belong the current user. (INCORRECT_PERMISSION) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not use snippets. (INCORRECT_PERMISSION) """ snip: t.Optional[models.Snippet] snip = helpers.get_or_404(models.Snippet, snippet_id) snip = models.Snippet.query.get(snippet_id) assert snip is not None if snip.user_id != current_user.id: raise APIException( 'The given snippet is not your snippet', 'The snippet "{}" does not belong to user "{}"'.format( snip.id, current_user.id), APICodes.INCORRECT_PERMISSION, 403) else: db.session.delete(snip) db.session.commit() return make_empty_response()
def unselect_rubric_item(submission_id: int, rubric_item_id: int) -> EmptyResponse: """Unselect the given rubric item for the given submission. .. :quickref: Submission; Unselect the given rubric item. :param submission_id: The submission to unselect the item for. :param rubric_item_id: The rubric items id to unselect. :returns: Nothing. """ submission = helpers.get_or_404(models.Work, submission_id) auth.ensure_permission('can_grade_work', submission.assignment.course_id) new_items = [ item for item in submission.selected_items if item.id != rubric_item_id ] if len(new_items) == len(submission.selected_items): raise APIException( 'Selected rubric item was not selected for this submission', f'The item {rubric_item_id} is not selected for {submission_id}', APICodes.INVALID_PARAM, 400) submission.selected_items = new_items db.session.commit() return make_empty_response()
def delete_submission(submission_id: int) -> EmptyResponse: """Delete a submission and all its files. .. :quickref: Submission; Delete a submission and all its files. .. warning:: This is irreversible, so make sure the user really wants this! :param submission_id: The submission to delete. :returns: Nothing """ submission = helpers.get_or_404(models.Work, submission_id) auth.ensure_permission('can_delete_submission', submission.assignment.course_id) for sub_file in db.session.query(models.File).filter_by( work_id=submission_id, is_directory=False).all(): try: sub_file.delete_from_disk() except FileNotFoundError: # pragma: no cover pass db.session.delete(submission) db.session.commit() return make_empty_response()
def delete_role(course_id: int, role_id: int) -> EmptyResponse: """Remove a :class:`.models.CourseRole` from the given :class:`.models.Course`. .. :quickref: Course; Delete a course role from a course. :param int course_id: The id of the course :returns: An empty response with return code 204 :raises APIException: If the role with the given ids does not exist. (OBJECT_NOT_FOUND) :raises APIException: If there are still users with this role. (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not manage the course with the given id. (INCORRECT_PERMISSION) """ auth.ensure_permission(CPerm.can_edit_course_roles, course_id) course = helpers.get_or_404( models.Course, course_id, also_error=lambda c: c.virtual, ) role = helpers.filter_single_or_404( models.CourseRole, models.CourseRole.course_id == course_id, models.CourseRole.id == role_id, also_error=lambda r: r.hidden, ) if course.lti_provider is not None: if LTICourseRole.codegrade_role_name_used(role.name): lms = course.lti_provider.lms_name raise APIException( f'You cannot delete default {lms} roles', ('The course "{}" is an LTI course so it is impossible to ' 'delete role {}').format(course.id, role.id), APICodes.INCORRECT_PERMISSION, 403) users_with_role = db.session.query(models.user_course).filter( models.user_course.c.course_id == role_id).exists() if db.session.query(users_with_role).scalar(): raise APIException( 'There are still users with this role', 'There are still users with role {}'.format(role_id), APICodes.INVALID_PARAM, 400) links_with_role = db.session.query( models.CourseRegistrationLink).filter_by( course_role_id=role_id).exists() if db.session.query(links_with_role).scalar(): raise APIException( 'There are still registration links with this role', f'The role "{role_id}" cannot be deleted as it is still in use', APICodes.INVALID_PARAM, 400) db.session.delete(role) db.session.commit() return make_empty_response()
def delete_rubric(assignment_id: int) -> EmptyResponse: """Delete the rubric for the given assignment. .. :quickref: Assignment; Delete the rubric of an assignment. :param assignment_id: The id of the :class:`.models.Assignment` whose rubric should be deleted. :returns: Nothing. :raises PermissionException: If the user does not have the ``manage_rubrics`` permission (INCORRECT_PERMISSION). :raises APIException: If the assignment has no rubric. (OBJECT_ID_NOT_FOUND) """ assig = helpers.get_or_404(models.Assignment, assignment_id) auth.ensure_permission('manage_rubrics', assig.course_id) if not assig.rubric_rows: raise APIException( 'Assignment has no rubric', 'The assignment with id "{}" has no rubric'.format(assignment_id), APICodes.OBJECT_ID_NOT_FOUND, 404 ) assig.rubric_rows = [] db.session.commit() return make_empty_response()
def delete_course_snippets(course_id: int, snippet_id: int) -> EmptyResponse: """Delete the :class:`.models.CourseSnippet` with the given id. .. :quickref: CourseSnippet; Delete a course snippet. :param int snippet_id: The id of the snippet :returns: An empty response with return code 204 :raises APIException: If the snippet with the given id does not exist. (OBJECT_ID_NOT_FOUND) :raises APIException: If the snippet does not belong the current user. (INCORRECT_PERMISSION) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not use snippets. (INCORRECT_PERMISSION) """ auth.ensure_permission(CPerm.can_manage_course_snippets, course_id) course = helpers.get_or_404(models.Course, course_id) snip = helpers.get_or_404( models.CourseSnippet, snippet_id, also_error=lambda snip: snip.course_id != course.id) db.session.delete(snip) db.session.commit() return make_empty_response()
def remove_comment(code_id: int, line: int) -> EmptyResponse: """Removes the given :class:`.models.Comment` in the given :class:`.models.File` .. :quickref: Code; Remove a comment. :param int code_id: The id of the code file :param int line: The line number of the comment :returns: An empty response with return code 204 :raises APIException: If there is no comment at the given line number. (OBJECT_NOT_FOUND) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not can grade work in the attached course. (INCORRECT_PERMISSION) """ comment = helpers.filter_single_or_404( models.Comment, models.Comment.file_id == code_id, models.Comment.line == line ) auth.ensure_permission( 'can_grade_work', comment.file.work.assignment.course_id ) db.session.delete(comment) db.session.commit() return make_empty_response()
def select_rubric_item(submission_id: int, rubricitem_id: int) -> EmptyResponse: """Select a rubric item of the given submission (:class:`.models.Work`). .. :quickref: Submission; Select a rubric item. :param int submission_id: The id of the submission :param int rubricitem_id: The id of the rubric item :returns: Nothing. :raises APIException: If either the submission or rubric item with the given ids does not exist. (OBJECT_ID_NOT_FOUND) :raises APIException: If the assignment of the rubric is not the assignment of the submission. (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not grade the given submission (INCORRECT_PERMISSION) """ work = helpers.get_or_404(models.Work, submission_id) rubric_item = helpers.get_or_404(models.RubricItem, rubricitem_id) auth.ensure_permission('can_grade_work', work.assignment.course_id) if rubric_item.rubricrow.assignment_id != work.assignment_id: raise APIException( 'Rubric item selected does not match assignment', 'The rubric item with id {} does not match the assignment'.format( rubricitem_id), APICodes.INVALID_PARAM, 400) work.remove_selected_rubric_item(rubric_item.rubricrow_id) work.select_rubric_items([rubric_item], current_user, False) db.session.commit() return make_empty_response()
def user_patch_handle_change_user_data() -> EmptyResponse: """Handle the PATCH login route when no ``type`` is given. :returns: An empty response. """ data = ensure_json_dict( request.get_json(), replace_log=lambda k, v: f'<PASSWORD "{k}">' if 'password' in k else v ) ensure_keys_in_dict( data, [ ('email', str), ('old_password', str), ('name', str), ('new_password', str) ] ) email = t.cast(str, data['email']) old_password = t.cast(str, data['old_password']) new_password = t.cast(str, data['new_password']) name = t.cast(str, data['name']) def _ensure_password( changed: str, msg: str = 'To change your {} you need a correct old password.' ) -> None: if current_user.password != old_password: raise APIException( msg.format(changed), 'The given old password was not correct', APICodes.INVALID_CREDENTIALS, 403 ) if old_password != '': _ensure_password('', 'The given old password is wrong') if current_user.email != email: auth.ensure_permission(GPerm.can_edit_own_info) _ensure_password('email') validate.ensure_valid_email(email) current_user.email = email if new_password != '': auth.ensure_permission(GPerm.can_edit_own_password) _ensure_password('password') validate.ensure_valid_password(new_password, user=current_user) current_user.password = new_password if current_user.name != name: auth.ensure_permission(GPerm.can_edit_own_info) if name == '': raise APIException( 'Your new name cannot be empty', 'The given new name was empty', APICodes.INVALID_PARAM, 400 ) current_user.name = name db.session.commit() return make_empty_response()
def user_patch_handle_reset_on_lti() -> EmptyResponse: """Handle the ``reset_on_lti`` type for the PATCH login route. :returns: An empty response. """ auth.ensure_logged_in() current_user.reset_email_on_lti = True db.session.commit() return make_empty_response()
def update_role(course_id: int, role_id: int) -> EmptyResponse: """Update the :class:`.models.Permission` of a given :class:`.models.CourseRole` in the given :class:`.models.Course`. .. :quickref: Course; Update a permission for a certain role. :param int course_id: The id of the course. :param int role_id: The id of the course role. :returns: An empty response with return code 204. :<json str permission: The name of the permission to change. :<json bool value: The value to set the permission to (``True`` means the specified role has the specified permission). :raises APIException: If the value or permission parameter are not in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If the role with the given id does not exist or the permission with the given name does not exist. (OBJECT_NOT_FOUND) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not manage the course with the given id. (INCORRECT_PERMISSION) """ content = ensure_json_dict(request.get_json()) auth.ensure_permission('can_edit_course_roles', course_id) ensure_keys_in_dict(content, [('value', bool), ('permission', str)]) value = t.cast(bool, content['value']) permission = t.cast(str, content['permission']) role = helpers.filter_single_or_404( models.CourseRole, models.CourseRole.course_id == course_id, models.CourseRole.id == role_id, ) perm = helpers.filter_single_or_404( models.Permission, models.Permission.name == permission, models.Permission.course_permission == True, # pylint: disable=singleton-comparison ) if (current_user.courses[course_id].id == role.id and perm.name == 'can_edit_course_roles'): raise APIException( 'You cannot remove this permission from your own role', ('The current user is in role {} which' ' cannot remove "can_edit_course_roles"').format(role.id), APICodes.INCORRECT_PERMISSION, 403) role.set_permission(perm, value) db.session.commit() return make_empty_response()
def patch_course_snippet(course_id: int, snippet_id: int) -> EmptyResponse: """Modify the :class:`.models.CourseSnippet` with the given id. .. :quickref: CourseSnippet; Change a snippets key and value. :param int snippet_id: The id of the snippet to change. :returns: An empty response with return code 204. :>json str key: The new key of the snippet. :>json str value: The new value of the snippet. :raises APIException: If the parameters "key" and/or "value" were not in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If the snippet does not belong to the current user. (INCORRECT_PERMISSION) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not use snippets. (INCORRECT_PERMISSION) :raises APIException: If another snippet with the same key already exists. (OBJECT_ALREADY_EXISTS) """ with helpers.get_from_request_transaction() as [get, _]: value = get('value', str) key = get('key', str) course = helpers.get_or_404(models.Course, course_id, with_for_update=True, with_for_update_of=models.Course) auth.CoursePermissions(course).ensure_may_edit_snippets() snip = helpers.get_or_404( models.CourseSnippet, snippet_id, also_error=lambda snip: snip.course_id != course.id) other = models.CourseSnippet.query.filter_by( course=course, key=key, ).one_or_none() if other is not None and other.id != snippet_id: raise APIException( 'A snippet with the same key already exists.', 'A snippet with key "{}" already exists for course "{}"'.format( key, course_id), APICodes.OBJECT_ALREADY_EXISTS, 400, ) snip.key = key snip.value = value db.session.commit() return make_empty_response()
def patch_course_snippet(course_id: int, snippet_id: int) -> EmptyResponse: """Modify the :class:`.models.CourseSnippet` with the given id. .. :quickref: CourseSnippet; Change a snippets key and value. :param int snippet_id: The id of the snippet to change. :returns: An empty response with return code 204. :<json str key: The new key of the snippet. :<json str value: The new value of the snippet. :raises APIException: If the parameters "key" and/or "value" were not in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If the snippet does not belong to the current user. (INCORRECT_PERMISSION) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not use snippets. (INCORRECT_PERMISSION) :raises APIException: If another snippet with the same key already exists. (OBJECT_ALREADY_EXISTS) """ auth.ensure_permission(CPerm.can_manage_course_snippets, course_id) content = get_json_dict_from_request() ensure_keys_in_dict(content, [('key', str), ('value', str)]) key = t.cast(str, content['key']) value = t.cast(str, content['value']) course = helpers.get_or_404(models.Course, course_id) snip = helpers.get_or_404( models.CourseSnippet, snippet_id, also_error=lambda snip: snip.course_id != course.id) other = models.CourseSnippet.query.filter_by( course=course, key=key, ).first() if other is not None and other.id != snippet_id: raise APIException( 'A snippet with the same key already exists.', 'A snippet with key "{}" already exists for course "{}"'.format( key, course_id), APICodes.OBJECT_ALREADY_EXISTS, 400, ) snip.key = key snip.value = value db.session.commit() return make_empty_response()
def update_role(course_id: int, role_id: int) -> EmptyResponse: """Update the :class:`.models.Permission` of a given :class:`.models.CourseRole` in the given :class:`.models.Course`. .. :quickref: Course; Update a permission for a certain role. :param int course_id: The id of the course. :param int role_id: The id of the course role. :returns: An empty response with return code 204. :<json str permission: The name of the permission to change. :<json bool value: The value to set the permission to (``True`` means the specified role has the specified permission). :raises APIException: If the value or permission parameter are not in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If the role with the given id does not exist or the permission with the given name does not exist. (OBJECT_NOT_FOUND) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not manage the course with the given id. (INCORRECT_PERMISSION) """ course = helpers.get_or_404(models.Course, course_id) auth.CoursePermissions(course).ensure_may_edit_roles() with helpers.get_from_request_transaction() as [get, _]: value = get('value', bool) permission = get('permission', CPerm) role = helpers.filter_single_or_404( models.CourseRole, models.CourseRole.course == course, models.CourseRole.id == role_id, also_error=lambda r: r.hidden, ) if (current_user.courses[course_id].id == role.id and permission == CPerm.can_edit_course_roles): raise APIException( 'You cannot remove this permission from your own role', ('The current user is in role {} which' ' cannot remove "can_edit_course_roles"').format(role.id), APICodes.INCORRECT_PERMISSION, 403) role.set_permission(permission, value) db.session.commit() return make_empty_response()
def put_comment(code_id: int, line: int) -> EmptyResponse: """Create or change a single :class:`.models.Comment` of a code :class:`.models.File`. .. :quickref: Code; Add or change a comment. :param int code_id: The id of the code file :param int line: The line number of the comment :returns: An empty response with return code 204 :<json str comment: The comment to add to the given file on the given line. :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not can grade work in the attached course. (INCORRECT_PERMISSION) """ comment = db.session.query(models.Comment).filter( models.Comment.file_id == code_id, models.Comment.line == line ).one_or_none() def __get_comment() -> str: content = ensure_json_dict(request.get_json()) ensure_keys_in_dict(content, [('comment', str)]) return t.cast(str, content['comment']) if comment: auth.ensure_permission( 'can_grade_work', comment.file.work.assignment.course_id ) comment.comment = __get_comment() else: file = helpers.get_or_404(models.File, code_id) auth.ensure_permission( 'can_grade_work', file.work.assignment.course_id, ) db.session.add( models.Comment( file_id=code_id, user_id=current_user.id, line=line, comment=__get_comment(), ) ) db.session.commit() return make_empty_response()
def user_patch_handle_send_reset_email() -> EmptyResponse: """Handle the ``reset_email`` type for the PATCH login route. :returns: An empty response. """ data = ensure_json_dict(request.get_json()) ensure_keys_in_dict(data, [('username', str)]) mail.send_reset_password_email( helpers.filter_single_or_404(models.User, models.User.username == data['username'])) db.session.commit() return make_empty_response()
def add_role(course_id: int) -> EmptyResponse: """Add a new :class:`.models.CourseRole` to the given :class:`.models.Course`. .. :quickref: Course; Add a new course role to a course. :param int course_id: The id of the course :returns: An empty response with return code 204. :<json str name: The name of the new course role. :raises APIException: If the name parameter was not in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If the course with the given id was not found. (OBJECT_NOT_FOUND) :raises APIException: If the course already has a role with the submitted name. (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not manage the course with the given id. (INCORRECT_PERMISSION) """ auth.ensure_permission(CPerm.can_edit_course_roles, course_id) content = get_json_dict_from_request() ensure_keys_in_dict(content, [('name', str)]) name = t.cast(str, content['name']) course = helpers.get_or_404( models.Course, course_id, also_error=lambda c: c.virtual, ) if models.CourseRole.query.filter_by( name=name, course_id=course_id).first() is not None: raise APIException( 'This course already has a role with this name', 'The course "{}" already has a role named "{}"'.format( course_id, name), APICodes.INVALID_PARAM, 400) role = models.CourseRole(name=name, course=course, hidden=False) db.session.add(role) db.session.commit() return make_empty_response()
def delete_submission_grader(submission_id: int) -> EmptyResponse: """Change the assigned grader of the given submission. .. :quickref: Submission; Delete grader for the submission. :returns: Empty response and a 204 status. :raises PermissionException: If the logged in user cannot manage the course of the submission. (INCORRECT_PERMISSION) """ work = helpers.get_or_404(models.Work, submission_id) auth.ensure_permission('can_assign_graders', work.assignment.course_id) work.assignee = None db.session.commit() return make_empty_response()
def delete_role(course_id: int, role_id: int) -> EmptyResponse: """Remove a :class:`.models.CourseRole` from the given :class:`.models.Course`. .. :quickref: Course; Delete a course role from a course. :param int course_id: The id of the course :returns: An empty response with return code 204 :raises APIException: If the role with the given ids does not exist. (OBJECT_NOT_FOUND) :raises APIException: If there are still users with this role. (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not manage the course with the given id. (INCORRECT_PERMISSION) """ auth.ensure_permission('can_edit_course_roles', course_id) course = helpers.get_or_404(models.Course, course_id) role = helpers.filter_single_or_404( models.CourseRole, models.CourseRole.course_id == course_id, models.CourseRole.id == role_id) if course.lti_provider is not None: if any(r['role'] == role.name for r in LTI_ROLE_LOOKUPS.values()): raise APIException( 'You cannot delete default LTI roles for a LTI course', ('The course "{}" is an LTI course ' 'so it is impossible to delete role {}').format( course.id, role.id), APICodes.INCORRECT_PERMISSION, 403) sql = db.session.query(models.user_course).filter( models.user_course.c.course_id == role_id).exists() if db.session.query(sql).scalar(): raise APIException( 'There are still users with this role', 'There are still users with role {}'.format(role_id), APICodes.INVALID_PARAM, 400) db.session.delete(role) db.session.commit() return make_empty_response()
def set_grader_to_not_done( assignment_id: int, grader_id: int ) -> EmptyResponse: """Indicate that the given grader is not yet done grading the given `:class:.models.Assignment`. .. :quickref: Assignment; Set the grader status to 'not done'. :param assignment_id: The id of the assignment the grader is not yet done grading. :param grader_id: The id of the `:class:.models.User` that is not yet done grading. :returns: An empty response with return code 204 :raises APIException: If the given grader was not indicated as done before calling this endpoint. (INVALID_STATE) :raises PermissionException: If the current user wants to change a status of somebody else but the user does not have the `can_update_grader_status` permission. (INCORRECT_PERMISSION) :raises PermissionException: If the current user wants to change its own status but does not have the `can_update_grader_status` or the `can_grade_work` permission. (INCORRECT_PERMISSION) """ assig = helpers.get_or_404(models.Assignment, assignment_id) if current_user.id == grader_id: auth.ensure_permission('can_grade_work', assig.course_id) else: auth.ensure_permission('can_update_grader_status', assig.course_id) try: send_mail = grader_id != current_user.id assig.set_graders_to_not_done([grader_id], send_mail=send_mail) db.session.commit() except ValueError: raise APIException( 'The grader is not finished!', f'The grader {grader_id} is not done.', APICodes.INVALID_STATE, 400, ) else: return make_empty_response()
def set_role_permission(role_id: int) -> EmptyResponse: """Update the :class:`.models.Permission` of a given :class:`.models.Role`. .. :quickref: Role; Update a permission for a certain role. :param int role_id: The id of the (non course) role. :returns: An empty response with return code 204. :<json str permission: The name of the permission to change. :<json bool value: The value to set the permission to (``True`` means the specified role has the specified permission). :raises PermissionException: If the current user does not have the ``can_manage_site_users`` permission. (INCORRECT_PERMISSION) """ content = ensure_json_dict(request.get_json()) ensure_keys_in_dict(content, [('permission', str), ('value', bool)]) perm_name = t.cast(str, content['permission']) value = t.cast(bool, content['value']) if (current_user.role_id == role_id and perm_name == 'can_manage_site_users'): raise APIException( 'You cannot remove this permission from your own role', ('The current user is in role {} which' ' cannot remove "can_manage_site_users"').format(role_id), APICodes.INCORRECT_PERMISSION, 403) perm = helpers.filter_single_or_404(models.Permission, models.Permission.name == perm_name, ~models.Permission.course_permission) role = helpers.get_or_404(models.Role, role_id) role.set_permission(perm, value) db.session.commit() return make_empty_response()
def select_rubric_items(submission_id: int, ) -> EmptyResponse: """Select the given rubric items for the given submission. .. :quickref: Submission; Select multiple rubric items. :param submission_id: The submission to unselect the item for. :>json array items: The ids of the rubric items you want to select. :returns: Nothing. :raises APIException: If the assignment of a given item does not belong to the assignment of the given submission. of the submission (INVALID_PARAM). :raises PermissionException: If the current user cannot grace work (INCORRECT_PERMISSION). """ submission = helpers.get_or_404(models.Work, submission_id) auth.ensure_permission('can_grade_work', submission.assignment.course_id) content = ensure_json_dict(request.get_json()) ensure_keys_in_dict(content, [('items', list)]) item_ids = t.cast(list, content['items']) items = [] for item_id in item_ids: items.append(helpers.get_or_404(models.RubricItem, item_id)) if any(item.rubricrow.assignment_id != submission.assignment_id for item in items): raise APIException( 'Selected rubric item is not coupled to the given submission', f'A given item of "{", ".join(str(i) for i in item_ids)}"' f' does not belong to assignment "{submission.assignment_id}"', APICodes.INVALID_PARAM, 400) submission.select_rubric_items(items, current_user, True) db.session.commit() return make_empty_response()
def delete_registration_link(course_id: int, link_id: uuid.UUID) -> EmptyResponse: """Delete the given registration link. .. :quickref: Course; The delete a registration link of the given course. :param course_id: The id of the course to which the registration link is connected. :param link_id: The id of the registration link. :returns: Nothing. """ course = helpers.get_or_404(models.Course, course_id, also_error=lambda c: c.virtual) auth.ensure_permission(CPerm.can_edit_course_users, course_id) link = helpers.get_or_404(models.CourseRegistrationLink, link_id, also_error=lambda l: l.course_id != course.id) db.session.delete(link) db.session.commit() return make_empty_response()
def patch_snippet(snippet_id: int) -> EmptyResponse: """Modify the :class:`.models.Snippet` with the given id. .. :quickref: Snippet; Change a snippets key and value. :param int snippet_id: The id of the snippet to change. :returns: An empty response with return code 204. :<json str key: The new key of the snippet. :<json str value: The new value of the snippet. :raises APIException: If the parameters "key" and/or "value" were not in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If the snippet does not belong to the current user. (INCORRECT_PERMISSION) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not use snippets. (INCORRECT_PERMISSION) """ content = ensure_json_dict(request.get_json()) ensure_keys_in_dict(content, [('key', str), ('value', str)]) key = t.cast(str, content['key']) value = t.cast(str, content['value']) snip = helpers.get_or_404(models.Snippet, snippet_id) if snip.user_id != current_user.id: raise APIException( 'The given snippet is not your snippet', 'The snippet "{}" does not belong to user "{}"'.format( snip.id, current_user.id), APICodes.INCORRECT_PERMISSION, 403) snip.key = key snip.value = value db.session.commit() return make_empty_response()
def update_submission_grader(submission_id: int) -> EmptyResponse: """Change the assigned grader of the given submission. .. :quickref: Submission; Update grader for the submission. :returns: Empty response and a 204 status. :>json int user_id: Id of the new grader. This is a required parameter. :raises PermissionException: If the logged in user cannot manage the course of the submission. (INCORRECT_PERMISSION) :raises APIException: If the new grader does not have the correct permission to grade this submission. (INCORRECT_PERMISSION) """ work = helpers.get_or_404(models.Work, submission_id) content = ensure_json_dict(request.get_json()) ensure_keys_in_dict(content, [('user_id', int)]) user_id = t.cast(int, content['user_id']) auth.ensure_permission('can_assign_graders', work.assignment.course_id) grader = helpers.get_or_404(models.User, user_id) if not grader.has_permission('can_grade_work', work.assignment.course_id): raise APIException( f'User "{grader.name}" doesn\'t have the required permission', f'User "{grader.name}" doesn\'t have permission "can_grade_work"', APICodes.INCORRECT_PERMISSION, 400) work.assignee = grader work.assignment.set_graders_to_not_done( [grader.id], send_mail=grader.id != current_user.id, ignore_errors=True, ) db.session.commit() return make_empty_response()
def delete_linter_output(linter_id: str) -> EmptyResponse: """Delete the all the output created by the :class:`.models.AssignmentLinter` with the given id. .. :quickref: Linter; Delete all linter input for a given linter. :param int linter_id: The id of the linter :returns: An empty response with return code 204 :raises APIException: If the linter with the given id does not exist. (OBJECT_ID_NOT_FOUND) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not use the linters in the course attached to the linter with the given id. (INCORRECT_PERMISSION) """ linter = helpers.get_or_404(models.AssignmentLinter, linter_id) auth.ensure_permission('can_use_linter', linter.assignment.course_id) db.session.delete(linter) db.session.commit() return make_empty_response()
def delete_code(file_id: int) -> EmptyResponse: """Delete the given file. .. :quickref: Code; Delete the given file. If a student does this request before the deadline, the file will be completely deleted. If the request is done after the deadline the user doing the delete will be removed from the ownership of the file and if there are no owners left the file is deleted. If the file owner of the given file is the same as that of the user doing the request (so the file will be completely deleted) the given file should not have any comments (Linter or normal) associated with it. If it still has comments the request will fail with error code 400. :returns: Nothing. :raises APIException: If the request will result in wrong state. (INVALID_STATE) :raises APIException: If there is not file with the given id. (OBJECT_ID_NOT_FOUND) :raises APIException: If you do not have permission to delete the given file. (INCORRECT_PERMISSION) """ code: models.File = helpers.get_or_404(models.File, file_id) auth.ensure_can_edit_work(code.work) def _raise_invalid() -> None: raise APIException( 'You cannot delete this file as you don\'t own it', f'File {file_id} is not owned by {current_user.id}', APICodes.INCORRECT_PERMISSION, 403 ) if code.work.user_id == current_user.id: current, other = models.FileOwner.student, models.FileOwner.teacher else: current, other = models.FileOwner.teacher, models.FileOwner.student if not all(child.fileowner == other for child in code.children.all()): raise APIException( 'You cannot delete this directory as it has children', f'The file "{file_id}" has children with fileowner "{current}"', APICodes.INVALID_STATE, 400 ) if code.fileowner == other: _raise_invalid() elif code.fileowner == current: if db.session.query( sql.or_( models.Comment.query.filter_by(file_id=code.id).exists(), models.LinterComment.query.filter_by(file_id=code.id).exists(), ) ).scalar(): raise APIException( 'You cannot delete this file as it has comments', f'The file "{file_id}" has comments associated with it.', APICodes.INVALID_STATE, 400, ) code.delete_from_disk() db.session.delete(code) elif code.fileowner == models.FileOwner.both: code.fileowner = other db.session.commit() return make_empty_response()
def set_grader_to_done(assignment_id: int, grader_id: int) -> EmptyResponse: """Indicate that the given grader is done grading the given `:class:.models.Assignment`. .. :quickref: Assignment; Set the grader status to 'done'. :param assignment_id: The id of the assignment the grader is done grading. :param grader_id: The id of the `:class:.models.User` that is done grading. :returns: An empty response with return code 204 :raises APIException: If the given grader was indicated as done before calling this endpoint. (INVALID_STATE) :raises PermissionException: If the current user wants to change a status of somebody else but the user does not have the `can_update_grader_status` permission. (INCORRECT_PERMISSION) :raises PermissionException: If the current user wants to change its own status but does not have the `can_update_grader_status` or the `can_grade_work` permission. (INCORRECT_PERMISSION) """ assig = helpers.get_or_404( models.Assignment, assignment_id, options=[joinedload(models.Assignment.finished_graders)], ) if current_user.id == grader_id: auth.ensure_permission('can_grade_work', assig.course_id) else: auth.ensure_permission('can_update_grader_status', assig.course_id) grader = helpers.get_or_404(models.User, grader_id) if not grader.has_permission('can_grade_work', assig.course_id): raise APIException( 'The given user is not a grader in this course', ( f'The user with id "{grader_id}" is not a grader ' f'in the course "{assig.course_id}"' ), APICodes.INVALID_PARAM, 400, ) if any(g.user_id == grader_id for g in assig.finished_graders): raise APIException( 'The grader is already finished!', f'The grader {grader_id} is already done.', APICodes.INVALID_STATE, 400, ) done_before = assig.graders_are_done() grader_done = models.AssignmentGraderDone( user_id=grader_id, assignment=assig, ) db.session.add(grader_done) db.session.commit() if not done_before and assig.graders_are_done(): psef.tasks.send_done_mail(assig.id) if assig.has_non_graded_submissions(grader_id): return make_empty_response( make_warning( 'You have non graded work!', APIWarnings.GRADER_NOT_DONE, ) ) return make_empty_response()
def divide_assignments(assignment_id: int) -> EmptyResponse: """Assign graders to all the latest :class:`.models.Work` objects of the given :class:`.models.Assignment`. .. :quickref: Assignment; Divide a submission among given TA's. The redivide tries to minimize shuffles. This means that calling it twice with the same data is effectively a noop. If the relative weight (so the percentage of work) of a user doesn't change it will not lose or gain any submissions. .. warning:: If a user was marked as done grading and gets assigned new submissions this user is marked as not done and gets a notification email! :<json dict graders: A mapping that maps user ids (strings) and the new weight they should get (numbers). :param int assignment_id: The id of the assignment :returns: An empty response with return code 204 :raises APIException: If no assignment with given id exists or the assignment has no submissions. (OBJECT_ID_NOT_FOUND) :raises APIException: If there was no grader in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If some grader id is invalid or some grader does not have the permission to grade the assignment. (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user is not allowed to divide submissions for this assignment. (INCORRECT_PERMISSION) """ assignment = helpers.get_or_404(models.Assignment, assignment_id) auth.ensure_permission('can_assign_graders', assignment.course_id) content = ensure_json_dict(request.get_json()) ensure_keys_in_dict(content, [('graders', dict)]) graders = {} for user_id, weight in t.cast(dict, content['graders']).items(): if not (isinstance(user_id, str) and isinstance(weight, (float, int))): raise APIException( 'Given graders weight or id is invalid', 'Both key and value in graders object should be integers', APICodes.INVALID_PARAM, 400 ) graders[int(user_id)] = weight if graders: users = helpers.filter_all_or_404( models.User, models.User.id.in_(graders.keys()) # type: ignore ) else: models.Work.query.filter_by(assignment_id=assignment.id).update( { 'assigned_to': None } ) assignment.assigned_graders = {} db.session.commit() return make_empty_response() if len(users) != len(graders): raise APIException( 'Invalid grader id given', 'Invalid grader (=user) id given', APICodes.INVALID_PARAM, 400 ) can_grade_work = helpers.filter_single_or_404( models.Permission, models.Permission.name == 'can_grade_work' ) for user in users: if not user.has_permission(can_grade_work, assignment.course_id): raise APIException( 'Selected grader has no permission to grade', f'The grader {user.id} has no permission to grade', APICodes.INVALID_PARAM, 400 ) assignment.divide_submissions([(user, graders[user.id]) for user in users]) db.session.commit() return make_empty_response()
def set_course_permission_user( course_id: int) -> t.Union[EmptyResponse, JSONResponse[_UserCourse]]: """Set the :class:`.models.CourseRole` of a :class:`.models.User` in the given :class:`.models.Course`. .. :quickref: Course; Change the course role for a user. :param int course_id: The id of the course :returns: If the user_id parameter is set in the request the response will be empty with return code 204. Otherwise the response will contain the JSON serialized user and course role with return code 201 :raises APIException: If the parameter role_id or not at least one of user_id and user_email are in the request. (MISSING_REQUIRED_PARAM) :raises APIException: If no role with the given role_id or no user with the supplied parameters exists. (OBJECT_ID_NOT_FOUND) :raises APIException: If the user was selected by email and the user is already in the course. (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not manage the course with the given id. (INCORRECT_PERMISSION) .. todo:: This function should probability be splitted. """ auth.ensure_permission(CPerm.can_edit_course_users, course_id) content = get_json_dict_from_request() ensure_keys_in_dict(content, [('role_id', int)]) role_id = t.cast(int, content['role_id']) role = helpers.filter_single_or_404( models.CourseRole, models.CourseRole.id == role_id, models.CourseRole.course_id == course_id) res: t.Union[EmptyResponse, JSONResponse[_UserCourse]] if 'user_id' in content: with get_from_map_transaction(content) as [get, _]: user_id = get('user_id', int) user = helpers.get_or_404(models.User, user_id) if user.id == current_user.id: raise APIException( 'You cannot change your own role', 'The user requested and the current user are the same', APICodes.INCORRECT_PERMISSION, 403) res = make_empty_response() elif 'username' in content: with get_from_map_transaction(content) as [get, _]: username = get('username', str) user = helpers.filter_single_or_404(models.User, models.User.username == username) if course_id in user.courses: raise APIException( 'The specified user is already in this course', 'The user {} is in course {}'.format(user.id, course_id), APICodes.INVALID_PARAM, 400) res = jsonify({ 'User': user, 'CourseRole': role, }, status_code=201) else: raise APIException( 'None of the keys "user_id" or "role_id" were found', ('The given content ({})' ' does not contain "user_id" or "user_email"').format(content), APICodes.MISSING_REQUIRED_PARAM, 400) if user.is_test_student: raise APIException('You cannot change the role of a test student', f'The user {user.id} is a test student', APICodes.INVALID_PARAM, 400) user.courses[role.course_id] = role db.session.commit() return res