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 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 get_all_course_assignments( course_id: int) -> JSONResponse[t.Sequence[models.Assignment]]: """Get all :class:`.models.Assignment` objects of the given :class:`.models.Course`. .. :quickref: Course; Get all assignments for single course. The returned assignments are sorted by deadline. :param int course_id: The id of the course :returns: A response containing the JSON serialized assignments sorted by deadline of the assignment. See :py:func:`.models.Assignment.__to_json__` for the way assignments are given. :raises APIException: If there is no course with the given id. (OBJECT_ID_NOT_FOUND) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user can not see assignments in the given course. (INCORRECT_PERMISSION) """ auth.ensure_permission(CPerm.can_see_assignments, course_id) course = helpers.get_or_404( models.Course, course_id, also_error=lambda c: c.virtual, ) return jsonify(course.get_all_visible_assignments())
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 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_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 get_assignment_rubric(assignment_id: int ) -> JSONResponse[t.Sequence[models.RubricRow]]: """Return the rubric corresponding to the given `assignment_id`. .. :quickref: Assignment; Get the rubric of an assignment. :param int assignment_id: The id of the assignment :returns: A list of JSON of :class:`.models.RubricRows` items :raises APIException: If no assignment with given id exists. (OBJECT_ID_NOT_FOUND) :raises APIException: If the assignment has no rubric. (OBJECT_ID_NOT_FOUND) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user is not allowed to see this is assignment. (INCORRECT_PERMISSION) """ assig = helpers.get_or_404(models.Assignment, assignment_id) auth.ensure_permission('can_see_assignments', 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 ) return jsonify(assig.rubric_rows)
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 create_new_assignment(course_id: int) -> JSONResponse[models.Assignment]: """Create a new course for the given assignment. .. :quickref: Course; Create a new assignment in a course. :param int course_id: The course to create an assignment in. :<json str name: The name of the new assignment. :returns: The newly created assignment. :raises PermissionException: If the current user does not have the ``can_create_assignment`` permission (INCORRECT_PERMISSION). """ auth.ensure_permission('can_create_assignment', course_id) content = ensure_json_dict(request.get_json()) ensure_keys_in_dict(content, [('name', str)]) name = t.cast(str, content['name']) course = helpers.get_or_404(models.Course, course_id) if course.lti_course_id is not None: raise APIException('You cannot add assignments to a LTI course', f'The course "{course_id}" is a LTI course', APICodes.INVALID_STATE, 400) assig = models.Assignment(name=name, course=course, deadline=datetime.datetime.utcnow()) db.session.add(assig) db.session.commit() return jsonify(assig)
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 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 create_or_edit_registration_link( course_id: int) -> JSONResponse[models.CourseRegistrationLink]: """Create or edit a registration link. .. :quickref: Course; Create or edit a registration link for a course. :param course_id: The id of the course in which this link should enroll users. :>json id: The id of the link to edit, omit to create a new link. :>json role_id: The id of the role that users should get when registering with this link. :>json expiration_date: The date this link should stop working, this date should be in ISO8061 format without any timezone information, as it will be interpret as a UTC date. :returns: The created or edited link. """ 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) with get_from_map_transaction( get_json_dict_from_request()) as [get, opt_get]: expiration_date = get('expiration_date', str) role_id = get('role_id', int) link_id = opt_get('id', str, default=None) if link_id is None: link = models.CourseRegistrationLink(course=course) db.session.add(link) else: link = helpers.filter_single_or_404( models.CourseRegistrationLink, models.CourseRegistrationLink.id == uuid.UUID(link_id), also_error=lambda l: l.course_id != course.id) link.course_role = helpers.get_or_404( models.CourseRole, role_id, also_error=lambda r: r.course_id != course.id) link.expiration_date = parsers.parse_datetime(expiration_date) if link.expiration_date < helpers.get_request_start_time(): helpers.add_warning('The link has already expired.', APIWarnings.ALREADY_EXPIRED) if link.course_role.has_permission(CPerm.can_edit_course_roles): helpers.add_warning( ('Users that register with this link will have the permission' ' to give themselves more permissions.'), APIWarnings.DANGEROUS_ROLE) db.session.commit() return jsonify(link)
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 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 get_all_works_for_assignment( assignment_id: int ) -> t.Union[JSONResponse[WorkList], ExtendedJSONResponse[WorkList]]: """Return all :class:`.models.Work` objects for the given :class:`.models.Assignment`. .. :quickref: Assignment; Get all works for an assignment. :qparam boolean extended: Whether to get extended or normal :class:`.models.Work` objects. The default value is ``false``, you can enable extended by passing ``true``, ``1`` or an empty string. :param int assignment_id: The id of the assignment :returns: A response containing the JSON serialized submissions. :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the assignment is hidden and the user is not allowed to view it. (INCORRECT_PERMISSION) """ assignment = helpers.get_or_404(models.Assignment, assignment_id) auth.ensure_permission('can_see_assignments', assignment.course_id) if assignment.is_hidden: auth.ensure_permission( 'can_see_hidden_assignments', assignment.course_id ) obj = models.Work.query.filter_by( assignment_id=assignment_id, ).options(joinedload( models.Work.selected_items, )).order_by(t.cast(t.Any, models.Work.created_at).desc()) if not current_user.has_permission( 'can_see_others_work', course_id=assignment.course_id ): obj = obj.filter_by(user_id=current_user.id) extended = request.args.get('extended', 'false').lower() if extended in {'true', '1', ''}: obj = obj.options(undefer(models.Work.comment)) return extended_jsonify( obj.all(), use_extended=lambda obj: isinstance(obj, models.Work), ) else: return jsonify(obj.all())
def get_registration_links( course_id: int ) -> JSONResponse[t.Sequence[models.CourseRegistrationLink]]: """Get the registration links for the given course. .. :quickref: Course; Get the registration links for this course. :param course_id: The course id for which to get the registration links. :returns: An array of registration links. """ 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) return jsonify(course.registration_links)
def patch_submission(submission_id: int) -> JSONResponse[models.Work]: """Update the given submission (:class:`.models.Work`) if it already exists. .. :quickref: Submission; Update a submissions grade and feedback. :param int submission_id: The id of the submission :returns: Empty response with return code 204 :>json float grade: The new grade, this can be null or float where null resets the grade or clears it. This field is optional :>json str feedback: The feedback for the student. This field is optional. :raise APIException: If the submission with the given id does not exist (OBJECT_ID_NOT_FOUND) :raise APIException: If the value of the "grade" parameter is not a float (INVALID_PARAM) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If user can not grade the submission with the given id (INCORRECT_PERMISSION) """ work = helpers.get_or_404(models.Work, submission_id) content = ensure_json_dict(request.get_json()) auth.ensure_permission('can_grade_work', work.assignment.course_id) if 'feedback' in content: ensure_keys_in_dict(content, [('feedback', str)]) feedback = t.cast(str, content['feedback']) work.comment = feedback if 'grade' in content: ensure_keys_in_dict(content, [('grade', (numbers.Real, type(None)))]) grade = t.cast(t.Optional[float], content['grade']) if not (grade is None or (0 <= float(grade) <= 10)): raise APIException( 'Grade submitted not between 0 and 10', f'Grade for work with id {submission_id} ' f'is {content["grade"]} which is not between 0 and 10', APICodes.INVALID_PARAM, 400) work.set_grade(grade, current_user) db.session.commit() return jsonify(work)
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 get_submission( submission_id: int ) -> ExtendedJSONResponse[t.Union[models.Work, t.Mapping[str, str]]]: """Get the given submission (:class:`.models.Work`). .. :quickref: Submission; Get a single submission. This API has some options based on the 'type' argument in the request - If ``type == 'zip'`` see :py:func:`.get_zip` - If ``type == 'feedback'`` see :py:func:`.submissions.get_feedback` :param int submission_id: The id of the submission :returns: A response with the JSON serialized submission as content unless specified otherwise :rtype: flask.Response :query str owner: The type of files to list, if set to `teacher` only teacher files will be listed, otherwise only student files will be listed. :raises APIException: If the submission with 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 submission does not belong to the current user and the user can not see others work in the attached course. (INCORRECT_PERMISSION) """ work = helpers.get_or_404(models.Work, submission_id) if work.user_id != current_user.id: auth.ensure_permission('can_see_others_work', work.assignment.course_id) if request.args.get('type') == 'zip': exclude_owner = models.File.get_exclude_owner( request.args.get('owner'), work.assignment.course_id, ) return extended_jsonify(get_zip(work, exclude_owner)) elif request.args.get('type') == 'feedback': auth.ensure_can_see_grade(work) return extended_jsonify(get_feedback(work)) return extended_jsonify(work)
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 get_all_graders( assignment_id: int ) -> JSONResponse[t.Sequence[t.Mapping[str, t.Union[float, str, bool]]]]: """Gets a list of all :class:`.models.User` objects who can grade the given :class:`.models.Assignment`. .. :quickref: Assignment; Get all graders for an assignment. :param int assignment_id: The id of the assignment :returns: A response containing the JSON serialized graders. :>jsonarr string name: The name of the grader. :>jsonarr int id: The user id of this grader. :>jsonarr bool divided: Is this user assigned to any submission for this assignment. :>jsonarr bool done: Is this user done grading? :raises APIException: If no assignment with given id exists. (OBJECT_ID_NOT_FOUND) :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) :raises PermissionException: If the user is not allowed to view graders of this assignment. (INCORRECT_PERMISSION) """ assignment = helpers.get_or_404(models.Assignment, assignment_id) auth.ensure_permission('can_see_assignee', assignment.course_id) result = assignment.get_all_graders(sort=True) divided: t.MutableMapping[int, float] = defaultdict(int) for assigned_grader in models.AssignmentAssignedGrader.query.filter_by( assignment_id=assignment_id ): divided[assigned_grader.user_id] = assigned_grader.weight return jsonify( [ { 'id': res[1], 'name': res[0], 'weight': float(divided[res[1]]), 'done': res[2], } for res in result ] )
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 get_all_course_roles( course_id: int ) -> JSONResponse[t.Union[t.Sequence[models.CourseRole], t.Sequence[t.MutableMapping[str, t.Union[t.Mapping[ str, bool], bool]]]]]: """Get a list of all :class:`.models.CourseRole` objects of a given :class:`.models.Course`. .. :quickref: Course; Get all course roles for a single course. :param int course_id: The id of the course to get the roles for. :returns: An array of all course roles for the given course. :>jsonarr perms: All permissions this role has as returned by :py:meth:`.models.CourseRole.get_all_permissions`. :>jsonarrtype perms: :py:class:`t.Mapping[str, bool]` :>jsonarr bool own: True if the current course role is the current users course role. :>jsonarr ``**rest``: The course role as returned by :py:meth:`.models.CourseRole.__to_json__` :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_roles: t.Sequence[models.CourseRole] course_roles = models.CourseRole.query.filter_by( course_id=course_id, hidden=False).order_by(models.CourseRole.name).all() if request.args.get('with_roles') == 'true': res = [] for course_role in course_roles: json_course = course_role.__to_json__() json_course['perms'] = CPerm.create_map( course_role.get_all_permissions()) json_course['own'] = current_user.courses[ course_role.course_id] == course_role res.append(json_course) return jsonify(res) return jsonify(course_roles)
def get_linter_state(linter_id: str) -> JSONResponse[models.AssignmentLinter]: """Get the state of the :class:`.models.AssignmentLinter` with the given id. .. :quickref: Linter; Get the state of a given linter. :param str linter_id: The id of the linter :returns: A response containing the JSON serialized linter :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) return jsonify(linter)
def get_rubric(submission_id: int) -> JSONResponse[t.Mapping[str, t.Any]]: """Return full rubric of the :class:`.models.Assignment` of the given submission (:class:`.models.Work`). .. :quickref: Submission; Get a rubric and its selected items. :param int submission_id: The id of the submission :returns: A response containing the JSON serialized rubric as described in :py:meth:`.Work.__rubric_to_json__`. :raises APIException: If the submission 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 see the assignment of the given submission. (INCORRECT_PERMISSION) """ work = helpers.get_or_404(models.Work, submission_id) auth.ensure_permission('can_see_assignments', work.assignment.course_id) return jsonify(work.__rubric_to_json__())
def get_assignments_feedback( assignment_id: int ) -> JSONResponse[t.Mapping[str, t.Mapping[str, t.Union[t.Sequence[str], str]]] ]: """Get all feedbacks for all latest submissions for a given assignment. .. :quickref: Assignment; Get feedback for all submissions in a assignment. :param int assignment_id: The assignment to query for. :returns: A mapping between the id of the submission and a object contain three keys: ``general`` for general feedback as a string, ``user`` for user feedback as a list of strings and ``linter`` for linter feedback as a list of strings. If a user cannot see others work only submissions by the current users are returned. """ assignment = helpers.get_or_404(models.Assignment, assignment_id) auth.ensure_enrolled(assignment.course_id) latest_subs = assignment.get_all_latest_submissions() try: auth.ensure_permission('can_see_others_work', assignment.course_id) except auth.PermissionException: latest_subs = latest_subs.filter_by(user_id=current_user.id) res = {} for sub in latest_subs: try: # This call should be cached in auth.py auth.ensure_can_see_grade(sub) user_feedback, linter_feedback = sub.get_all_feedback() item = { 'general': sub.comment or '', 'user': list(user_feedback), 'linter': list(linter_feedback), } except auth.PermissionException: item = {'user': [], 'linter': [], 'general': ''} res[str(sub.id)] = item return jsonify(res)
def get_all_course_users( course_id: int ) -> JSONResponse[t.Union[t.List[_UserCourse], t.List[models.User]]]: """Return a list of all :class:`.models.User` objects and their :class:`.models.CourseRole` in the given :class:`.models.Course`. .. :quickref: Course; Get all users for a single course. :param int course_id: The id of the course :query string q: Search for users matching this query string. This will change the output to a list of users. :returns: A response containing the JSON serialized users and course roles :>jsonarr User: A member of the given course. :>jsonarrtype User: :py:class:`~.models.User` :>jsonarr CourseRole: The role that this user has. :>jsonarrtype CourseRole: :py:class:`~.models.CourseRole` """ auth.ensure_permission(CPerm.can_list_course_users, course_id) course = helpers.get_or_404(models.Course, course_id) if 'q' in request.args: @limiter.limit('1 per second', key_func=lambda: str(current_user.id)) def get_users_in_course() -> t.List[models.User]: query: str = request.args.get('q', '') base = course.get_all_users_in_course( include_test_students=False).from_self(models.User) return helpers.filter_users_by_name(query, base).all() return jsonify(get_users_in_course()) users = course.get_all_users_in_course(include_test_students=False) user_course: t.List[_UserCourse] user_course = [{ 'User': user, 'CourseRole': crole } for user, crole in users] return jsonify(sorted(user_course, key=lambda item: item['User'].name))
def create_course_snippet( course_id: int) -> JSONResponse[models.CourseSnippet]: """Add or modify a :class:`.models.CourseSnippet` by key. .. :quickref: CourseSnippet; Add or modify a course snippet. :returns: A response containing the JSON serialized snippet and return code 201. :<json str value: The new value of the snippet. :<json str key: The key of the new or existing snippet. :raises APIException: If the parameters "key", "value", and/or "course_id" were not in the request. (MISSING_REQUIRED_PARAM) :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) content = get_json_dict_from_request() ensure_keys_in_dict(content, [('value', str), ('key', str)]) key = t.cast(str, content['key']) value = t.cast(str, content['value']) course = helpers.get_or_404(models.Course, course_id) snippet = models.CourseSnippet.query.filter_by( course=course, key=key, ).first() if snippet is None: snippet = models.CourseSnippet( course=course, key=key, value=value, ) db.session.add(snippet) else: snippet.value = value db.session.commit() return jsonify(snippet, status_code=201)
def create_new_assignment(course_id: int) -> JSONResponse[models.Assignment]: """Create a new course for the given assignment. .. :quickref: Course; Create a new assignment in a course. :param int course_id: The course to create an assignment in. :<json str name: The name of the new assignment. :returns: The newly created assignment. :raises PermissionException: If the current user does not have the ``can_create_assignment`` permission (INCORRECT_PERMISSION). """ auth.ensure_permission(CPerm.can_create_assignment, 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 course.lti_provider is not None: lms = course.lti_provider.lms_name raise APIException(f'You cannot add assignments to a {lms} course', f'The course "{course_id}" is a LTI course', APICodes.INVALID_STATE, 400) assig = models.Assignment( name=name, course=course, is_lti=False, ) db.session.add(assig) db.session.commit() return jsonify(assig)