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
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
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
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}
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}
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}
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
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
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}
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
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()
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
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)
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)
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
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)
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}
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, }
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)
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."
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."
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
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
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