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 ensure_can_submit_work( assig: 'psef.models.Assignment', author: 'psef.models.User', ) -> None: """Check if the current user can submit for the given assignment as the given author. .. note:: This function also checks if the assignment is a LTI assignment. If this is the case it makes sure the ``author`` can do grade passback. :param assig: The assignment that should be submitted to. :param author: The author of the submission. :raises PermissionException: If there the current user cannot submit for the given author. :raises APIException: If the author is not enrolled in course of the given assignment or if the LTI state was wrong. """ submit_self = psef.current_user.id == author.id if assig.course_id not in author.courses: raise APIException( 'The given user is not enrolled in this course', ( f'The user "{author.id}" is not enrolled ' f'in course "{assig.course_id}"' ), APICodes.INVALID_STATE, 400, ) if submit_self: ensure_permission('can_submit_own_work', assig.course_id) else: ensure_permission('can_submit_others_work', assig.course_id) if not assig.is_open: ensure_permission('can_upload_after_deadline', assig.course_id) if assig.is_lti and assig.id not in author.assignment_results: raise APIException( ( "This assignment is a LTI assignment and it seems we " "don't have the possibility to passback the grade to the " "LMS. Please {}visit the assignment on the LMS again, if " "this issue persist please contact your administrator." ).format('let the given author ' if submit_self else ''), ( f'The assignment {assig.id} is not present in the ' f'user {author.id} `assignment_results`' ), APICodes.INVALID_STATE, 400, )
def login() -> ExtendedJSONResponse[t.Mapping[str, t.Union[models.User, str]]]: """Login a :class:`.models.User` if the request is valid. .. :quickref: User; Login a given user. :returns: A response containing the JSON serialized user :<json str email: The email of the user to log in. :<json str username: The password of the user to log in. :>json user: The user that was logged in. :>jsonobj user: :py:class:`~.models.User` :>json str access_token: A JWT token that can be used to send requests to the server logged in as the given user. :raises APIException: If the request does not contain email and/or password parameter. (MISSING_REQUIRED_PARAM) :raises APIException: If no user with email exists or the password is wrong. (LOGIN_FAILURE) :raises APIException: If the user with the given email and password is inactive. (INACTIVE_USER) """ data = ensure_json_dict(request.get_json()) ensure_keys_in_dict(data, [('username', str), ('password', str)]) username = t.cast(str, data['username']) password = t.cast(str, data['password']) # WARNING: Do not use the `helpers.filter_single_or_404` function here as # we have to return the same error for a wrong email as for a wrong # password! user: t.Optional[models.User] user = db.session.query(models.User, ).filter( models.User.username == username, ).first() if user is None or user.password != password: raise APIException('The supplied email or password is wrong.', ('The user with username "{}" does not exist ' + 'or has a different password').format(username), APICodes.LOGIN_FAILURE, 400) if not user.is_active: raise APIException( 'User is not active', ('The user with id "{}" is not active any more').format(user.id), APICodes.INACTIVE_USER, 403) return extended_jsonify({ 'user': user, 'access_token': flask_jwt.create_access_token( identity=user.id, fresh=True, ) })
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()) 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.name != name: auth.ensure_permission('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 if current_user.email != email: auth.ensure_permission('can_edit_own_info') if not validate_email(email): raise APIException( 'The given email is not valid.', 'The email "{email}" is not valid.', APICodes.INVALID_PARAM, 400, ) _ensure_password('email') current_user.email = email if new_password != '': _ensure_password('password') auth.ensure_permission('can_edit_own_password') current_user.password = new_password db.session.commit() return make_empty_response()
def extract_to_temp( file: FileStorage, ignore_filter: IgnoreFilterManager, handle_ignore: IgnoreHandling = IgnoreHandling.keep) -> str: """Extracts the contents of file into a temporary directory. :param file: The archive to extract. :param ignore_filter: The files and directories that should be ignored. :param handle_ignore: Determines how ignored files should be handled. :returns: The pathname of the new temporary directory. """ tmpfd, tmparchive = tempfile.mkstemp() try: os.remove(tmparchive) tmparchive += os.path.basename( secure_filename('archive_' + file.filename)) tmpdir = tempfile.mkdtemp() file.save(tmparchive) if handle_ignore == IgnoreHandling.error: arch = archive.Archive(tmparchive) wrong_files = ignore_filter.get_ignored_files_in_archive(arch) if wrong_files: raise IgnoredFilesException(invalid_files=wrong_files) arch.extract(to_path=tmpdir, method='safe') else: archive.extract(tmparchive, to_path=tmpdir, method='safe') if handle_ignore == IgnoreHandling.delete: ignore_filter.delete_from_dir(tmpdir) except (tarfile.ReadError, zipfile.BadZipFile): raise APIException( 'The given archive could not be extracted', "The given archive doesn't seem to be an archive", APICodes.INVALID_ARCHIVE, 400, ) except (InvalidFile, archive.UnsafeArchive) as e: raise APIException( 'The given archive contains invalid files', str(e), APICodes.INVALID_FILE_IN_ARCHIVE, 400, ) finally: os.close(tmpfd) os.remove(tmparchive) return tmpdir
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 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 parse_datetime( # pylint: disable=function-redefined to_parse: object, allow_none: bool = False, ) -> t.Optional[DatetimeWithTimezone]: """Parse a datetime string using dateutil. :param to_parse: The object to parse, if this is not a string the parsing will always fail. :param allow_none: Allow ``None`` to be passed without raising a exception. if ``to_parse`` is ``None`` and this option is ``True`` the result will be ``None``. :returns: The parsed DatetimeWithTimezone object. :raises APIException: If the parsing fails for whatever reason. """ if to_parse is None and allow_none: return None if isinstance(to_parse, str): try: parsed = dateutil.parser.parse(to_parse) except (ValueError, OverflowError): pass else: # This assumes that datetimes without tzinfo are in UTC. That is # not correct according to the ISO spec, however it is what we used # to do so we need to do this because of backwards compatibility. return DatetimeWithTimezone.from_datetime(parsed, default_tz=timezone.utc) raise APIException('The given date is not valid!', '{} cannot be parsed by dateutil.'.format(to_parse), APICodes.INVALID_PARAM, 400)
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 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 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 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 ensure_can_edit_work(work: 'psef.models.Work') -> None: """Make sure the current user can edit files in the given work. :param work: The work the given user should be able to see edit files in. :returns: Nothing. :raises PermissionException: If the user should not be able te edit these files. """ if work.user_id == psef.current_user.id: if work.assignment.is_open: ensure_permission('can_submit_own_work', work.assignment.course_id) else: ensure_permission( 'can_upload_after_deadline', work.assignment.course_id ) else: if work.assignment.is_open: raise APIException( ( 'You cannot edit work as teacher' ' if the assignment is stil open!' ), f'The assignment "{work.assignment.id}" is still open.', APICodes.INCORRECT_PERMISSION, 403, ) ensure_permission('can_edit_others_work', work.assignment.course_id)
def send_reset_password_email(user: models.User) -> None: """Send the reset password email to a user. :param user: The user that has requested a reset password email. :returns: Nothing """ token = user.get_reset_token() html_body = current_app.config['EMAIL_TEMPLATE'].replace( '\n\n', '<br><br>').format( site_url=current_app.config["EXTERNAL_URL"], url=(f'{current_app.config["EXTERNAL_URL"]}/reset_' f'password/?user={user.id}&token={token}'), user_id=user.id, token=token, user_name=html.escape(user.name), user_email=html.escape(user.email), ) try: _send_mail(html_body, f'Reset password on {psef.app.config["EXTERNAL_URL"]}', [user.email]) except Exception as exc: logger.bind(exc_info=True) raise APIException( 'Something went wrong sending the email, ' 'please contact your site admin', f'Sending email to {user.id} went wrong.', APICodes.UNKOWN_ERROR, 500, ) from exc
def raise_error() -> None: raise APIException( "All files are ignored by a rule in the assignment's ignore file", 'No files were in the given archive after filtering.', APICodes.NO_FILES_SUBMITTED, 400, )
def user_patch_handle_reset_password() -> JSONResponse[t.Mapping[str, str]]: """Handle the ``reset_password`` type for the PATCH login route. :returns: A response with a jsonified mapping between ``access_token`` and a token which can be used to login. This is only key available. """ data = ensure_json_dict(request.get_json()) ensure_keys_in_dict(data, [('new_password', str), ('token', str), ('user_id', int)]) password = t.cast(str, data['new_password']) user_id = t.cast(int, data['user_id']) token = t.cast(str, data['token']) if password == '': raise APIException('Password should at least be 1 char', f'The password is {len(password)} chars long', APICodes.INVALID_PARAM, 400) user = helpers.get_or_404(models.User, user_id) user.reset_password(token, password) db.session.commit() return jsonify({ 'access_token': flask_jwt.create_access_token( identity=user.id, fresh=True, ) })
def get_course_data(course_id: int) -> JSONResponse[t.Mapping[str, t.Any]]: """Return course data for a given :class:`.models.Course`. .. :quickref: Course; Get data for a given course. :param int course_id: The id of the course :returns: A response containing the JSON serialized course :>json str role: The name of the role the current user has in this course. :>json ``**rest``: JSON serialization of :py:class:`psef.models.Course`. :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) """ # TODO: Optimize this loop to a single query for course_role in current_user.courses.values(): if course_role.course_id == course_id: return jsonify({ 'role': course_role.name, **course_role.course.__to_json__(), }) raise APIException('Course not found', 'The course with id {} was not found'.format(course_id), APICodes.OBJECT_ID_NOT_FOUND, 404)
def search_users() -> JSONResponse[t.Sequence[models.User]]: """Search for a user by name and username. .. :quickref: User; Fuzzy search for a user by name and username. :param str q: The string to search for, all SQL wildcard are escaped and spaces are replaced by wildcards. :returns: A list of :py:class:`.models.User` objects that match the given query string. :raises APIException: If the query string less than 3 characters long. (INVALID_PARAM) :raises PermissionException: If the currently logged in user does not have the permission ``can_search_users``. (INCORRECT_PERMISSION) :raises RateLimitExceeded: If you hit this end point more than once per second. (RATE_LIMIT_EXCEEDED) """ ensure_keys_in_dict(request.args, [('q', str)]) query = t.cast(str, request.args.get('q')) if len(query) < 3: raise APIException( 'The search string should be at least 3 chars', f'The search string "{query}" is not 3 chars or longer.', APICodes.INVALID_PARAM, 400) likes = [ t.cast(t.Any, col).ilike('%{}%'.format( escape_like(query).replace(' ', '%'), )) for col in [models.User.name, models.User.username] ] return jsonify(models.User.query.filter(or_(*likes)).all())
def parse_email_list( to_parse: object, allow_none: bool = False, ) -> t.Optional[t.List[t.Tuple[str, str]]]: """Parse email list into a list of emails. This list should be in the form of a ``address-list`` as specified in RFC2822. :param to_parse: The object to parse, it should be a :class:`str` if you want it to succeed. :param allow_none: If ``True`` we will not error if ``to_parse`` is ``None``. :returns: A list of addresses or ``None`` if ``to_parse`` is ``None`` and ``allow_none`` is ``True``. :raises APIException: If the parsing fails in some way. """ if allow_none and to_parse is None: return None if isinstance(to_parse, str): addresses = email.utils.getaddresses([to_parse.strip()]) if all(validate_email(email) for _, email in addresses): return addresses raise APIException( f'The given string of emails contains invalid items', f'The string "{to_parse}" contains invalid items.', APICodes.INVALID_PARAM, 400 )
def parse_enum( to_parse: object, parse_into_enum: t.Type[T], allow_none: bool = False, option_name: t.Optional[str] = None, ) -> t.Optional[T]: """Parse the given string to the given parse_into_enum. :param to_parse: The object to parse. If this value is not a string or ``None`` the function will always return a type error. :param parse_into_enum: The enum to parse to. :param allow_none: Allow ``None`` to be passed and return ``None`` if this is the case. If this value is ``False`` and ``None`` is passed the function will raise a :class:`.APIException`. :param option_name: The name of the option, only used in error display. :returns: A instance of the given enum. :raises APIException: If the parsing fails in some way. """ if allow_none and to_parse is None: return None if isinstance(to_parse, str): try: return parse_into_enum[to_parse] except KeyError: pass raise APIException( f'The given {option_name or "option"} is not a valid option', f'{to_parse} is not a member from {parse_into_enum.__name__}.', APICodes.INVALID_PARAM, 400 )
def parse_datetime( to_parse: object, allow_none: bool = False, ) -> t.Optional[datetime.datetime]: """Parse a datetime string using dateutil. :param to_parse: The object to parse, if this is not a string the parsing will always fail. :param allow_none: Allow ``None`` to be passed without raising a exception. if ``to_parse`` is ``None`` and this option is ``True`` the result will be ``None``. :returns: The parsed datetime object. :raises APIException: If the parsing fails for whatever reason. """ if to_parse is None and allow_none: return None if isinstance(to_parse, str): try: return dateutil.parser.parse(to_parse) except (ValueError, OverflowError): pass raise APIException( 'The given date is not valid!', '{} cannot be parsed by dateutil.'.format(to_parse), APICodes.INVALID_PARAM, 400 )
def get_submission_files_from_request( check_size: bool, ) -> t.MutableSequence[FileStorage]: """Get all the submitted files in the current request. This function also checks if the files are in the correct format and are lot too large. :returns: The files in the current request. The length of this list is always at least one. :raises APIException: When a given files is not correct. """ res = [] if ( check_size and request.content_length and request.content_length > app.config['MAX_UPLOAD_SIZE'] ): helpers.raise_file_too_big_exception() if not request.files: raise APIException( "No file in HTTP request.", "There was no file in the HTTP request.", APICodes.MISSING_REQUIRED_PARAM, 400 ) for key, file in request.files.items(): if not key.startswith('file'): raise APIException( 'The parameter name should start with "file".', 'Expected ^file.*$ got {}.'.format(key), APICodes.INVALID_PARAM, 400 ) # This will not be used on werkzeug >=0.14.0 if not file.filename: # pragma: no cover raise APIException( 'The filename should not be empty.', 'Got an empty filename for key {}'.format(key), APICodes.INVALID_PARAM, 400 ) res.append(file) return res
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)
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 get_course_or_global_permissions( ) -> JSONResponse[t.Union[GlobalPermMap, t.Mapping[int, CoursePermMap]]]: """Get all the global :class:`.psef.models.Permission` or the value of a permission in all courses of the currently logged in :class:`.psef.models.User` .. :quickref: Permission; Get global permissions or all the course permissions for the current user. :qparam str type: The type of permissions to get. This can be ``global`` or ``course``. :qparam str permission: The permissions to get when getting course permissions. You can pass this parameter multiple times to get multiple permissions. DEPRECATED: This option is deprecated, as it is preferred that you simply get all permissions for a course. :returns: The returning object depends on the given ``type``. If it was ``global`` a mapping between permissions name and a boolean indicating if the currently logged in user has this permissions is returned. If it was ``course`` such a mapping is returned for every course the user is enrolled in. So it is a mapping between course ids and permission mapping. The permissions given as ``permission`` query parameter are the only ones that are present in the permission map. When no ``permission`` query is given all course permissions are returned. """ ensure_keys_in_dict(request.args, [('type', str)]) permission_type = t.cast(str, request.args['type']).lower() if permission_type == 'global': return jsonify(GPerm.create_map(current_user.get_all_permissions())) elif permission_type == 'course' and 'permission' in request.args: add_deprecate_warning( 'Requesting a subset of course permissions is deprecated') # Make sure at least one permission is present ensure_keys_in_dict(request.args, [('permission', str)]) perm_names = t.cast(t.List[str], request.args.getlist('permission')) return jsonify({ course_id: CPerm.create_map(v) for course_id, v in current_user.get_permissions_in_courses( [CPerm.get_by_name(p) for p in perm_names]).items() }) elif permission_type == 'course': return jsonify({ course_id: CPerm.create_map(v) for course_id, v in current_user.get_all_permissions_in_courses().items() }) else: raise APIException( 'Invalid permission type given', f'The given type "{permission_type}" is not "global" or "course"', APICodes.INVALID_PARAM, 400, )
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 get_file_contents(code: models.File) -> bytes: """Get the contents of the given :class:`.models.File`. :param code: The file object to read. :returns: The contents of the file with newlines. """ if code.is_directory: raise APIException( 'Cannot display this file as it is a directory.', f'The selected file with id {code.id} is a directory.', APICodes.OBJECT_WRONG_TYPE, 400) filename = code.get_diskname() if os.path.islink(filename): raise APIException( f'This file is a symlink to `{os.readlink(filename)}`.', 'The file {} is a symlink'.format(code.id), APICodes.INVALID_STATE, 410) with open(filename, 'rb') as codefile: return codefile.read()
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 set_reminder( assig: models.Assignment, content: t.Dict[str, helpers.JSONType], ) -> t.Optional[psef.errors.HttpWarning]: """Set the reminder of an assignment from a JSON dict. :param assig: The assignment to set the reminder for. :param content: The json input. :returns: A warning if it should be returned to the user. """ ensure_keys_in_dict(content, [ ('done_type', (type(None), str)), ('done_email', (type(None), str)), ('reminder_time', (type(None), str)), ]) # yapf: disable done_type = parsers.parse_enum( content.get('done_type', None), models.AssignmentDoneType, allow_none=True, option_name='done type' ) reminder_time = parsers.parse_datetime( content.get('reminder_time', None), allow_none=True, ) done_email = parsers.try_parse_email_list( content.get('done_email', None), allow_none=True, ) if reminder_time and (reminder_time - datetime.datetime.utcnow()).total_seconds() < 60: raise APIException( ( 'The given date is not far enough from the current time, ' 'it should be at least 60 seconds in the future.' ), f'{reminder_time} is not atleast 60 seconds in the future', APICodes.INVALID_PARAM, 400 ) assig.change_notifications(done_type, reminder_time, done_email) if done_email is not None and assig.graders_are_done(): return make_warning( 'Grading is already done, no email will be sent!', APIWarnings.CONDITION_ALREADY_MET ) return None
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)