Merge pull request #15046 from edx/aed/TNL-6832
TNL-6832 | Grade reports should include unenrolled learners
This commit is contained in:
@@ -929,12 +929,18 @@ class CourseEnrollmentManager(models.Manager):
|
||||
|
||||
return is_course_full
|
||||
|
||||
def users_enrolled_in(self, course_id):
|
||||
"""Return a queryset of User for every user enrolled in the course."""
|
||||
return User.objects.filter(
|
||||
courseenrollment__course_id=course_id,
|
||||
courseenrollment__is_active=True
|
||||
)
|
||||
def users_enrolled_in(self, course_id, include_inactive=False):
|
||||
"""
|
||||
Return a queryset of User for every user enrolled in the course. If
|
||||
`include_inactive` is True, returns both active and inactive enrollees
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -21,6 +21,7 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
super(CourseEnrollmentTests, self).setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.user_2 = UserFactory.create()
|
||||
|
||||
def test_enrollment_status_hash_cache_key(self):
|
||||
username = 'test-user'
|
||||
@@ -82,3 +83,23 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
|
||||
# Modifying enrollments should delete the cached value.
|
||||
CourseEnrollmentFactory.create(user=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):
|
||||
def grouper(iterable, chunk_size=self.USER_BATCH_SIZE, fillvalue=None):
|
||||
args = [iter(iterable)] * chunk_size
|
||||
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')
|
||||
return grouper(users)
|
||||
|
||||
@@ -412,7 +413,7 @@ class ProblemGradeReport(object):
|
||||
start_time = time()
|
||||
start_date = datetime.now(UTC)
|
||||
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)
|
||||
|
||||
# This struct encapsulates both the display names of each static item in the
|
||||
|
||||
@@ -162,21 +162,21 @@ class InstructorTaskCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
|
||||
self.login(user_email, "test")
|
||||
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."""
|
||||
if email is None:
|
||||
email = InstructorTaskCourseTestCase.get_user_email(username)
|
||||
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
|
||||
|
||||
def create_instructor(self, username, email=None):
|
||||
"""Creates an instructor for the test course."""
|
||||
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."""
|
||||
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
|
||||
def get_task_status(task_id):
|
||||
|
||||
@@ -398,6 +398,25 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
|
||||
with self.assertNumQueries(41):
|
||||
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):
|
||||
""" Test that teams appear correctly in the grade report when it is enabled for the course. """
|
||||
@@ -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)
|
||||
class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent, InstructorTaskModuleTestCase):
|
||||
|
||||
Reference in New Issue
Block a user