Commit b33a9436 by Alex Dusenbery

TNL-6832 | Include inactive enrollees in course and problem grade reports.

parent fd5eef38
...@@ -929,12 +929,18 @@ class CourseEnrollmentManager(models.Manager): ...@@ -929,12 +929,18 @@ class CourseEnrollmentManager(models.Manager):
return is_course_full return is_course_full
def users_enrolled_in(self, course_id): def users_enrolled_in(self, course_id, include_inactive=False):
"""Return a queryset of User for every user enrolled in the course.""" """
return User.objects.filter( Return a queryset of User for every user enrolled in the course. If
courseenrollment__course_id=course_id, `include_inactive` is True, returns both active and inactive enrollees
courseenrollment__is_active=True for the course. Otherwise returns actively enrolled users only.
) """
filter_kwargs = {
'courseenrollment__course_id': course_id,
}
if not include_inactive:
filter_kwargs['courseenrollment__is_active'] = True
return User.objects.filter(**filter_kwargs)
def enrollment_counts(self, course_id): def enrollment_counts(self, course_id):
""" """
......
...@@ -21,6 +21,7 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): ...@@ -21,6 +21,7 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
def setUp(self): def setUp(self):
super(CourseEnrollmentTests, self).setUp() super(CourseEnrollmentTests, self).setUp()
self.user = UserFactory.create() self.user = UserFactory.create()
self.user_2 = UserFactory.create()
def test_enrollment_status_hash_cache_key(self): def test_enrollment_status_hash_cache_key(self):
username = 'test-user' username = 'test-user'
...@@ -82,3 +83,23 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): ...@@ -82,3 +83,23 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
# Modifying enrollments should delete the cached value. # Modifying enrollments should delete the cached value.
CourseEnrollmentFactory.create(user=self.user) CourseEnrollmentFactory.create(user=self.user)
self.assertIsNone(cache.get(CourseEnrollment.enrollment_status_hash_cache_key(self.user))) self.assertIsNone(cache.get(CourseEnrollment.enrollment_status_hash_cache_key(self.user)))
def test_users_enrolled_in_active_only(self):
"""CourseEnrollment.users_enrolled_in should return only Users with active enrollments when
`include_inactive` has its default value (False)."""
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
CourseEnrollmentFactory.create(user=self.user_2, course_id=self.course.id, is_active=False)
active_enrolled_users = list(CourseEnrollment.objects.users_enrolled_in(self.course.id))
self.assertEqual([self.user], active_enrolled_users)
def test_users_enrolled_in_all(self):
"""CourseEnrollment.users_enrolled_in should return active and inactive users when
`include_inactive` is True."""
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
CourseEnrollmentFactory.create(user=self.user_2, course_id=self.course.id, is_active=False)
all_enrolled_users = list(
CourseEnrollment.objects.users_enrolled_in(self.course.id, include_inactive=True)
)
self.assertListEqual([self.user, self.user_2], all_enrolled_users)
...@@ -272,7 +272,8 @@ class CourseGradeReport(object): ...@@ -272,7 +272,8 @@ class CourseGradeReport(object):
def grouper(iterable, chunk_size=self.USER_BATCH_SIZE, fillvalue=None): def grouper(iterable, chunk_size=self.USER_BATCH_SIZE, fillvalue=None):
args = [iter(iterable)] * chunk_size args = [iter(iterable)] * chunk_size
return izip_longest(*args, fillvalue=fillvalue) return izip_longest(*args, fillvalue=fillvalue)
users = CourseEnrollment.objects.users_enrolled_in(context.course_id)
users = CourseEnrollment.objects.users_enrolled_in(context.course_id, include_inactive=True)
users = users.select_related('profile__allow_certificate') users = users.select_related('profile__allow_certificate')
return grouper(users) return grouper(users)
...@@ -412,7 +413,7 @@ class ProblemGradeReport(object): ...@@ -412,7 +413,7 @@ class ProblemGradeReport(object):
start_time = time() start_time = time()
start_date = datetime.now(UTC) start_date = datetime.now(UTC)
status_interval = 100 status_interval = 100
enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id) enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id, include_inactive=True)
task_progress = TaskProgress(action_name, enrolled_students.count(), start_time) task_progress = TaskProgress(action_name, enrolled_students.count(), start_time)
# This struct encapsulates both the display names of each static item in the # This struct encapsulates both the display names of each static item in the
......
...@@ -162,21 +162,21 @@ class InstructorTaskCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase) ...@@ -162,21 +162,21 @@ class InstructorTaskCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
self.login(user_email, "test") self.login(user_email, "test")
self.current_user = username self.current_user = username
def _create_user(self, username, email=None, is_staff=False, mode='honor'): def _create_user(self, username, email=None, is_staff=False, mode='honor', enrollment_active=True):
"""Creates a user and enrolls them in the test course.""" """Creates a user and enrolls them in the test course."""
if email is None: if email is None:
email = InstructorTaskCourseTestCase.get_user_email(username) email = InstructorTaskCourseTestCase.get_user_email(username)
thisuser = UserFactory.create(username=username, email=email, is_staff=is_staff) thisuser = UserFactory.create(username=username, email=email, is_staff=is_staff)
CourseEnrollmentFactory.create(user=thisuser, course_id=self.course.id, mode=mode) CourseEnrollmentFactory.create(user=thisuser, course_id=self.course.id, mode=mode, is_active=enrollment_active)
return thisuser return thisuser
def create_instructor(self, username, email=None): def create_instructor(self, username, email=None):
"""Creates an instructor for the test course.""" """Creates an instructor for the test course."""
return self._create_user(username, email, is_staff=True) return self._create_user(username, email, is_staff=True)
def create_student(self, username, email=None, mode='honor'): def create_student(self, username, email=None, mode='honor', enrollment_active=True):
"""Creates a student for the test course.""" """Creates a student for the test course."""
return self._create_user(username, email, is_staff=False, mode=mode) return self._create_user(username, email, is_staff=False, mode=mode, enrollment_active=enrollment_active)
@staticmethod @staticmethod
def get_task_status(task_id): def get_task_status(task_id):
......
...@@ -398,6 +398,25 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase): ...@@ -398,6 +398,25 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
with self.assertNumQueries(41): with self.assertNumQueries(41):
CourseGradeReport.generate(None, None, course.id, None, 'graded') CourseGradeReport.generate(None, None, course.id, None, 'graded')
def test_inactive_enrollments(self):
"""
Test that students with inactive enrollments are included in report.
"""
self.create_student('active-student', 'active@example.com')
self.create_student('inactive-student', 'inactive@example.com', enrollment_active=False)
self.current_task = Mock()
self.current_task.update_state = Mock()
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task') as mock_current_task:
mock_current_task.return_value = self.current_task
result = CourseGradeReport.generate(None, None, self.course.id, None, 'graded')
expected_students = 2
self.assertDictContainsSubset(
{'attempted': expected_students, 'succeeded': expected_students, 'failed': 0}, result
)
class TestTeamGradeReport(InstructorGradeReportTestCase): class TestTeamGradeReport(InstructorGradeReportTestCase):
""" Test that teams appear correctly in the grade report when it is enabled for the course. """ """ Test that teams appear correctly in the grade report when it is enabled for the course. """
...@@ -760,6 +779,55 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase): ...@@ -760,6 +779,55 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
} }
]) ])
@patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task')
def test_inactive_enrollment_included(self, _get_current_task):
"""
Students with inactive enrollments in a course should be included in Problem Grade Report.
"""
inactive_student = self.create_student('inactive-student', 'inactive@example.com', enrollment_active=False)
vertical = ItemFactory.create(
parent_location=self.problem_section.location,
category='vertical',
metadata={'graded': True},
display_name='Problem Vertical'
)
self.define_option_problem(u'Problem1', parent=vertical)
self.submit_student_answer(self.student_1.username, u'Problem1', ['Option 1'])
result = ProblemGradeReport.generate(None, None, self.course.id, None, 'graded')
self.assertDictContainsSubset({'action_name': 'graded', 'attempted': 3, 'succeeded': 3, 'failed': 0}, result)
problem_name = u'Homework 1: Subsection - Problem1'
header_row = self.csv_header_row + [problem_name + ' (Earned)', problem_name + ' (Possible)']
self.verify_rows_in_csv([
dict(zip(
header_row,
[
unicode(self.student_1.id),
self.student_1.email,
self.student_1.username,
'0.01', '1.0', '2.0',
]
)),
dict(zip(
header_row,
[
unicode(self.student_2.id),
self.student_2.email,
self.student_2.username,
'0.0', u'Not Attempted', '2.0',
]
)),
dict(zip(
header_row,
[
unicode(inactive_student.id),
inactive_student.email,
inactive_student.username,
'0.0', u'Not Attempted', '2.0',
]
))
])
@attr(shard=3) @attr(shard=3)
class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent, InstructorTaskModuleTestCase): class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent, InstructorTaskModuleTestCase):
......
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