Exemple #1
0
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)
    )
Exemple #2
0
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))
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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))
Exemple #6
0
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)
    )
Exemple #7
0
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)
Exemple #8
0
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)
    )
Exemple #9
0
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)
Exemple #10
0
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)