From c2ebfde429d9b545ef3bca26c8144582dff6b3d5 Mon Sep 17 00:00:00 2001 From: jsa Date: Tue, 10 Jun 2014 11:45:00 -0400 Subject: [PATCH] implement forums endpoint for searching users --- .../django_comment_client/base/tests.py | 107 ++++++++++++++-- .../django_comment_client/base/urls.py | 1 + .../django_comment_client/base/views.py | 121 ++++++++++++------ lms/lib/comment_client/user.py | 1 + 4 files changed, 178 insertions(+), 52 deletions(-) diff --git a/lms/djangoapps/django_comment_client/base/tests.py b/lms/djangoapps/django_comment_client/base/tests.py index f05a245fb2..b77ee1f11c 100644 --- a/lms/djangoapps/django_comment_client/base/tests.py +++ b/lms/djangoapps/django_comment_client/base/tests.py @@ -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"], []) diff --git a/lms/djangoapps/django_comment_client/base/urls.py b/lms/djangoapps/django_comment_client/base/urls.py index 486cf5a93f..bb850e420b 100644 --- a/lms/djangoapps/django_comment_client/base/urls.py +++ b/lms/djangoapps/django_comment_client/base/urls.py @@ -27,4 +27,5 @@ urlpatterns = patterns('django_comment_client.base.views', # nopep8 url(r'^(?P[\w\-.]+)/threads/create$', 'create_thread', name='create_thread'), url(r'^(?P[\w\-.]+)/follow$', 'follow_commentable', name='follow_commentable'), url(r'^(?P[\w\-.]+)/unfollow$', 'unfollow_commentable', name='unfollow_commentable'), + url(r'users$', 'users', name='users'), ) diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 6f8939a4bc..71a1263226 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -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}) diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index 5e617f5c46..2f65e0f9b0 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -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: