implement forums endpoint for searching users
This commit is contained in:
@@ -1,23 +1,24 @@
|
||||
import logging
|
||||
import json
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.test.client import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import User
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.management import call_command
|
||||
from util.testing import UrlResetMixin
|
||||
from django_comment_common.models import Role
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
from django_comment_client.base import views
|
||||
from django_comment_client.tests.unicode import UnicodeTestMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from mock import patch, ANY
|
||||
from nose.tools import assert_true, assert_equal # pylint: disable=E0611
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from nose.tools import assert_true, assert_equal # pylint: disable=E0611
|
||||
from mock import patch, ANY
|
||||
from django_comment_client.base import views
|
||||
from django_comment_client.tests.unicode import UnicodeTestMixin
|
||||
from django_comment_common.models import Role, FORUM_ROLE_STUDENT
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
from student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from util.testing import UrlResetMixin
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -732,3 +733,85 @@ class CreateSubCommentUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, Moc
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(mock_request.called)
|
||||
self.assertEqual(mock_request.call_args[1]["data"]["body"], text)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class UsersEndpointTestCase(ModuleStoreTestCase, MockRequestSetupMixin):
|
||||
|
||||
def set_post_counts(self, mock_request, threads_count=1, comments_count=1):
|
||||
"""
|
||||
sets up a mock response from the comments service for getting post counts for our other_user
|
||||
"""
|
||||
self._set_mock_request_data(mock_request, {
|
||||
"threads_count": threads_count,
|
||||
"comments_count": comments_count,
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
seed_permissions_roles(self.course.id)
|
||||
self.student = UserFactory.create()
|
||||
self.enrollment = CourseEnrollmentFactory(user=self.student, course_id=self.course.id)
|
||||
self.other_user = UserFactory.create(username="other")
|
||||
CourseEnrollmentFactory(user=self.other_user, course_id=self.course.id)
|
||||
|
||||
def make_request(self, method='get', course_id=None, **kwargs):
|
||||
course_id = course_id or self.course.id
|
||||
request = getattr(RequestFactory(), method)("dummy_url", kwargs)
|
||||
request.user = self.student
|
||||
request.view_name = "users"
|
||||
return views.users(request, course_id=course_id.to_deprecated_string())
|
||||
|
||||
@patch('lms.lib.comment_client.utils.requests.request')
|
||||
def test_finds_exact_match(self, mock_request):
|
||||
self.set_post_counts(mock_request)
|
||||
response = self.make_request(username="other")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(
|
||||
json.loads(response.content)["users"],
|
||||
[{"id": self.other_user.id, "username": self.other_user.username}]
|
||||
)
|
||||
|
||||
@patch('lms.lib.comment_client.utils.requests.request')
|
||||
def test_finds_no_match(self, mock_request):
|
||||
self.set_post_counts(mock_request)
|
||||
response = self.make_request(username="othor")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(json.loads(response.content)["users"], [])
|
||||
|
||||
def test_requires_GET(self):
|
||||
response = self.make_request(method='post', username="other")
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_requires_username_param(self):
|
||||
response = self.make_request()
|
||||
self.assertEqual(response.status_code, 400)
|
||||
content = json.loads(response.content)
|
||||
self.assertIn("errors", content)
|
||||
self.assertNotIn("users", content)
|
||||
|
||||
def test_course_does_not_exist(self):
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string("does/not/exist")
|
||||
response = self.make_request(course_id=course_id, username="other")
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
content = json.loads(response.content)
|
||||
self.assertIn("errors", content)
|
||||
self.assertNotIn("users", content)
|
||||
|
||||
def test_requires_requestor_enrolled_in_course(self):
|
||||
# unenroll self.student from the course.
|
||||
self.enrollment.delete()
|
||||
|
||||
response = self.make_request(username="other")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
content = json.loads(response.content)
|
||||
self.assertTrue(content.has_key("errors"))
|
||||
self.assertFalse(content.has_key("users"))
|
||||
|
||||
@patch('lms.lib.comment_client.utils.requests.request')
|
||||
def test_requires_matched_user_has_forum_content(self, mock_request):
|
||||
self.set_post_counts(mock_request, 0, 0)
|
||||
response = self.make_request(username="other")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(json.loads(response.content)["users"], [])
|
||||
|
||||
@@ -27,4 +27,5 @@ urlpatterns = patterns('django_comment_client.base.views', # nopep8
|
||||
url(r'^(?P<commentable_id>[\w\-.]+)/threads/create$', 'create_thread', name='create_thread'),
|
||||
url(r'^(?P<commentable_id>[\w\-.]+)/follow$', 'follow_commentable', name='follow_commentable'),
|
||||
url(r'^(?P<commentable_id>[\w\-.]+)/unfollow$', 'unfollow_commentable', name='unfollow_commentable'),
|
||||
url(r'users$', 'users', name='users'),
|
||||
)
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import time
|
||||
import random
|
||||
import os.path
|
||||
import logging
|
||||
import urlparse
|
||||
import functools
|
||||
import logging
|
||||
import os.path
|
||||
import random
|
||||
import time
|
||||
import urlparse
|
||||
|
||||
import lms.lib.comment_client as cc
|
||||
import django_comment_client.utils as utils
|
||||
import django_comment_client.settings as cc_settings
|
||||
|
||||
|
||||
from django.core import exceptions
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators import csrf
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import exceptions
|
||||
from django.core.files.storage import get_storage_class
|
||||
from django.http import Http404
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators import csrf
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import get_course_with_access, get_course_by_id
|
||||
from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
|
||||
|
||||
from django_comment_client.utils import JsonResponse, JsonError, extract, add_courseware_context
|
||||
|
||||
import django_comment_client.settings as cc_settings
|
||||
from django_comment_client.utils import (
|
||||
add_courseware_context,
|
||||
get_annotated_content_info,
|
||||
get_ability,
|
||||
JsonError,
|
||||
JsonResponse,
|
||||
safe_content
|
||||
)
|
||||
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
|
||||
from courseware.access import has_access
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
import lms.lib.comment_client as cc
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,9 +55,9 @@ def permitted(fn):
|
||||
|
||||
def ajax_content_response(request, course_id, content):
|
||||
user_info = cc.User.from_django_user(request.user).to_dict()
|
||||
annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info)
|
||||
annotated_content_info = get_annotated_content_info(course_id, content, request.user, user_info)
|
||||
return JsonResponse({
|
||||
'content': utils.safe_content(content),
|
||||
'content': safe_content(content),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
|
||||
@@ -133,7 +137,7 @@ def create_thread(request, course_id, commentable_id):
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_id, data)
|
||||
else:
|
||||
return JsonResponse(utils.safe_content(data))
|
||||
return JsonResponse(safe_content(data))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -154,7 +158,7 @@ def update_thread(request, course_id, thread_id):
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), thread.to_dict())
|
||||
else:
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
def _create_comment(request, course_key, thread_id=None, parent_id=None):
|
||||
@@ -195,7 +199,7 @@ def _create_comment(request, course_key, thread_id=None, parent_id=None):
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, course_key, comment.to_dict())
|
||||
else:
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -222,7 +226,7 @@ def delete_thread(request, course_id, thread_id):
|
||||
"""
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.delete()
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -241,7 +245,7 @@ def update_comment(request, course_id, comment_id):
|
||||
if request.is_ajax():
|
||||
return ajax_content_response(request, SlashSeparatedCourseKey.from_deprecated_string(course_id), comment.to_dict())
|
||||
else:
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -255,7 +259,7 @@ def endorse_comment(request, course_id, comment_id):
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
|
||||
comment.save()
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -271,8 +275,8 @@ def openclose_thread(request, course_id, thread_id):
|
||||
thread.save()
|
||||
thread = thread.to_dict()
|
||||
return JsonResponse({
|
||||
'content': utils.safe_content(thread),
|
||||
'ability': utils.get_ability(SlashSeparatedCourseKey.from_deprecated_string(course_id), thread, request.user),
|
||||
'content': safe_content(thread),
|
||||
'ability': get_ability(SlashSeparatedCourseKey.from_deprecated_string(course_id), thread, request.user),
|
||||
})
|
||||
|
||||
|
||||
@@ -300,7 +304,7 @@ def delete_comment(request, course_id, comment_id):
|
||||
"""
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.delete()
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -313,7 +317,7 @@ def vote_for_comment(request, course_id, comment_id, value):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
user.vote(comment, value)
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -327,7 +331,7 @@ def undo_vote_for_comment(request, course_id, comment_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
user.unvote(comment)
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -341,7 +345,7 @@ def vote_for_thread(request, course_id, thread_id, value):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.vote(thread, value)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -355,7 +359,7 @@ def flag_abuse_for_thread(request, course_id, thread_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.flagAbuse(user, thread)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -372,7 +376,7 @@ def un_flag_abuse_for_thread(request, course_id, thread_id):
|
||||
thread = cc.Thread.find(thread_id)
|
||||
remove_all = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, 'staff', course)
|
||||
thread.unFlagAbuse(user, thread, remove_all)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -386,7 +390,7 @@ def flag_abuse_for_comment(request, course_id, comment_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.flagAbuse(user, comment)
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -403,7 +407,7 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
|
||||
remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.unFlagAbuse(user, comment, remove_all)
|
||||
return JsonResponse(utils.safe_content(comment.to_dict()))
|
||||
return JsonResponse(safe_content(comment.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -417,7 +421,7 @@ def undo_vote_for_thread(request, course_id, thread_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
user.unvote(thread)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -431,7 +435,7 @@ def pin_thread(request, course_id, thread_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.pin(user, thread_id)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -445,7 +449,7 @@ def un_pin_thread(request, course_id, thread_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
thread.un_pin(user, thread_id)
|
||||
return JsonResponse(utils.safe_content(thread.to_dict()))
|
||||
return JsonResponse(safe_content(thread.to_dict()))
|
||||
|
||||
|
||||
@require_POST
|
||||
@@ -598,3 +602,40 @@ def upload(request, course_id): # ajax upload file to a question or answer
|
||||
'file_url': file_url,
|
||||
}
|
||||
})
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
def users(request, course_id):
|
||||
"""
|
||||
Given a `username` query parameter, find matches for users in the forum for this course.
|
||||
|
||||
Only exact matches are supported here, so the length of the result set will either be 0 or 1.
|
||||
"""
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
try:
|
||||
course = get_course_with_access(request.user, 'load_forum', course_key)
|
||||
except Http404:
|
||||
# course didn't exist, or requesting user does not have access to it.
|
||||
return JsonError(status=404)
|
||||
|
||||
try:
|
||||
username = request.GET['username']
|
||||
except KeyError:
|
||||
# 400 is default status for JsonError
|
||||
return JsonError(["username parameter is required"])
|
||||
|
||||
user_objs = []
|
||||
try:
|
||||
matched_user = User.objects.get(username=username)
|
||||
cc_user = cc.User.from_django_user(matched_user)
|
||||
cc_user.course_id=course_key
|
||||
cc_user.retrieve(complete=False)
|
||||
if (cc_user['threads_count'] + cc_user['comments_count']) > 0:
|
||||
user_objs.append({
|
||||
'id': matched_user.id,
|
||||
'username': matched_user.username,
|
||||
})
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
return JsonResponse({"users": user_objs})
|
||||
|
||||
@@ -117,6 +117,7 @@ class User(models.Model):
|
||||
def _retrieve(self, *args, **kwargs):
|
||||
url = self.url(action='get', params=self.attributes)
|
||||
retrieve_params = self.default_retrieve_params
|
||||
retrieve_params.update(kwargs)
|
||||
if self.attributes.get('course_id'):
|
||||
retrieve_params['course_id'] = self.course_id.to_deprecated_string()
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user