Commit bea463d9 by jsa

Add acceptance tests for forums comment deletion.

JIRA: FOR-472
parent 7be356b3
......@@ -57,6 +57,8 @@ from collections import namedtuple
from courseware.courses import get_courses, sort_by_announcement
from courseware.access import has_access
from django_comment_common.models import Role
from external_auth.models import ExternalAuthMap
import external_auth.views
......@@ -1211,6 +1213,7 @@ def auto_auth(request):
* `full_name` for the user profile (the user's full name; defaults to the username)
* `staff`: Set to "true" to make the user global staff.
* `course_id`: Enroll the student in the course with `course_id`
* `roles`: Comma-separated list of roles to grant the student in the course with `course_id`
If username, email, or password are not provided, use
randomly generated credentials.
......@@ -1226,6 +1229,7 @@ def auto_auth(request):
full_name = request.GET.get('full_name', username)
is_staff = request.GET.get('staff', None)
course_id = request.GET.get('course_id', None)
role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()]
# Get or create the user object
post_data = {
......@@ -1268,6 +1272,11 @@ def auto_auth(request):
if course_id is not None:
CourseEnrollment.enroll(user, course_id)
# Apply the roles
for role_name in role_names:
role = Role.objects.get(name=role_name, course_id=course_id)
user.roles.add(role)
# Log in as the user
user = authenticate(username=username, password=password)
login(request, user)
......
......@@ -2,7 +2,6 @@
Stub implementation of cs_comments_service for acceptance tests
"""
from datetime import datetime
import re
import urlparse
from .http import StubHttpRequestHandler, StubHttpService
......@@ -14,6 +13,7 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
"/api/v1/users/(?P<user_id>\\d+)$": self.do_user,
"/api/v1/threads$": self.do_threads,
"/api/v1/threads/(?P<thread_id>\\w+)$": self.do_thread,
"/api/v1/comments/(?P<comment_id>\\w+)$": self.do_comment,
}
path = urlparse.urlparse(self.path).path
for pattern in pattern_handlers:
......@@ -25,8 +25,13 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
self.send_response(404, content="404 Not Found")
def do_PUT(self):
if self.path.startswith('/set_config'):
return StubHttpRequestHandler.do_PUT(self)
self.send_response(204, "")
def do_DELETE(self):
self.send_json_response({})
def do_user(self, user_id):
self.send_json_response({
"id": user_id,
......@@ -36,44 +41,28 @@ class StubCommentsServiceHandler(StubHttpRequestHandler):
})
def do_thread(self, thread_id):
match = re.search("(?P<num>\\d+)_responses", thread_id)
resp_total = int(match.group("num")) if match else 0
thread = {
"id": thread_id,
"commentable_id": "dummy",
"type": "thread",
"title": "Thread title",
"body": "Thread body",
"created_at": datetime.utcnow().isoformat(),
"unread_comments_count": 0,
"comments_count": resp_total,
"votes": {"up_count": 0},
"abuse_flaggers": [],
"closed": "closed" in thread_id,
}
params = urlparse.parse_qs(urlparse.urlparse(self.path).query)
if "recursive" in params and params["recursive"][0] == "True":
thread["resp_total"] = resp_total
thread["children"] = []
resp_skip = int(params.get("resp_skip", ["0"])[0])
resp_limit = int(params.get("resp_limit", ["10000"])[0])
num_responses = min(resp_limit, resp_total - resp_skip)
self.log_message("Generating {} children; resp_limit={} resp_total={} resp_skip={}".format(num_responses, resp_limit, resp_total, resp_skip))
for i in range(num_responses):
response_id = str(resp_skip + i)
thread["children"].append({
"id": str(response_id),
"type": "comment",
"body": response_id,
"created_at": datetime.utcnow().isoformat(),
"votes": {"up_count": 0},
"abuse_flaggers": [],
})
self.send_json_response(thread)
if thread_id in self.server.config.get('threads', {}):
thread = self.server.config['threads'][thread_id].copy()
params = urlparse.parse_qs(urlparse.urlparse(self.path).query)
if "recursive" in params and params["recursive"][0] == "True":
thread.setdefault('children', [])
resp_total = thread.setdefault('resp_total', len(thread['children']))
resp_skip = int(params.get("resp_skip", ["0"])[0])
resp_limit = int(params.get("resp_limit", ["10000"])[0])
thread['children'] = thread['children'][resp_skip:(resp_skip + resp_limit)]
self.send_json_response(thread)
else:
self.send_response(404, content="404 Not Found")
def do_threads(self):
self.send_json_response({"collection": [], "page": 1, "num_pages": 1})
def do_comment(self, comment_id):
# django_comment_client calls GET comment before doing a DELETE, so that's what this is here to support.
if comment_id in self.server.config.get('comments', {}):
comment = self.server.config['comments'][comment_id]
self.send_json_response(comment)
class StubCommentsService(StubHttpService):
HANDLER_CLASS = StubCommentsServiceHandler
......@@ -8,3 +8,6 @@ XQUEUE_STUB_URL = os.environ.get('xqueue_url', 'http://localhost:8040')
# Get the URL of the Ora stub used in the test
ORA_STUB_URL = os.environ.get('ora_url', 'http://localhost:8041')
# Get the URL of the comments service stub used in the test
COMMENTS_STUB_URL = os.environ.get('comments_url', 'http://localhost:4567')
"""
Tools for creating discussion content fixture data.
"""
from datetime import datetime
import json
import factory
import requests
from . import COMMENTS_STUB_URL
class ContentFactory(factory.Factory):
FACTORY_FOR = dict
id = None
user_id = "dummy-user-id"
username = "dummy-username"
course_id = "dummy-course-id"
commentable_id = "dummy-commentable-id"
anonymous = False
anonymous_to_peers = False
at_position_list = []
abuse_flaggers = []
created_at = datetime.utcnow().isoformat()
updated_at = datetime.utcnow().isoformat()
endorsed = False
closed = False
votes = {"up_count": 0}
class Thread(ContentFactory):
comments_count = 0
unread_comments_count = 0
title = "dummy thread title"
body = "dummy thread body"
type = "thread"
group_id = None
pinned = False
read = False
class Comment(ContentFactory):
thread_id = None
depth = 0
type = "comment"
body = "dummy comment body"
class Response(Comment):
depth = 1
body = "dummy response body"
class SingleThreadViewFixture(object):
def __init__(self, thread):
self.thread = thread
def addResponse(self, response, comments=[]):
response['children'] = comments
self.thread.setdefault('children', []).append(response)
self.thread['comments_count'] += len(comments) + 1
def _get_comment_map(self):
"""
Generate a dict mapping each response/comment in the thread
by its `id`.
"""
def _visit(obj):
res = []
for child in obj.get('children', []):
res.append((child['id'], child))
if 'children' in child:
res += _visit(child)
return res
return dict(_visit(self.thread))
def push(self):
"""
Push the data to the stub comments service.
"""
requests.put(
'{}/set_config'.format(COMMENTS_STUB_URL),
data={
"threads": json.dumps({self.thread['id']: self.thread}),
"comments": json.dumps(self._get_comment_map())
}
)
......@@ -53,10 +53,7 @@ class DiscussionSingleThreadPage(CoursePage):
def has_add_response_button(self):
"""Returns true if the add response button is visible, false otherwise"""
return (
self.is_css_present(".add-response-btn") and
self.css_map(".add-response-btn", lambda el: el.visible)[0]
)
return self._is_element_visible(".add-response-btn")
def click_add_response_button(self):
"""
......@@ -68,3 +65,25 @@ class DiscussionSingleThreadPage(CoursePage):
lambda: self.is_css_present("#wmd-input-reply-body-{thread_id}:focus".format(thread_id=self.thread_id)),
"Response field received focus"
))
def _is_element_visible(self, selector):
return (
self.is_css_present(selector) and
self.css_map(selector, lambda el: el.visible)[0]
)
def is_comment_visible(self, comment_id):
"""Returns true if the comment is viewable onscreen"""
return self._is_element_visible("#comment_{}".format(comment_id))
def is_comment_deletable(self, comment_id):
"""Returns true if the delete comment button is present, false otherwise"""
return self._is_element_visible("#comment_{} div.action-delete".format(comment_id))
def delete_comment(self, comment_id):
with self.handle_alert():
self.css_click("#comment_{} div.action-delete".format(comment_id))
fulfill(EmptyPromise(
lambda: not self.is_comment_visible(comment_id),
"Deleted comment was removed"
))
......@@ -6,6 +6,7 @@ from .helpers import UniqueCourseTest
from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.lms.discussion_single_thread import DiscussionSingleThreadPage
from ..fixtures.course import CourseFixture
from ..fixtures.discussion import SingleThreadViewFixture, Thread, Response, Comment
class DiscussionSingleThreadTest(UniqueCourseTest):
......@@ -19,9 +20,16 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
# Create a course to register for
CourseFixture(**self.course_info).install()
AutoAuthPage(self.browser, course_id=self.course_id).visit()
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id).visit().get_user_id()
def setup_thread(self, thread, num_responses):
view = SingleThreadViewFixture(thread=thread)
for i in range(num_responses):
view.addResponse(Response(id=str(i), body=str(i)))
view.push()
def test_no_responses(self):
self.setup_thread(Thread(id="0_responses"), 0)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "0_responses")
page.visit()
self.assertEqual(page.get_response_total_text(), "0 responses")
......@@ -31,6 +39,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
self.assertIsNone(page.get_load_responses_button_text())
def test_few_responses(self):
self.setup_thread(Thread(id="5_responses"), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
page.visit()
self.assertEqual(page.get_response_total_text(), "5 responses")
......@@ -39,6 +48,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
self.assertIsNone(page.get_load_responses_button_text())
def test_two_response_pages(self):
self.setup_thread(Thread(id="50_responses"), 50)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "50_responses")
page.visit()
self.assertEqual(page.get_response_total_text(), "50 responses")
......@@ -52,6 +62,7 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
self.assertEqual(page.get_load_responses_button_text(), None)
def test_three_response_pages(self):
self.setup_thread(Thread(id="150_responses"), 150)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "150_responses")
page.visit()
self.assertEqual(page.get_response_total_text(), "150 responses")
......@@ -70,12 +81,57 @@ class DiscussionSingleThreadTest(UniqueCourseTest):
self.assertEqual(page.get_load_responses_button_text(), None)
def test_add_response_button(self):
self.setup_thread(Thread(id="5_responses"), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses")
page.visit()
self.assertTrue(page.has_add_response_button())
page.click_add_response_button()
def test_add_response_button_closed_thread(self):
self.setup_thread(Thread(id="5_responses_closed", closed=True), 5)
page = DiscussionSingleThreadPage(self.browser, self.course_id, "5_responses_closed")
page.visit()
self.assertFalse(page.has_add_response_button())
class DiscussionCommentDeletionTest(UniqueCourseTest):
"""
Tests for deleting comments displayed beneath responses in the single thread view.
"""
def setUp(self):
super(DiscussionCommentDeletionTest, self).setUp()
# Create a course to register for
CourseFixture(**self.course_info).install()
def setup_user(self, roles=[]):
roles_str = ','.join(roles)
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id()
def setup_view(self):
view = SingleThreadViewFixture(Thread(id="comment_deletion_test_thread"))
view.addResponse(
Response(id="response1"),
[Comment(id="comment_other_author", user_id="other"), Comment(id="comment_self_author", user_id=self.user_id)])
view.push()
def test_comment_deletion_as_student(self):
self.setup_user()
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page.visit()
self.assertTrue(page.is_comment_deletable("comment_self_author"))
self.assertTrue(page.is_comment_visible("comment_other_author"))
self.assertFalse(page.is_comment_deletable("comment_other_author"))
page.delete_comment("comment_self_author")
def test_comment_deletion_as_moderator(self):
self.setup_user(roles=['Moderator'])
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_deletion_test_thread")
page.visit()
self.assertTrue(page.is_comment_deletable("comment_self_author"))
self.assertTrue(page.is_comment_deletable("comment_other_author"))
page.delete_comment("comment_self_author")
page.delete_comment("comment_other_author")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment