def update_notification(notification_id: int ) -> ExtendedJSONResponse[Notification]: """Update the read status for the given notification. .. :quickref: Notification; Update a single notification. :>json boolean read: Should the notification be considered read. :param notification_id: The id of the notification to update. :returns: The updated notification. """ with helpers.get_from_request_transaction() as [get, _]: read = get('read', bool) notification = helpers.get_or_404( Notification, notification_id, also_error=lambda n: ( n.deleted or not auth.NotificationPermissions( n, ).ensure_may_see.as_bool() ) ) auth.NotificationPermissions(notification).ensure_may_edit() notification.read = read db.session.commit() return ExtendedJSONResponse.make( notification, use_extended=(models.CommentReply, Notification) )
def add_comment() -> ExtendedJSONResponse[CommentBase]: """Create a new comment base, or retrieve an existing one. .. :quickref: Comment; Create a new comment base. :>json int file_id: The id of the file in which this comment should be placed. :>json int line: The line on which this comment should be placed. :returns: The just created comment base. """ with helpers.get_from_request_transaction() as [get, _]: file_id = get('file_id', int) line = get('line', int) file = helpers.filter_single_or_404( models.File, models.File.id == file_id, also_error=lambda f: f.deleted, with_for_update=True, ) base = CommentBase.create_if_not_exists(file=file, line=line) FeedbackBasePermissions(base).ensure_may_add() if base.id is None: db.session.add(base) # type: ignore[unreachable] db.session.commit() return ExtendedJSONResponse.make(base, use_extended=(CommentBase, CommentReply))
def update_reply(comment_base_id: int, reply_id: int) -> ExtendedJSONResponse[CommentReply]: """Update the content of reply. .. :quickref: Comment; Update the content of an inline feedback reply. :>json string comment: The new content of the reply. :param comment_base_id: The base of the given reply. :param reply_id: The id of the reply for which you want to update. :returns: The just updated reply. """ with helpers.get_from_request_transaction() as [get, _]: message = get('comment', str, transform=lambda x: x.replace('\0', '')) reply = helpers.filter_single_or_404( CommentReply, CommentReply.id == reply_id, CommentReply.comment_base_id == comment_base_id, ~CommentReply.deleted, with_for_update=True, with_for_update_of=CommentReply, ) FeedbackReplyPermissions(reply).ensure_may_edit() edit = reply.update(message) if edit is not None: db.session.add(edit) db.session.commit() return ExtendedJSONResponse.make(reply, use_extended=CommentReply)
def self_information() -> t.Union[JSONResponse[models.User], JSONResponse[t.Dict[int, str]], ExtendedJSONResponse[models.User], ]: """Get the info of the currently logged in user. .. :quickref: User; Get information about the currently logged in user. :query type: If this is ``roles`` a mapping between course_id and role name will be returned, if this is ``extended`` the result of :py:meth:`.models.User.__extended_to_json__()` will be returned. If this is something else or not present the result of :py:meth:`.models.User.__to_json__()` will be returned. :query with_permissions: Setting this to true will add the key ``permissions`` to the user. The value will be a mapping indicating which global permissions this user has. :returns: A response containing the JSON serialized user :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN) """ args = request.args if args.get('type') == 'roles': return JSONResponse.make( { role.course_id: role.name for role in current_user.courses.values() } ) elif helpers.extended_requested() or args.get('type') == 'extended': user = models.User.resolve(current_user) if request_arg_true('with_permissions'): jsonify_options.get_options().add_permissions_to_user = user return ExtendedJSONResponse.make(user, use_extended=models.User) return JSONResponse.make(current_user)
def add_reply(comment_base_id: int) -> ExtendedJSONResponse[CommentReply]: """Add a reply to a comment base. .. :quickref: Comment; Add a reply to a comment base. :>json string comment: The content of the new reply. :>json string reply_type: The type of formatting used for the contents of the new reply. Should be a member of :class:`.models.CommentReplyType`. :>json t.Optional[int] in_reply_to: The id of the reply this new reply should be considered a reply to. (OPTIONAL). :param comment_base_id: The id of the base to which you want to add a reply. :returns: The just created reply. """ with helpers.get_from_request_transaction() as [get, opt_get]: message = get('comment', str, transform=lambda x: x.replace('\0', '')) in_reply_to_id = opt_get('in_reply_to', (int, type(None)), None) reply_type = get('reply_type', models.CommentReplyType) base = helpers.get_or_404( CommentBase, comment_base_id, also_error=lambda b: b.file.deleted, ) in_reply_to = None if in_reply_to_id is not None: in_reply_to = helpers.get_or_404( CommentReply, in_reply_to_id, also_error=lambda r: r.comment_base != base or r.deleted) reply = base.add_reply(current_user, message, reply_type, in_reply_to) FeedbackReplyPermissions(reply).ensure_may_add() db.session.flush() warning_authors = set() for author in set(r.author for r in base.user_visible_replies if r.can_see_author): with as_current_user(author): if not FeedbackReplyPermissions(reply).ensure_may_see.as_bool(): warning_authors.add(author) if warning_authors: multiple = len(warning_authors) > 1 helpers.add_warning( ('The author{s} {authors} {do_not} have sufficient permissions' ' to see this reply, the reply will probably only be visible' ' when the assignment state is set to "done".').format( s='s' if multiple else '', do_not="don't" if multiple else "doesn't", authors=helpers.readable_join( [u.get_readable_name() for u in sorted(warning_authors)]), ), APIWarnings.POSSIBLE_INVISIBLE_REPLY) db.session.commit() return ExtendedJSONResponse.make(reply, use_extended=(CommentBase, CommentReply))
def get_all_notifications() -> t.Union[ExtendedJSONResponse[NotificationsJSON], JSONResponse[HasUnreadNotifcationJSON], ]: """Get all notifications for the current user. .. :quickref: Notification; Get all notifications. :query boolean has_unread: If considered true a short digest will be send, i.e. a single object with one key ``has_unread`` with a boolean value. Please use this if you simply want to check if there are unread notifications. :returns: Either a :class:`.NotificationsJSON` or a `HasUnreadNotifcationJSON` based on the ``has_unread`` parameter. """ notifications = db.session.query(Notification).join( Notification.comment_reply ).filter( ~models.CommentReply.deleted, Notification.receiver == current_user, ).order_by( Notification.read.asc(), Notification.created_at.desc(), ).options( contains_eager(Notification.comment_reply), defaultload(Notification.comment_reply).defer( models.CommentReply.last_edit ), defaultload( Notification.comment_reply, ).defaultload( models.CommentReply.comment_base, ).defaultload( models.CommentBase.file, ).selectinload( models.File.work, ), ).yield_per(_MAX_NOTIFICATION_AMOUNT) def can_see(noti: Notification) -> bool: return auth.NotificationPermissions(noti).ensure_may_see.as_bool() if request_arg_true('has_unread'): has_unread = any( map(can_see, notifications.filter(~Notification.read)) ) return JSONResponse.make({'has_unread': has_unread}) return ExtendedJSONResponse.make( NotificationsJSON( notifications=[ n for n in itertools.islice(notifications, _MAX_NOTIFICATION_AMOUNT) if can_see(n) ] ), use_extended=(models.CommentReply, Notification) )
def user_patch_handle_change_user_data() -> ExtendedJSONResponse[models.User]: """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 ExtendedJSONResponse.make(current_user, use_extended=models.User)
def update_notifications() -> ExtendedJSONResponse[NotificationsJSON]: """Update the read status for the given notifications. .. :quickref: Notification; Update multiple notifications in bulk. :>jsonarr int id: The id of the notification to update. :>jsonarr boolean read: Should the notification be considered read. :returns: The updated notifications in the same order as given in the body. """ with helpers.get_from_request_transaction() as [get, _]: notifications: t.List[helpers.JSONType] = get('notifications', list) notifications_to_update = {} for noti_json in notifications: with helpers.get_from_map_transaction( helpers.ensure_json_dict(noti_json) ) as [get, _]: notification_id = get('id', int) read = get('read', bool) notifications_to_update[notification_id] = read found_notifications = helpers.get_in_or_error( Notification, Notification.id, list(notifications_to_update.keys()), also_error=lambda n: n.deleted, as_map=True, ) result = [] for n_id, read in notifications_to_update.items(): found_notification = found_notifications[n_id] auth.NotificationPermissions(found_notification).ensure_may_edit() found_notification.read = read result.append(found_notification) db.session.commit() return ExtendedJSONResponse.make( {'notifications': result}, use_extended=(models.CommentReply, Notification) )
def get_reply_edits( comment_base_id: int, reply_id: int) -> ExtendedJSONResponse[t.List[CommentReplyEdit]]: """Get the edits of a reply. .. :quickref: Comment; Get the edits of an inline feedback reply. :param comment_base_id: The base of the given reply. :param reply_id: The id of the reply for which you want to get the replies. :returns: A list of edits, sorted from newest to oldest. """ reply = helpers.filter_single_or_404( CommentReply, CommentReply.id == reply_id, CommentReply.comment_base_id == comment_base_id, ~CommentReply.deleted, ) FeedbackReplyPermissions(reply).ensure_may_see_edits() return ExtendedJSONResponse.make(reply.edits.all(), use_extended=CommentReplyEdit)
def update_reply_approval(comment_base_id: int, reply_id: int) -> ExtendedJSONResponse[CommentReply]: """Update the content of reply. .. :quickref: Comment; Update the content of an inline feedback reply. :>json string comment: The new content of the reply. :param comment_base_id: The base of the given reply. :param reply_id: The id of the reply for which you want to update. :returns: The just updated reply. """ reply = helpers.filter_single_or_404( CommentReply, CommentReply.id == reply_id, CommentReply.comment_base_id == comment_base_id, ~CommentReply.deleted, with_for_update=True, with_for_update_of=CommentReply, ) FeedbackReplyPermissions(reply).ensure_may_change_approval() reply.is_approved = flask.request.method == 'POST' db.session.commit() return ExtendedJSONResponse.make(reply, use_extended=CommentReply)