feat: add partial username search for course discussion user stats

This commit is contained in:
SaadYousaf
2022-06-24 12:27:41 +05:00
committed by Saad Yousaf
parent 2dc7990653
commit 28a8e618ac
5 changed files with 137 additions and 11 deletions

View File

@@ -94,7 +94,13 @@ from .serializers import (
UserStatsSerializer,
get_context,
)
from .utils import discussion_open_for_user, set_attribute
from .utils import (
discussion_open_for_user,
get_usernames_from_search_string,
add_stats_for_users_with_no_discussion_content,
set_attribute,
)
User = get_user_model()
@@ -1602,6 +1608,7 @@ def get_course_discussion_user_stats(
page: int,
page_size: int,
order_by: UserOrdering = None,
username_search_string: str = None,
) -> Dict:
"""
Get paginated course discussion stats for users in the course.
@@ -1612,6 +1619,7 @@ def get_course_discussion_user_stats(
page (int): Page number to fetch
page_size (int): Number of items in each page
order_by (UserOrdering): The ordering to use for the user stats
username_search_string (str): Partial string to match user names
Returns:
Paginated data of a user's discussion stats sorted based on the specified ordering.
@@ -1625,11 +1633,31 @@ def get_course_discussion_user_stats(
order_by = order_by or UserOrdering.BY_ACTIVITY
if order_by != UserOrdering.BY_ACTIVITY:
raise ValidationError({"order_by": "Invalid value"})
course_stats_response = get_course_user_stats(course_key, {
params = {
'sort_key': str(order_by),
'page': page,
'per_page': page_size,
})
}
comma_separated_usernames = matched_users_count = matched_users_pages = None
if username_search_string:
comma_separated_usernames, matched_users_count, matched_users_pages = get_usernames_from_search_string(
course_key, username_search_string, page, page_size
)
if not comma_separated_usernames:
return DiscussionAPIPagination(request, 0, 1).get_paginated_response({
"results": [],
})
params['usernames'] = comma_separated_usernames
course_stats_response = get_course_user_stats(course_key, params)
if comma_separated_usernames:
updated_course_stats = add_stats_for_users_with_no_discussion_content(
course_stats_response["user_stats"],
comma_separated_usernames,
)
course_stats_response["user_stats"] = updated_course_stats
serializer = UserStatsSerializer(
course_stats_response["user_stats"],
context={"is_privileged": is_privileged},
@@ -1639,8 +1667,8 @@ def get_course_discussion_user_stats(
paginator = DiscussionAPIPagination(
request,
course_stats_response["page"],
course_stats_response["num_pages"],
course_stats_response["count"],
matched_users_pages if username_search_string else course_stats_response["num_pages"],
matched_users_count if username_search_string else course_stats_response["count"],
)
return paginator.get_paginated_response({
"results": serializer.data,

View File

@@ -229,3 +229,4 @@ class TopicListGetForm(Form):
class CourseActivityStatsForm(_PaginationForm):
"""Form for validating course activity stats API query parameters"""
order_by = ChoiceField(choices=UserOrdering.choices, required=False)
username = CharField(required=False)

View File

@@ -21,12 +21,12 @@ from rest_framework.parsers import JSONParser
from rest_framework.test import APIClient, APITestCase
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.models import get_retired_username_by_username
from common.djangoapps.student.models import get_retired_username_by_username, CourseEnrollment
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, SuperuserFactory, UserFactory
from common.djangoapps.util.testing import PatchMediaTypeMixin, UrlResetMixin
@@ -2890,7 +2890,8 @@ class CourseDiscussionRolesAPIViewTest(APITestCase, UrlResetMixin, ModuleStoreTe
@ddt.ddt
@httpretty.activate
class CourseActivityStatsTest(ForumsEnableMixin, UrlResetMixin, CommentsServiceMockMixin, APITestCase):
class CourseActivityStatsTest(ForumsEnableMixin, UrlResetMixin, CommentsServiceMockMixin, APITestCase,
SharedModuleStoreTestCase):
"""
Tests for the course stats endpoint
"""
@@ -2898,11 +2899,12 @@ class CourseActivityStatsTest(ForumsEnableMixin, UrlResetMixin, CommentsServiceM
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self) -> None:
super().setUp()
self.course_key = 'course-v1:test+test+test'
seed_permissions_roles(self.course_key)
self.course = CourseFactory.create()
self.course_key = str(self.course.id)
seed_permissions_roles(self.course.id)
self.user = UserFactory(username='user')
self.moderator = UserFactory(username='moderator')
moderator_role = Role.objects.get(name="Moderator", course_id=self.course_key)
moderator_role = Role.objects.get(name="Moderator", course_id=self.course.id)
moderator_role.users.add(self.moderator)
self.stats = [
{
@@ -2915,6 +2917,16 @@ class CourseActivityStatsTest(ForumsEnableMixin, UrlResetMixin, CommentsServiceM
}
for idx in range(10)
]
for stat in self.stats:
user = UserFactory.create(
username=stat['username'],
email=f"{stat['username']}@example.com",
password='12345'
)
CourseEnrollment.enroll(user, self.course.id, mode='audit')
CourseEnrollment.enroll(self.moderator, self.course.id, mode='audit')
self.stats_without_flags = [{**stat, "active_flags": None, "inactive_flags": None} for stat in self.stats]
self.register_course_stats_response(self.course_key, self.stats, 1, 3)
self.url = reverse("discussion_course_activity_stats", kwargs={"course_key_string": self.course_key})
@@ -2973,3 +2985,35 @@ class CourseActivityStatsTest(ForumsEnableMixin, UrlResetMixin, CommentsServiceM
self.client.login(username=self.user.username, password='test')
response = self.client.get(self.url, {"order_by": order_by})
assert "order_by" in response.json()["field_errors"]
@ddt.data(
('user', 'user-0,user-1,user-2,user-3,user-4,user-5,user-6,user-7,user-8,user-9'),
('moderator', 'moderator'),
)
@ddt.unpack
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_DISCUSSION_SERVICE': True})
def test_with_username_param(self, username_search_string, comma_separated_usernames):
"""
Test for endpoint with username param.
"""
params = {'username': username_search_string}
self.client.login(username=self.moderator.username, password='test')
self.client.get(self.url, params)
assert urlparse(
httpretty.last_request().path # lint-amnesty, pylint: disable=no-member
).path == f'/api/v1/users/{self.course_key}/stats'
assert parse_qs(
urlparse(httpretty.last_request().path).query # lint-amnesty, pylint: disable=no-member
).get('usernames', [None]) == [comma_separated_usernames]
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_DISCUSSION_SERVICE': True})
def test_with_username_param_with_no_matches(self):
"""
Test for endpoint with username param with no matches.
"""
params = {'username': 'unknown'}
self.client.login(username=self.moderator.username, password='test')
response = self.client.get(self.url, params)
data = response.json()
self.assertFalse(data['results'])
assert data['pagination']['count'] == 0

View File

@@ -2,6 +2,10 @@
Utils for discussion API.
"""
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.core.paginator import Paginator
from django.db.models.functions import Length
from lms.djangoapps.discussion.django_comment_client.utils import has_discussion_privileges
@@ -28,3 +32,50 @@ def set_attribute(threads, attribute, value):
for thread in threads:
thread[attribute] = value
return threads
def get_usernames_from_search_string(course_id, search_string, page_number, page_size):
"""
Gets usernames for all users in course that match string.
Args:
course_id (CourseKey): Course to check discussions for
search_string (str): String to search matching
page_number (int): Page number to fetch
page_size (int): Number of items in each page
Returns:
page_matched_users (str): comma seperated usernames for the page
matched_users_count (int): count of matched users in course
matched_users_pages (int): pages of matched users in course
"""
matched_users_in_course = User.objects.filter(
courseenrollment__course_id=course_id,
username__contains=search_string).order_by(Length('username').asc()).values_list('username', flat=True)
if not matched_users_in_course:
return '', 0, 0
matched_users_count = len(matched_users_in_course)
paginator = Paginator(matched_users_in_course, page_size)
page_matched_users = paginator.page(page_number)
matched_users_pages = int(matched_users_count / page_size)
return ','.join(page_matched_users), matched_users_count, matched_users_pages
def add_stats_for_users_with_no_discussion_content(course_stats, users_in_course):
"""
Update users stats for users with no discussion stats available in course
"""
users_returned_from_api = [user['username'] for user in course_stats]
user_list = users_in_course.split(',')
users_with_no_discussion_content = set(user_list) ^ set(users_returned_from_api)
updated_course_stats = course_stats
for user in users_with_no_discussion_content:
updated_course_stats.append({
'username': user,
'threads': 0,
'replies': 0,
'responses': 0,
'active_flags': 0,
'inactive_flags': 0,
})
return updated_course_stats

View File

@@ -173,12 +173,14 @@ class CourseActivityStatsView(DeveloperErrorViewMixin, APIView):
raise ValidationError(form_query_string.errors)
order_by = form_query_string.cleaned_data.get('order_by', None)
order_by = UserOrdering(order_by) if order_by else None
username_search_string = form_query_string.cleaned_data.get('username', None)
data = get_course_discussion_user_stats(
request,
course_key_string,
form_query_string.cleaned_data['page'],
form_query_string.cleaned_data['page_size'],
order_by,
username_search_string,
)
return data