Commit 58be2d98 by Chris Dodge Committed by Jonathan Piacenti

cdodge/filter-out-observers: remove observer (or other roles) from aggregate API calls

make sure the social stats API endpoint accept an new source key format, but the forum service needs the old style. Plus add tests
parent 9de077bb
...@@ -6,6 +6,7 @@ Run these tests @ Devstack: ...@@ -6,6 +6,7 @@ Run these tests @ Devstack:
from datetime import datetime from datetime import datetime
import json import json
import uuid import uuid
import mock
from random import randint from random import randint
from urllib import urlencode from urllib import urlencode
...@@ -27,7 +28,8 @@ from .content import TEST_COURSE_OVERVIEW_CONTENT, TEST_COURSE_UPDATES_CONTENT, ...@@ -27,7 +28,8 @@ from .content import TEST_COURSE_OVERVIEW_CONTENT, TEST_COURSE_UPDATES_CONTENT,
from .content import TEST_STATIC_TAB1_CONTENT, TEST_STATIC_TAB2_CONTENT from .content import TEST_STATIC_TAB1_CONTENT, TEST_STATIC_TAB2_CONTENT
TEST_API_KEY = str(uuid.uuid4()) TEST_API_KEY = str(uuid.uuid4())
USER_COUNT = 4 USER_COUNT = 5
SAMPLE_GRADE_DATA_COUNT = 4
class SecureClient(Client): class SecureClient(Client):
...@@ -141,9 +143,10 @@ class CoursesApiTests(TestCase): ...@@ -141,9 +143,10 @@ class CoursesApiTests(TestCase):
user_profile = user.profile user_profile = user.profile
user_profile.avatar_url = 'http://example.com/{}.png'.format(user.id) user_profile.avatar_url = 'http://example.com/{}.png'.format(user.id)
user_profile.title = 'Software Engineer {}'.format(user.id) user_profile.title = 'Software Engineer {}'.format(user.id)
user_profile.city = 'Cambridge'
user_profile.save() user_profile.save()
for i in xrange(USER_COUNT - 1): for i in xrange(SAMPLE_GRADE_DATA_COUNT - 1):
category = 'mentoring' category = 'mentoring'
module_type = 'mentoring' module_type = 'mentoring'
if i % 2 is 0: if i % 2 is 0:
...@@ -1480,7 +1483,36 @@ class CoursesApiTests(TestCase): ...@@ -1480,7 +1483,36 @@ class CoursesApiTests(TestCase):
response = self.do_get(completion_uri) response = self.do_get(completion_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def _fake_get_get_course_social_stats(course_id):
return {
'1': {'foo':'bar'},
'2': {'one': 'two'}
}
@mock.patch("lms.lib.comment_client.user.get_course_social_stats", _fake_get_get_course_social_stats)
def test_social_metrics(self):
test_uri = '{}/{}/metrics/social/'.format(self.base_courses_uri, self.test_course_id)
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertTrue(len(response.data.keys()), 2)
self.assertIn('1', response.data)
self.assertIn('2', response.data)
# make the first user an observer to asset that its content is being filtered out from
# the aggregates
allow_access(self.course, self.users[0], 'observer')
response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200)
self.assertTrue(len(response.data.keys()), 1)
self.assertNotIn('1', response.data)
self.assertIn('2', response.data)
def test_courses_leaders_list_get(self): def test_courses_leaders_list_get(self):
# make the last user an observer to asset that its content is being filtered out from
# the aggregates
allow_access(self.course, self.users[USER_COUNT-1], 'observer')
test_uri = '{}/{}/metrics/proficiency/leaders/'.format(self.base_courses_uri, self.test_course_id) test_uri = '{}/{}/metrics/proficiency/leaders/'.format(self.base_courses_uri, self.test_course_id)
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -1535,6 +1567,7 @@ class CoursesApiTests(TestCase): ...@@ -1535,6 +1567,7 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_courses_completions_leaders_list_get(self): def test_courses_completions_leaders_list_get(self):
completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, unicode(self.course.id)) completion_uri = '{}/{}/completions/'.format(self.base_courses_uri, unicode(self.course.id))
users = [] users = []
for i in xrange(1, 5): for i in xrange(1, 5):
...@@ -1549,6 +1582,9 @@ class CoursesApiTests(TestCase): ...@@ -1549,6 +1582,9 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
users.append(response.data['id']) users.append(response.data['id'])
# make the last user an observer to make sure that data is being filtered out
allow_access(self.course, self.users[USER_COUNT-1], 'observer')
for i in xrange(1, 26): for i in xrange(1, 26):
local_content_name = 'Video_Sequence{}'.format(i) local_content_name = 'Video_Sequence{}'.format(i)
local_content = ItemFactory.create( local_content = ItemFactory.create(
...@@ -1565,11 +1601,18 @@ class CoursesApiTests(TestCase): ...@@ -1565,11 +1601,18 @@ class CoursesApiTests(TestCase):
user_id = users[2] user_id = users[2]
else: else:
user_id = users[3] user_id = users[3]
content_id = unicode(local_content.scope_ids.usage_id) content_id = unicode(local_content.scope_ids.usage_id)
completions_data = {'content_id': content_id, 'user_id': user_id} completions_data = {'content_id': content_id, 'user_id': user_id}
response = self.do_post(completion_uri, completions_data) response = self.do_post(completion_uri, completions_data)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
# observer should complete everything, so we can assert that it is filtered out
response = self.do_post(completion_uri, {
'content_id': content_id, 'user_id': self.users[USER_COUNT-1].id
})
self.assertEqual(response.status_code, 201)
test_uri = '{}/{}/metrics/completions/leaders/?{}'.format(self.base_courses_uri, self.test_course_id, 'count=6') test_uri = '{}/{}/metrics/completions/leaders/?{}'.format(self.base_courses_uri, self.test_course_id, 'count=6')
response = self.do_get(test_uri) response = self.do_get(test_uri)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -1761,6 +1804,11 @@ class CoursesApiTests(TestCase): ...@@ -1761,6 +1804,11 @@ class CoursesApiTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['city'], city) self.assertEqual(response.data['city'], city)
# make all the classwide users an observer to assert that its content is being filtered out from
# the aggregates
for user in self.users:
allow_access(self.course, user, 'observer')
response = self.do_get('{}{}{}'.format('/api/courses/', self.test_course_id, '/metrics/cities/')) response = self.do_get('{}{}{}'.format('/api/courses/', self.test_course_id, '/metrics/cities/'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 4) self.assertEqual(len(response.data['results']), 4)
......
...@@ -23,7 +23,7 @@ from courseware.views import get_static_tab_contents ...@@ -23,7 +23,7 @@ from courseware.views import get_static_tab_contents
from django_comment_common.models import FORUM_ROLE_MODERATOR from django_comment_common.models import FORUM_ROLE_MODERATOR
from instructor.access import revoke_access, update_forum_role from instructor.access import revoke_access, update_forum_role
from student.models import CourseEnrollment, CourseEnrollmentAllowed from student.models import CourseEnrollment, CourseEnrollmentAllowed
from student.roles import CourseAccessRole, CourseInstructorRole, CourseStaffRole, CourseObserverRole, UserBasedRole from student.roles import CourseRole, CourseAccessRole, CourseInstructorRole, CourseStaffRole, CourseObserverRole, UserBasedRole
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -41,6 +41,8 @@ from .serializers import GradeSerializer, CourseLeadersSerializer, CourseComplet ...@@ -41,6 +41,8 @@ from .serializers import GradeSerializer, CourseLeadersSerializer, CourseComplet
from lms.lib.comment_client.user import get_course_social_stats from lms.lib.comment_client.user import get_course_social_stats
from lms.lib.comment_client.utils import CommentClientRequestError from lms.lib.comment_client.utils import CommentClientRequestError
from opaque_keys.edx.keys import CourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -304,6 +306,27 @@ def _manage_role(course_descriptor, user, role, action): ...@@ -304,6 +306,27 @@ def _manage_role(course_descriptor, user, role, action):
update_forum_role(course_descriptor.id, user, FORUM_ROLE_MODERATOR, 'revoke') update_forum_role(course_descriptor.id, user, FORUM_ROLE_MODERATOR, 'revoke')
def _get_aggregate_exclusion_user_ids(course_key):
"""
This helper method will return the list of user ids that are marked in roles
that can be excluded from certain aggregate queries. The list of roles to exclude
can be defined in a AGGREGATION_EXCLUDE_ROLES settings variable
"""
exclude_user_ids = set()
exclude_role_list = getattr(settings, 'AGGREGATION_EXCLUDE_ROLES', [CourseObserverRole.ROLE])
for role in exclude_role_list:
users = CourseRole(role, course_key).users_with_role()
user_ids = set()
for user in users:
user_ids.add(user.id)
exclude_user_ids = exclude_user_ids.union(user_ids)
return exclude_user_ids
class CourseContentList(SecureAPIView): class CourseContentList(SecureAPIView):
""" """
**Use Case** **Use Case**
...@@ -1515,13 +1538,14 @@ class CoursesLeadersList(SecureListAPIView): ...@@ -1515,13 +1538,14 @@ class CoursesLeadersList(SecureListAPIView):
course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612 course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not course_descriptor: if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
queryset = StudentModule.objects.filter( queryset = StudentModule.objects.filter(
course_id__exact=course_key, course_id__exact=course_key,
grade__isnull=False, grade__isnull=False,
max_grade__isnull=False, max_grade__isnull=False,
max_grade__gt=0, max_grade__gt=0,
student__is_active=True student__is_active=True
) ).exclude(student__in=_get_aggregate_exclusion_user_ids(course_key))
if content_id: if content_id:
content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612 content_descriptor, content_key, existing_content = get_course_child(request, request.user, course_key, content_id) # pylint: disable=W0612
...@@ -1580,7 +1604,10 @@ class CoursesCompletionsLeadersList(SecureAPIView): ...@@ -1580,7 +1604,10 @@ class CoursesCompletionsLeadersList(SecureAPIView):
course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612 course_descriptor, course_key, course_content = get_course(request, request.user, course_id) # pylint: disable=W0612
if not course_descriptor: if not course_descriptor:
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
queryset = CourseModuleCompletion.objects.filter(course_id=course_key)
exclude_users = _get_aggregate_exclusion_user_ids(course_key)
queryset = CourseModuleCompletion.objects.filter(course_id=course_key)\
.exclude(user__in=exclude_users)
if user_id: if user_id:
user_completions = queryset.filter(user__id=user_id).count() user_completions = queryset.filter(user__id=user_id).count()
...@@ -1591,6 +1618,7 @@ class CoursesCompletionsLeadersList(SecureAPIView): ...@@ -1591,6 +1618,7 @@ class CoursesCompletionsLeadersList(SecureAPIView):
total_completions = queryset.filter(user__is_active=True).count() total_completions = queryset.filter(user__is_active=True).count()
users = CourseModuleCompletion.objects.filter(user__is_active=True)\ users = CourseModuleCompletion.objects.filter(user__is_active=True)\
.exclude(user__in=exclude_users)\
.aggregate(total=Count('user__id', distinct=True)) .aggregate(total=Count('user__id', distinct=True))
if users and users['total'] > 0: if users and users['total'] > 0:
...@@ -1636,7 +1664,19 @@ class CoursesSocialMetrics(SecureListAPIView): ...@@ -1636,7 +1664,19 @@ class CoursesSocialMetrics(SecureListAPIView):
def get(self, request, course_id): # pylint: disable=W0613 def get(self, request, course_id): # pylint: disable=W0613
try: try:
data = get_course_social_stats(course_id) course_key = CourseKey.from_string(course_id)
# the forum service expects the legacy slash separated string format
data = get_course_social_stats(course_key.to_deprecated_string())
# remove any excluded users from the aggregate
exclude_users = _get_aggregate_exclusion_user_ids(course_key)
for user_id in exclude_users:
if str(user_id) in data:
del data[str(user_id)]
http_status = status.HTTP_200_OK http_status = status.HTTP_200_OK
except CommentClientRequestError, e: except CommentClientRequestError, e:
data = { data = {
...@@ -1666,7 +1706,9 @@ class CoursesCitiesMetrics(SecureListAPIView): ...@@ -1666,7 +1706,9 @@ class CoursesCitiesMetrics(SecureListAPIView):
course_descriptor, course_key, course_content = get_course(self.request, self.request.user, course_id) # pylint: disable=W0612 course_descriptor, course_key, course_content = get_course(self.request, self.request.user, course_id) # pylint: disable=W0612
if not course_descriptor: if not course_descriptor:
raise Http404 raise Http404
queryset = CourseEnrollment.users_enrolled_in(course_key)
exclude_users = _get_aggregate_exclusion_user_ids(course_key)
queryset = CourseEnrollment.users_enrolled_in(course_key).exclude(id__in=exclude_users)
if city: if city:
city = city.split(',')[:upper_bound] city = city.split(',')[:upper_bound]
q_list = [Q(profile__city__iexact=item.strip()) for item in city] q_list = [Q(profile__city__iexact=item.strip()) for item in city]
......
...@@ -162,7 +162,9 @@ class User(models.Model): ...@@ -162,7 +162,9 @@ class User(models.Model):
def get_course_social_stats(course_id): def get_course_social_stats(course_id):
"""
Helper method to get the social stats from the comment service
"""
url = _url_for_course_social_stats() url = _url_for_course_social_stats()
params = {'course_id': course_id} params = {'course_id': course_id}
response = perform_request( response = perform_request(
......
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