Ejemplo n.º 1
0
def test_mention_filtering_parent_comment(db, topic, user_list):
    """Test notification filtering for parent comments."""
    parent_comment = Comment(topic, user_list[0], "Comment content.")
    comment = Comment(topic, user_list[1], f"@{user_list[0].username}",
                      parent_comment)
    mentions = CommentNotification.get_mentions_for_comment(db, comment)
    assert not mentions
Ejemplo n.º 2
0
def test_mention_filtering_parent_comment(mocker, db, topic, user_list):
    """Test notification filtering for parent comments."""
    parent_comment = Comment(topic, user_list[0], "Comment content.")
    parent_comment.user_id = user_list[0].user_id
    comment = mocker.Mock(
        user_id=user_list[1].user_id,
        markdown=f"@{user_list[0].username}",
        parent_comment=parent_comment,
    )
    mentions = CommentNotification.get_mentions_for_comment(db, comment)
    assert not mentions
Ejemplo n.º 3
0
def test_prevent_duplicate_notifications(db, user_list, topic):
    """Test that notifications are cleaned up for edits.

    Flow:
        1. A comment is created by user A that mentions user B. Notifications are
           generated, and yield A mentioning B.
        2. The comment is edited to mention C and not B.
        3. The comment is edited to mention B and C.
        4. The comment is deleted.
    """
    # 1
    comment = Comment(topic, user_list[0], f"@{user_list[1].username}")
    db.add(comment)
    db.commit()
    mentions = CommentNotification.get_mentions_for_comment(db, comment)
    assert len(mentions) == 1
    assert mentions[0].user == user_list[1]
    db.add_all(mentions)
    db.commit()

    # 2
    comment.markdown = f"@{user_list[2].username}"
    db.commit()
    mentions = CommentNotification.get_mentions_for_comment(db, comment)
    assert len(mentions) == 1
    to_delete, to_add = CommentNotification.prevent_duplicate_notifications(
        db, comment, mentions)
    assert len(to_delete) == 1
    assert mentions == to_add
    assert to_delete[0].user.username == user_list[1].username

    # 3
    comment.markdown = f"@{user_list[1].username} @{user_list[2].username}"
    db.commit()
    mentions = CommentNotification.get_mentions_for_comment(db, comment)
    assert len(mentions) == 2
    to_delete, to_add = CommentNotification.prevent_duplicate_notifications(
        db, comment, mentions)
    assert not to_delete
    assert len(to_add) == 1

    # 4
    comment.is_deleted = True
    db.commit()
    notifications = (db.query(CommentNotification.user_id).filter(
        and_(
            CommentNotification.comment_id == comment.comment_id,
            CommentNotification.notification_type ==
            CommentNotificationType.USER_MENTION,
        )).all())
    assert not notifications
Ejemplo n.º 4
0
def post_comment_reply(request: Request, markdown: str) -> dict:
    """Post a reply to a comment with Intercooler."""
    parent_comment = request.context
    new_comment = Comment(
        topic=parent_comment.topic,
        author=request.user,
        markdown=markdown,
        parent_comment=parent_comment,
    )
    request.db_session.add(new_comment)

    if parent_comment.user != request.user:
        notification = CommentNotification(
            parent_comment.user,
            new_comment,
            CommentNotificationType.COMMENT_REPLY,
        )
        request.db_session.add(notification)

    # commit and then re-query the new comment to get complete data
    request.tm.commit()

    new_comment = (request.query(Comment).join_all_relationships().filter_by(
        comment_id=new_comment.comment_id).one())

    return {'comment': new_comment}
Ejemplo n.º 5
0
def post_toplevel_comment(request: Request, markdown: str) -> dict:
    """Post a new top-level comment on a topic with Intercooler."""
    topic = request.context

    new_comment = Comment(
        topic=topic,
        author=request.user,
        markdown=markdown,
    )
    request.db_session.add(new_comment)

    if topic.user != request.user and not topic.is_deleted:
        notification = CommentNotification(
            topic.user,
            new_comment,
            CommentNotificationType.TOPIC_REPLY,
        )
        request.db_session.add(notification)

    # commit and then re-query the new comment to get complete data
    request.tm.commit()

    new_comment = (request.query(Comment).join_all_relationships().filter_by(
        comment_id=new_comment.comment_id).one())

    return {'comment': new_comment, 'topic': topic}
Ejemplo n.º 6
0
def post_toplevel_comment(request: Request, markdown: str) -> dict:
    """Post a new top-level comment on a topic with Intercooler."""
    topic = request.context

    new_comment = Comment(topic=topic, author=request.user, markdown=markdown)
    request.db_session.add(new_comment)

    request.db_session.add(LogComment(LogEventType.COMMENT_POST, request, new_comment))

    if CommentNotification.should_create_reply_notification(new_comment):
        notification = CommentNotification(
            topic.user, new_comment, CommentNotificationType.TOPIC_REPLY
        )
        request.db_session.add(notification)

    # commit and then re-query the new comment to get complete data
    request.tm.commit()

    new_comment = (
        request.query(Comment)
        .join_all_relationships()
        .filter_by(comment_id=new_comment.comment_id)
        .one()
    )

    return {"comment": new_comment, "topic": topic}
Ejemplo n.º 7
0
def test_comment_creation_validates_schema(mocker, session_user, topic):
    """Ensure that comment creation goes through schema validation."""
    mocker.spy(CommentSchema, "load")

    Comment(topic, session_user, "A test comment")
    call_args = CommentSchema.load.call_args[0]
    assert {"markdown": "A test comment"} in call_args
Ejemplo n.º 8
0
def test_mention_filtering_top_level(db, user_list, session_group):
    """Test notification filtering for top-level comments."""
    topic = Topic.create_text_topic(session_group, user_list[0], "Some title",
                                    "some text")
    comment = Comment(topic, user_list[1], f"@{user_list[0].username}")
    mentions = CommentNotification.get_mentions_for_comment(db, comment)
    assert not mentions
Ejemplo n.º 9
0
def post_comment_reply(request: Request, markdown: str) -> dict:
    """Post a reply to a comment with Intercooler."""
    parent_comment = request.context
    new_comment = Comment(
        topic=parent_comment.topic,
        author=request.user,
        markdown=markdown,
        parent_comment=parent_comment,
    )
    request.db_session.add(new_comment)

    request.db_session.add(
        LogComment(LogEventType.COMMENT_POST, request, new_comment))

    if CommentNotification.should_create_reply_notification(new_comment):
        notification = CommentNotification(
            parent_comment.user, new_comment,
            CommentNotificationType.COMMENT_REPLY)
        request.db_session.add(notification)

    _mark_comment_read_from_interaction(request, parent_comment)

    # commit and then re-query the new comment to get complete data
    request.tm.commit()

    new_comment = (request.query(Comment).join_all_relationships().filter_by(
        comment_id=new_comment.comment_id).one())

    return {"comment": new_comment}
Ejemplo n.º 10
0
def test_comment_creation_validates_schema(mocker, session_user, topic):
    """Ensure that comment creation goes through schema validation."""
    mocker.spy(CommentSchema, 'load')

    Comment(topic, session_user, 'A test comment')
    call_args = CommentSchema.load.call_args[0]
    assert {'markdown': 'A test comment'} in call_args
Ejemplo n.º 11
0
def comment(db, session_user, topic):
    """Create a comment in the database, delete it as teardown."""
    new_comment = Comment(topic, session_user, 'A comment')
    db.add(new_comment)
    db.commit()

    yield new_comment

    db.delete(new_comment)
    db.commit()
Ejemplo n.º 12
0
def test_remove_delete_single_decrement(db, topic, session_user):
    """Ensure that remove+delete doesn't double-decrement num_comments."""
    # add 2 comments
    comment1 = Comment(topic, session_user, "Comment 1")
    comment2 = Comment(topic, session_user, "Comment 2")
    db.add_all([comment1, comment2])
    db.commit()
    db.refresh(topic)
    assert topic.num_comments == 2

    # remove one and check the decrement
    comment1.is_removed = True
    db.add(comment1)
    db.commit()
    db.refresh(topic)
    assert topic.num_comments == 1

    # delete the same comment and check it didn't decrement again
    comment1.is_deleted = True
    db.add(comment1)
    db.commit()
    db.refresh(topic)
    assert topic.num_comments == 1
Ejemplo n.º 13
0
def post_comment_on_topic(request: Request, markdown: str) -> HTTPFound:
    """Post a new top-level comment on a topic."""
    topic = request.context

    new_comment = Comment(topic=topic, author=request.user, markdown=markdown)
    request.db_session.add(new_comment)

    request.db_session.add(
        LogComment(LogEventType.COMMENT_POST, request, new_comment))

    if CommentNotification.should_create_reply_notification(new_comment):
        notification = CommentNotification(topic.user, new_comment,
                                           CommentNotificationType.TOPIC_REPLY)
        request.db_session.add(notification)

    raise HTTPFound(location=topic.permalink)
Ejemplo n.º 14
0
def post_group_topics(
    request: Request,
    title: str,
    markdown: str,
    link: str,
    tags: str,
) -> HTTPFound:
    """Post a new topic to a group."""
    if link:
        new_topic = Topic.create_link_topic(
            group=request.context,
            author=request.user,
            title=title,
            link=link,
        )

        # if they specified both a link and markdown, use the markdown to post
        # an initial comment on the topic
        if markdown:
            new_comment = Comment(
                topic=new_topic,
                author=request.user,
                markdown=markdown,
            )
            request.db_session.add(new_comment)
    else:
        new_topic = Topic.create_text_topic(
            group=request.context,
            author=request.user,
            title=title,
            markdown=markdown,
        )

    try:
        new_topic.tags = tags.split(',')
    except ValidationError:
        raise ValidationError({'tags': ['Invalid tags']})

    request.db_session.add(new_topic)

    request.db_session.add(
        LogTopic(LogEventType.TOPIC_POST, request, new_topic))

    # flush the changes to the database so the new topic's ID is generated
    request.db_session.flush()

    raise HTTPFound(location=new_topic.permalink)
Ejemplo n.º 15
0
def test_comments_affect_topic_num_comments(session_user, topic, db):
    """Ensure adding/deleting comments affects the topic's comment count."""
    assert topic.num_comments == 0

    # Insert some comments, ensure each one increments the count
    comments = []
    for num in range(0, 5):
        new_comment = Comment(topic, session_user, "comment")
        comments.append(new_comment)
        db.add(new_comment)
        db.commit()
        db.refresh(topic)
        assert topic.num_comments == len(comments)

    # Delete all the comments, ensure each one decrements the count
    for num, comment in enumerate(comments, start=1):
        comment.is_deleted = True
        db.commit()
        db.refresh(topic)
        assert topic.num_comments == len(comments) - num
Ejemplo n.º 16
0
def post_comment_on_topic(request: Request, markdown: str) -> HTTPFound:
    """Post a new top-level comment on a topic."""
    topic = request.context

    new_comment = Comment(
        topic=topic,
        author=request.user,
        markdown=markdown,
    )
    request.db_session.add(new_comment)

    if topic.user != request.user and not topic.is_deleted:
        notification = CommentNotification(
            topic.user,
            new_comment,
            CommentNotificationType.TOPIC_REPLY,
        )
        request.db_session.add(notification)

    raise HTTPFound(location=topic.permalink)
Ejemplo n.º 17
0
def post_comment_reply(request: Request, markdown: str) -> dict:
    """Post a reply to a comment with Intercooler."""
    parent_comment = request.context

    wait_mins = _reply_wait_minutes(request, request.user, parent_comment.user)
    if wait_mins:
        incr_counter("comment_back_and_forth_warnings")
        raise HTTPUnprocessableEntity(
            f"You can't reply to this user yet. Please wait {wait_mins} minutes."
        )

    new_comment = Comment(
        topic=parent_comment.topic,
        author=request.user,
        markdown=markdown,
        parent_comment=parent_comment,
    )
    request.db_session.add(new_comment)

    request.db_session.add(
        LogComment(LogEventType.COMMENT_POST, request, new_comment))

    if CommentNotification.should_create_reply_notification(new_comment):
        notification = CommentNotification(
            parent_comment.user, new_comment,
            CommentNotificationType.COMMENT_REPLY)
        request.db_session.add(notification)

    _mark_comment_read_from_interaction(request, parent_comment)

    # commit and then re-query the new comment to get complete data
    request.tm.commit()

    new_comment = (request.query(Comment).join_all_relationships().filter_by(
        comment_id=new_comment.comment_id).one())

    return {"comment": new_comment}
Ejemplo n.º 18
0
def get_settings_theme_previews(request: Request) -> dict:
    """Generate the theme preview page."""
    # get the generic/unknown user and a random group to display on the example posts
    fake_user = request.query(User).filter(User.user_id == -1).one()
    group = request.query(Group).order_by(func.random()).limit(1).one()

    fake_link_topic = Topic.create_link_topic(group, fake_user,
                                              "Example Link Topic",
                                              "https://tildes.net/")

    fake_text_topic = Topic.create_text_topic(group, fake_user,
                                              "Example Text Topic",
                                              "No real text")
    fake_text_topic.content_metadata = {
        "excerpt": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    }

    fake_topics = [fake_link_topic, fake_text_topic]

    # manually add other necessary attributes to the fake topics
    for fake_topic in fake_topics:
        fake_topic.topic_id = sys.maxsize
        fake_topic.tags = ["tag one", "tag two"]
        fake_topic.num_comments = 123
        fake_topic.num_votes = 12
        fake_topic.created_time = utc_now() - timedelta(hours=12)

    # create a fake top-level comment that appears to be written by the user
    markdown = (
        "This is what a regular comment written by yourself would look like.\n\n"
        "It has **formatting** and a [link](https://tildes.net).")
    fake_top_comment = Comment(fake_link_topic, request.user, markdown)
    fake_top_comment.comment_id = sys.maxsize
    fake_top_comment.created_time = utc_now() - timedelta(hours=12, minutes=30)

    child_comments_markdown = [
        ("This reply has received an Exemplary label. It also has a blockquote:\n\n"
         "> Hello World!"),
        ("This is a reply written by the topic's OP with a code block in it:\n\n"
         "```js\n"
         "function foo() {\n"
         "    ['1', '2', '3'].map(parseInt);\n"
         "}\n"
         "```"),
        ("This reply is new and has the *Mark New Comments* stripe on its left "
         "(even if you don't have that feature enabled)."),
    ]

    fake_comments = [fake_top_comment]

    # vary the ID and created_time on each fake comment so CommentTree works properly
    current_comment_id = fake_top_comment.comment_id
    current_created_time = fake_top_comment.created_time
    for markdown in child_comments_markdown:
        current_comment_id -= 1
        current_created_time += timedelta(minutes=5)

        fake_comment = Comment(fake_link_topic,
                               fake_user,
                               markdown,
                               parent_comment=fake_top_comment)
        fake_comment.comment_id = current_comment_id
        fake_comment.created_time = current_created_time
        fake_comment.parent_comment_id = fake_top_comment.comment_id

        fake_comments.append(fake_comment)

    # add other necessary attributes to all of the fake comments
    for fake_comment in fake_comments:
        fake_comment.num_votes = 0

    fake_tree = CommentTree(fake_comments, CommentTreeSortOption.NEWEST,
                            request.user)

    # add a fake Exemplary label to the first child comment
    fake_comments[1].labels = [
        CommentLabel(fake_comments[1], fake_user, CommentLabelOption.EXEMPLARY,
                     1.0)
    ]

    # the comment to mark as new is the last one, so set a visit time just before it
    fake_last_visit_time = fake_comments[-1].created_time - timedelta(
        minutes=1)

    return {
        "theme_options": THEME_OPTIONS,
        "fake_topics": fake_topics,
        "fake_comment_tree": fake_tree,
        "last_visit": fake_last_visit_time,
    }
Ejemplo n.º 19
0
def post_group_topics(
    request: Request,
    title: str,
    markdown: str,
    link: str,
    tags: str,
    confirm_repost: bool,
) -> Union[HTTPFound, Response]:
    """Post a new topic to a group."""
    group = request.context

    if link:
        # check to see if this link has been posted before
        previous_topics = (request.query(Topic).filter(
            Topic.link == link).order_by(desc(
                Topic.created_time)).limit(5).all())

        if previous_topics and not confirm_repost:
            # Render partial form for Intercooler.js request, whole page for normal POST
            # (I don't like this much, there must be a better way to handle this)
            if "X-IC-Request" in request.headers:
                template = "tildes:templates/includes/new_topic_form.jinja2"
            else:
                template = "tildes:templates/new_topic.jinja2"

            return render_to_response(
                template,
                {
                    "group": group,
                    "title": title,
                    "link": link,
                    "markdown": markdown,
                    "tags": tags,
                    "previous_topics": previous_topics,
                },
                request=request,
            )

        new_topic = Topic.create_link_topic(group=group,
                                            author=request.user,
                                            title=title,
                                            link=link)

        # if they specified both a link and markdown, use the markdown to post an
        # initial comment on the topic
        if markdown:
            new_comment = Comment(topic=new_topic,
                                  author=request.user,
                                  markdown=markdown)
            request.db_session.add(new_comment)

            request.db_session.add(
                LogComment(LogEventType.COMMENT_POST, request, new_comment))
    else:
        new_topic = Topic.create_text_topic(group=group,
                                            author=request.user,
                                            title=title,
                                            markdown=markdown)

    try:
        new_topic.tags = tags.split(",")
    except ValidationError:
        raise ValidationError({"tags": ["Invalid tags"]})

    # remove any tag that's the same as the group's name
    new_topic.tags = [tag for tag in new_topic.tags if tag != str(group.path)]

    request.apply_rate_limit("topic_post")

    request.db_session.add(new_topic)

    request.db_session.add(
        LogTopic(LogEventType.TOPIC_POST, request, new_topic))

    # if the user added tags to the topic, show the field by default in the future
    if tags and not request.user.show_tags_on_new_topic:
        request.user.show_tags_on_new_topic = True
        request.db_session.add(request.user)

    # flush the changes to the database so the new topic's ID is generated
    request.db_session.flush()

    raise HTTPFound(location=new_topic.permalink)
Ejemplo n.º 20
0
def test_comment_excerpt_excludes_blockquote(topic, session_user):
    """Ensure that comment excerpts don't include text from blockquotes."""
    markdown = "> Something you said\n\nYeah, I agree."
    comment = Comment(topic, session_user, markdown)

    assert comment.excerpt == "Yeah, I agree."
Ejemplo n.º 21
0
def test_comment_excerpt_excludes_del(topic, session_user):
    """Ensure that comment excerpts don't include text from strikethrough (<del>)."""
    markdown = "I really ~~hate~~ love it."
    comment = Comment(topic, session_user, markdown)

    assert comment.excerpt == "I really love it."
Ejemplo n.º 22
0
def test_comment_tree(db, topic, session_user):
    """Ensure that building and pruning a comment tree works."""
    all_comments = []

    sort = CommentTreeSortOption.POSTED

    # add two root comments
    root = Comment(topic, session_user, "root")
    root2 = Comment(topic, session_user, "root2")
    all_comments.extend([root, root2])
    db.add_all(all_comments)
    db.commit()

    # check that both show up in the tree as top-level comments
    tree = CommentTree(all_comments, sort)
    assert list(tree) == [root, root2]

    # delete the second root comment and check that the tree now excludes it
    root2.is_deleted = True
    db.commit()
    tree = list(CommentTree(all_comments, sort))
    assert tree == [root]

    # add two replies to the remaining root comment
    child = Comment(topic, session_user, "1", parent_comment=root)
    child2 = Comment(topic, session_user, "2", parent_comment=root)
    all_comments.extend([child, child2])
    db.add_all(all_comments)
    db.commit()

    # check that the tree is built as expected so far (one root, two replies)
    tree = list(CommentTree(all_comments, sort))
    assert tree == [root]
    assert root.replies == [child, child2]
    assert child.replies == []
    assert child2.replies == []

    # add two more replies to the second depth-1 comment
    subchild = Comment(topic, session_user, "2a", parent_comment=child2)
    subchild2 = Comment(topic, session_user, "2b", parent_comment=child2)
    all_comments.extend([subchild, subchild2])
    db.add_all(all_comments)
    db.commit()

    # check the tree again
    tree = list(CommentTree(all_comments, sort))
    assert tree == [root]
    assert root.replies == [child, child2]
    assert child.replies == []
    assert child2.replies == [subchild, subchild2]

    # check depth values are as expected
    assert root.depth == 0
    assert child.depth == 1
    assert subchild.depth == 2

    # delete child2 (which has replies) and ensure it stays in the tree
    child2.is_deleted = True
    db.commit()
    tree = list(CommentTree(all_comments, sort))
    assert root.replies == [child, child2]

    # delete child2's children and ensure that whole branch is pruned
    subchild.is_deleted = True
    subchild2.is_deleted = True
    db.commit()
    tree = list(CommentTree(all_comments, sort))
    assert root.replies == [child]

    # delete root and remaining child and ensure tree is empty
    child.is_deleted = True
    root.is_deleted = True
    db.commit()
    tree = list(CommentTree(all_comments, sort))
    assert not tree
Ejemplo n.º 23
0
def test_mention_filtering_self_mention(db, user_list, topic):
    """Test notification filtering for self-mentions."""
    comment = Comment(topic, user_list[0], f"@{user_list[0]}")
    mentions = CommentNotification.get_mentions_for_comment(db, comment)
    assert not mentions
Ejemplo n.º 24
0
def test_comment_creation_uses_markdown_field(mocker, session_user, topic):
    """Ensure the Markdown field class is validating new comments."""
    mocker.spy(Markdown, "_validate")

    Comment(topic, session_user, "A test comment")
    assert Markdown._validate.called