Bulk-reads and Request caching in Course Grade Report

This commit is contained in:
Nimisha Asthagiri
2017-05-03 20:22:01 -04:00
parent 3aba9da8fa
commit 16e9636513
22 changed files with 477 additions and 130 deletions

View File

@@ -12,14 +12,19 @@ from time import time
from instructor_analytics.basic import list_problem_responses
from instructor_analytics.csvs import format_dictlist
from certificates.models import CertificateWhitelist, certificate_info_for_user
from certificates.models import CertificateWhitelist, certificate_info_for_user, GeneratedCertificate
from courseware.courses import get_course_by_id
from lms.djangoapps.grades.context import grading_context_for_course
from lms.djangoapps.grades.context import grading_context_for_course, grading_context
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.models import PersistentCourseGrade
from lms.djangoapps.teams.models import CourseTeamMembership
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from openedx.core.djangoapps.course_groups.cohorts import get_cohort, is_course_cohorted
from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
from openedx.core.djangoapps.course_groups.cohorts import get_cohort, is_course_cohorted, bulk_cache_cohorts
from openedx.core.djangoapps.user_api.course_tag.api import BulkCourseTags
from student.models import CourseEnrollment
from student.roles import BulkRoleCache
from xmodule.modulestore.django import modulestore
from xmodule.partitions.partitions_service import PartitionService
from xmodule.split_test_module import get_split_user_partitions
@@ -30,7 +35,7 @@ from .utils import upload_csv_to_report_store
TASK_LOG = logging.getLogger('edx.celery.task')
class CourseGradeReportContext(object):
class _CourseGradeReportContext(object):
"""
Internal class that provides a common context to use for a single grade
report. When a report is parallelized across multiple processes,
@@ -57,6 +62,10 @@ class CourseGradeReportContext(object):
def course(self):
return get_course_by_id(self.course_id)
@lazy
def course_structure(self):
return get_course_in_cache(self.course_id)
@lazy
def course_experiments(self):
return get_split_user_partitions(self.course.user_partitions)
@@ -75,9 +84,9 @@ class CourseGradeReportContext(object):
Returns an OrderedDict that maps an assignment type to a dict of
subsection-headers and average-header.
"""
grading_context = grading_context_for_course(self.course_id)
grading_cxt = grading_context(self.course_structure)
graded_assignments_map = OrderedDict()
for assignment_type_name, subsection_infos in grading_context['all_graded_subsections_by_type'].iteritems():
for assignment_type_name, subsection_infos in grading_cxt['all_graded_subsections_by_type'].iteritems():
graded_subsections_map = OrderedDict()
for subsection_index, subsection_info in enumerate(subsection_infos, start=1):
subsection = subsection_info['subsection_block']
@@ -112,17 +121,64 @@ class CourseGradeReportContext(object):
return self.task_progress.update_task_state(extra_meta={'step': message})
class _CertificateBulkContext(object):
def __init__(self, context, users):
certificate_whitelist = CertificateWhitelist.objects.filter(course_id=context.course_id, whitelist=True)
self.whitelisted_user_ids = [entry.user_id for entry in certificate_whitelist]
self.certificates_by_user = {
certificate.user.id: certificate
for certificate in
GeneratedCertificate.objects.filter(course_id=context.course_id, user__in=users)
}
class _TeamBulkContext(object):
def __init__(self, context, users):
if context.teams_enabled:
self.teams_by_user = {
membership.user.id: membership.team.name
for membership in
CourseTeamMembership.objects.filter(team__course_id=context.course_id, user__in=users)
}
else:
self.teams_by_user = {}
class _EnrollmentBulkContext(object):
def __init__(self, context, users):
CourseEnrollment.bulk_fetch_enrollment_states(users, context.course_id)
self.verified_users = [
verified.user.id for verified in
SoftwareSecurePhotoVerification.verified_query().filter(user__in=users).select_related('user__id')
]
class _CourseGradeBulkContext(object):
def __init__(self, context, users):
self.certs = _CertificateBulkContext(context, users)
self.teams = _TeamBulkContext(context, users)
self.enrollments = _EnrollmentBulkContext(context, users)
bulk_cache_cohorts(context.course_id, users)
BulkRoleCache.prefetch(users)
PersistentCourseGrade.prefetch(context.course_id, users)
BulkCourseTags.prefetch(context.course_id, users)
class CourseGradeReport(object):
"""
Class to encapsulate functionality related to generating Grade Reports.
"""
# Batch size for chunking the list of enrollees in the course.
USER_BATCH_SIZE = 100
@classmethod
def generate(cls, _xmodule_instance_args, _entry_id, course_id, _task_input, action_name):
"""
Public method to generate a grade report.
"""
context = CourseGradeReportContext(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name)
return CourseGradeReport()._generate(context)
with modulestore().bulk_operations(course_id):
context = _CourseGradeReportContext(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name)
return CourseGradeReport()._generate(context)
def _generate(self, context):
"""
@@ -166,6 +222,7 @@ class CourseGradeReport(object):
A generator of batches of (success_rows, error_rows) for this report.
"""
for users in self._batch_users(context):
users = filter(lambda u: u is not None, users)
yield self._rows_for_users(context, users)
def _compile(self, context, batched_rows):
@@ -211,10 +268,11 @@ class CourseGradeReport(object):
"""
Returns a generator of batches of users.
"""
def grouper(iterable, chunk_size=1, fillvalue=None):
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 = users.select_related('profile__allow_certificate')
return grouper(users)
def _user_grade_results(self, course_grade, context):
@@ -249,7 +307,7 @@ class CourseGradeReport(object):
"""
cohort_group_names = []
if context.cohorts_enabled:
group = get_cohort(user, context.course_id, assign=False)
group = get_cohort(user, context.course_id, assign=False, use_cached=True)
cohort_group_names.append(group.name if group else '')
return cohort_group_names
@@ -264,20 +322,13 @@ class CourseGradeReport(object):
experiment_group_names.append(group.name if group else '')
return experiment_group_names
def _user_team_names(self, user, context):
def _user_team_names(self, user, bulk_teams):
"""
Returns a list of names of teams in which the given user belongs.
"""
team_names = []
if context.teams_enabled:
try:
membership = CourseTeamMembership.objects.get(user=user, team__course_id=context.course_id)
team_names.append(membership.team.name)
except CourseTeamMembership.DoesNotExist:
team_names.append('')
return team_names
return [bulk_teams.teams_by_user.get(user.id, '')]
def _user_verification_mode(self, user, context):
def _user_verification_mode(self, user, context, bulk_enrollments):
"""
Returns a list of enrollment-mode and verification-status for the
given user.
@@ -286,19 +337,21 @@ class CourseGradeReport(object):
verification_status = SoftwareSecurePhotoVerification.verification_status_for_user(
user,
context.course_id,
enrollment_mode
enrollment_mode,
user_is_verified=user.id in bulk_enrollments.verified_users,
)
return [enrollment_mode, verification_status]
def _user_certificate_info(self, user, context, course_grade, whitelisted_user_ids):
def _user_certificate_info(self, user, context, course_grade, bulk_certs):
"""
Returns the course certification information for the given user.
"""
is_whitelisted = user.id in bulk_certs.whitelisted_user_ids
certificate_info = certificate_info_for_user(
user,
context.course_id,
course_grade.letter_grade,
user.id in whitelisted_user_ids
is_whitelisted,
bulk_certs.certificates_by_user.get(user.id),
)
TASK_LOG.info(
u'Student certificate eligibility: %s '
@@ -311,7 +364,7 @@ class CourseGradeReport(object):
course_grade.letter_grade,
context.course.grade_cutoffs,
user.profile.allow_certificate,
user.id in whitelisted_user_ids,
is_whitelisted,
)
return certificate_info
@@ -319,24 +372,30 @@ class CourseGradeReport(object):
"""
Returns a list of rows for the given users for this report.
"""
certificate_whitelist = CertificateWhitelist.objects.filter(course_id=context.course_id, whitelist=True)
whitelisted_user_ids = [entry.user_id for entry in certificate_whitelist]
success_rows, error_rows = [], []
for user, course_grade, error in CourseGradeFactory().iter(users, course_key=context.course_id):
if not course_grade:
# An empty gradeset means we failed to grade a student.
error_rows.append([user.id, user.username, error.message])
else:
success_rows.append(
[user.id, user.email, user.username] +
self._user_grade_results(course_grade, context) +
self._user_cohort_group_names(user, context) +
self._user_experiment_group_names(user, context) +
self._user_team_names(user, context) +
self._user_verification_mode(user, context) +
self._user_certificate_info(user, context, course_grade, whitelisted_user_ids)
)
return success_rows, error_rows
with modulestore().bulk_operations(context.course_id):
bulk_context = _CourseGradeBulkContext(context, users)
success_rows, error_rows = [], []
for user, course_grade, error in CourseGradeFactory().iter(
users,
course=context.course,
collected_block_structure=context.course_structure,
course_key=context.course_id,
):
if not course_grade:
# An empty gradeset means we failed to grade a student.
error_rows.append([user.id, user.username, error.message])
else:
success_rows.append(
[user.id, user.email, user.username] +
self._user_grade_results(course_grade, context) +
self._user_cohort_group_names(user, context) +
self._user_experiment_group_names(user, context) +
self._user_team_names(user, bulk_context.teams) +
self._user_verification_mode(user, context, bulk_context.enrollments) +
self._user_certificate_info(user, context, course_grade, bulk_context.certs)
)
return success_rows, error_rows
class ProblemGradeReport(object):

View File

@@ -34,9 +34,11 @@ from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMe
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup, CohortMembership
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from openedx.core.djangoapps.credit.tests.factories import CreditCourseFactory
import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
from openedx.core.djangoapps.util.testing import ContentGroupTestCase, TestConditionalContent
from request_cache.middleware import RequestCache
from shoppingcart.models import (
Order, PaidCourseRegistration, CourseRegistrationCode, Invoice,
CourseRegistrationCodeInvoiceItem, InvoiceTransaction, Coupon
@@ -44,8 +46,9 @@ from shoppingcart.models import (
from student.models import CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED
from student.tests.factories import CourseEnrollmentFactory, CourseModeFactory, UserFactory
from survey.models import SurveyForm, SurveyAnswer
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
from xmodule.partitions.partitions import Group, UserPartition
from ..models import ReportStore
@@ -321,6 +324,44 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
result = CourseGradeReport.generate(None, None, self.course.id, None, 'graded')
self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 4),
(ModuleStoreEnum.Type.split, 3),
)
@ddt.unpack
def test_query_counts(self, store_type, mongo_count):
with self.store.default_store(store_type):
experiment_group_a = Group(2, u'Expériment Group A')
experiment_group_b = Group(3, u'Expériment Group B')
experiment_partition = UserPartition(
1,
u'Content Expériment Configuration',
u'Group Configuration for Content Expériments',
[experiment_group_a, experiment_group_b],
scheme_id='random'
)
course = CourseFactory.create(
cohort_config={'cohorted': True, 'auto_cohort': True, 'auto_cohort_groups': ['cohort 1', 'cohort 2']},
user_partitions=[experiment_partition],
teams_configuration={
'max_size': 2, 'topics': [{'topic-id': 'topic', 'name': 'Topic', 'description': 'A Topic'}]
},
)
_ = CreditCourseFactory(course_key=course.id)
num_users = 5
for _ in range(num_users):
user = UserFactory.create()
CourseEnrollment.enroll(user, course.id, mode='verified')
SoftwareSecurePhotoVerificationFactory.create(user=user, status='approved')
RequestCache.clear_request_cache()
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
with check_mongo_calls(mongo_count):
with self.assertNumQueries(41):
CourseGradeReport.generate(None, None, course.id, None, 'graded')
class TestTeamGradeReport(InstructorGradeReportTestCase):
""" Test that teams appear correctly in the grade report when it is enabled for the course. """
@@ -1783,7 +1824,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 3,
'skipped': 2
}
with self.assertNumQueries(186):
with self.assertNumQueries(171):
self.assertCertificatesGenerated(task_input, expected_results)
expected_results = {