Update report generation code to include old state for backwards compatibility

Include report data generated by blocks as columns instead of a json dict
This commit is contained in:
Kshitij Sobti
2018-07-04 00:30:33 +05:30
parent 0d5c660d8a
commit 491532c2db
3 changed files with 68 additions and 36 deletions

View File

@@ -185,7 +185,7 @@ def async_migrate_transcript(self, course_key, **kwargs): # pylint: disable=un
sub_tasks = [] sub_tasks = []
video_location = unicode(video.location) video_location = unicode(video.location)
for lang in all_transcripts.keys(): for lang in all_transcripts:
sub_tasks.append(async_migrate_transcript_subtask.s( sub_tasks.append(async_migrate_transcript_subtask.s(
video_location, revision, lang, force_update, **kwargs video_location, revision, lang, force_update, **kwargs
)) ))

View File

@@ -595,8 +595,9 @@ class ProblemResponses(object):
block and it child blocks. block and it child blocks.
Returns: Returns:
List[Dict]: Returns a list of dictionaries containing the student Tuple[List[Dict], List[str]]: Returns a list of dictionaries
data which will be included in the final csv. containing the student data which will be included in the
final csv, and the features/keys to include in that CSV.
""" """
usage_key = UsageKey.from_string(usage_key_str).map_into_course(course_key) usage_key = UsageKey.from_string(usage_key_str).map_into_course(course_key)
user = get_user_model().objects.get(pk=user_id) user = get_user_model().objects.get(pk=user_id)
@@ -608,6 +609,8 @@ class ProblemResponses(object):
store = modulestore() store = modulestore()
user_state_client = DjangoXBlockUserStateClient() user_state_client = DjangoXBlockUserStateClient()
student_data_keys = set()
with store.bulk_operations(course_key): with store.bulk_operations(course_key):
for title, path, block_key in cls._build_problem_list(course_blocks, usage_key): for title, path, block_key in cls._build_problem_list(course_blocks, usage_key):
# Chapter and sequential blocks are filtered out since they include state # Chapter and sequential blocks are filtered out since they include state
@@ -616,21 +619,22 @@ class ProblemResponses(object):
continue continue
block = store.get_item(block_key) block = store.get_item(block_key)
generated_report_data = {}
# Blocks can implement the generate_report_data method to provide their own # Blocks can implement the generate_report_data method to provide their own
# human-readable formatting for user state. # human-readable formatting for user state.
if hasattr(block, 'generate_report_data'): if hasattr(block, 'generate_report_data'):
try: try:
user_state_iterator = user_state_client.iter_all_for_block(block_key) user_state_iterator = user_state_client.iter_all_for_block(block_key)
responses = [ generated_report_data = {
{'username': username, 'state': state} username: state
for username, state in for username, state in
block.generate_report_data(user_state_iterator, max_count) block.generate_report_data(user_state_iterator, max_count)
] }
except NotImplementedError: except NotImplementedError:
responses = list_problem_responses(course_key, block_key, max_count) pass
else:
responses = list_problem_responses(course_key, block_key, max_count) responses = list_problem_responses(course_key, block_key, max_count)
student_data += responses student_data += responses
for response in responses: for response in responses:
@@ -639,12 +643,24 @@ class ProblemResponses(object):
response['location'] = ' > '.join(path) response['location'] = ' > '.join(path)
# A machine-friendly location for the current block # A machine-friendly location for the current block
response['block_key'] = str(block_key) response['block_key'] = str(block_key)
user_data = generated_report_data.get(response['username'], {})
response.update(user_data)
student_data_keys = student_data_keys.union(user_data.keys())
if max_count is not None: if max_count is not None:
max_count -= len(responses) max_count -= len(responses)
if max_count <= 0: if max_count <= 0:
break break
return student_data # Keep the keys in a useful order, starting with username, title and location,
# then the columns returned by the xblock report generator in sorted order and
# finally end with the more machine friendly block_key and state.
student_data_keys_list = (
['username', 'title', 'location'] +
sorted(student_data_keys) +
['block_key', 'state']
)
return student_data, student_data_keys_list
@classmethod @classmethod
def generate(cls, _xmodule_instance_args, _entry_id, course_id, task_input, action_name): def generate(cls, _xmodule_instance_args, _entry_id, course_id, task_input, action_name):
@@ -661,14 +677,17 @@ class ProblemResponses(object):
problem_location = task_input.get('problem_location') problem_location = task_input.get('problem_location')
# Compute result table and format it # Compute result table and format it
student_data = cls._build_student_data( student_data, student_data_keys = cls._build_student_data(
user_id=task_input.get('user_id'), user_id=task_input.get('user_id'),
course_key=course_id, course_key=course_id,
usage_key_str=problem_location usage_key_str=problem_location
) )
features = ['username', 'title', 'location', 'block_key', 'state'] for data in student_data:
header, rows = format_dictlist(student_data, features) for key in student_data_keys:
data.setdefault(key, '')
header, rows = format_dictlist(student_data, student_data_keys)
task_progress.attempted = task_progress.succeeded = len(rows) task_progress.attempted = task_progress.succeeded = len(rows)
task_progress.skipped = task_progress.total - task_progress.attempted task_progress.skipped = task_progress.total - task_progress.attempted

View File

@@ -36,10 +36,15 @@ from shoppingcart.models import (
Invoice, Invoice,
InvoiceTransaction, InvoiceTransaction,
Order, Order,
PaidCourseRegistration PaidCourseRegistration,
) )
from six import text_type from six import text_type
from student.models import ALLOWEDTOENROLL_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit from student.models import (
ALLOWEDTOENROLL_TO_ENROLLED,
CourseEnrollment,
CourseEnrollmentAllowed,
ManualEnrollmentAudit,
)
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from survey.models import SurveyAnswer, SurveyForm from survey.models import SurveyAnswer, SurveyForm
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
@@ -57,24 +62,24 @@ from lms.djangoapps.instructor_task.tasks_helper.enrollments import (
upload_enrollment_report, upload_enrollment_report,
upload_exec_summary_report, upload_exec_summary_report,
upload_may_enroll_csv, upload_may_enroll_csv,
upload_students_csv upload_students_csv,
) )
from lms.djangoapps.instructor_task.tasks_helper.grades import ( from lms.djangoapps.instructor_task.tasks_helper.grades import (
ENROLLED_IN_COURSE, ENROLLED_IN_COURSE,
NOT_ENROLLED_IN_COURSE, NOT_ENROLLED_IN_COURSE,
CourseGradeReport, CourseGradeReport,
ProblemGradeReport, ProblemGradeReport,
ProblemResponses ProblemResponses,
) )
from lms.djangoapps.instructor_task.tasks_helper.misc import ( from lms.djangoapps.instructor_task.tasks_helper.misc import (
cohort_students_and_upload, cohort_students_and_upload,
upload_course_survey_report, upload_course_survey_report,
upload_ora2_data upload_ora2_data,
) )
from lms.djangoapps.instructor_task.tests.test_base import ( from lms.djangoapps.instructor_task.tests.test_base import (
InstructorTaskCourseTestCase, InstructorTaskCourseTestCase,
InstructorTaskModuleTestCase, InstructorTaskModuleTestCase,
TestReportMixin TestReportMixin,
) )
from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
@@ -481,6 +486,7 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
Tests that generation of CSV files listing student answers to a Tests that generation of CSV files listing student answers to a
given problem works. given problem works.
""" """
def setUp(self): def setUp(self):
super(TestProblemResponsesReport, self).setUp() super(TestProblemResponsesReport, self).setUp()
self.initialize_course() self.initialize_course()
@@ -510,7 +516,7 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
student = self.create_student('student{}'.format(ctr)) student = self.create_student('student{}'.format(ctr))
self.submit_student_answer(student.username, u'Problem1', ['Option 1']) self.submit_student_answer(student.username, u'Problem1', ['Option 1'])
student_data = ProblemResponses._build_student_data( student_data, _ = ProblemResponses._build_student_data(
user_id=self.instructor.id, user_id=self.instructor.id,
course_key=self.course.id, course_key=self.course.id,
usage_key_str=str(self.course.location), usage_key_str=str(self.course.location),
@@ -530,7 +536,7 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
problem = self.define_option_problem(u'Problem1') problem = self.define_option_problem(u'Problem1')
self.submit_student_answer(self.student.username, u'Problem1', ['Option 1']) self.submit_student_answer(self.student.username, u'Problem1', ['Option 1'])
with self._remove_capa_report_generator(): with self._remove_capa_report_generator():
student_data = ProblemResponses._build_student_data( student_data, _ = ProblemResponses._build_student_data(
user_id=self.instructor.id, user_id=self.instructor.id,
course_key=self.course.id, course_key=self.course.id,
usage_key_str=str(problem.location), usage_key_str=str(problem.location),
@@ -552,11 +558,12 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
``generate_report_data`` method works as expected. ``generate_report_data`` method works as expected.
""" """
self.define_option_problem(u'Problem1') self.define_option_problem(u'Problem1')
state = {'some': 'state'} self.submit_student_answer(self.student.username, u'Problem1', ['Option 1'])
state = {'some': 'state', 'more': 'state!'}
mock_generate_report_data.return_value = iter([ mock_generate_report_data.return_value = iter([
('student', state), ('student', state),
]) ])
student_data = ProblemResponses._build_student_data( student_data, _ = ProblemResponses._build_student_data(
user_id=self.instructor.id, user_id=self.instructor.id,
course_key=self.course.id, course_key=self.course.id,
usage_key_str=str(self.course.location), usage_key_str=str(self.course.location),
@@ -567,7 +574,8 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
'location': 'test_course > Section > Subsection > Problem1', 'location': 'test_course > Section > Subsection > Problem1',
'block_key': 'i4x://edx/1.23x/problem/Problem1', 'block_key': 'i4x://edx/1.23x/problem/Problem1',
'title': 'Problem1', 'title': 'Problem1',
'state': state, 'some': 'state',
'more': 'state!',
}, student_data[0]) }, student_data[0])
def test_build_student_data_for_block_with_real_generate_report_data(self): def test_build_student_data_for_block_with_real_generate_report_data(self):
@@ -577,7 +585,7 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
""" """
self.define_option_problem(u'Problem1') self.define_option_problem(u'Problem1')
self.submit_student_answer(self.student.username, u'Problem1', ['Option 1']) self.submit_student_answer(self.student.username, u'Problem1', ['Option 1'])
student_data = ProblemResponses._build_student_data( student_data, _ = ProblemResponses._build_student_data(
user_id=self.instructor.id, user_id=self.instructor.id,
course_key=self.course.id, course_key=self.course.id,
usage_key_str=str(self.course.location), usage_key_str=str(self.course.location),
@@ -588,12 +596,11 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
'location': 'test_course > Section > Subsection > Problem1', 'location': 'test_course > Section > Subsection > Problem1',
'block_key': 'i4x://edx/1.23x/problem/Problem1', 'block_key': 'i4x://edx/1.23x/problem/Problem1',
'title': 'Problem1', 'title': 'Problem1',
'state': { 'Answer ID': 'i4x-edx-1_23x-problem-Problem1_2_1',
'Answer ID': 'i4x-edx-1_23x-problem-Problem1_2_1', 'Answer': 'Option 1',
'Answer': 'Option 1', 'Question': u'The correct answer is Option 1',
'Question': u'The correct answer is Option 1'
},
}, student_data[0]) }, student_data[0])
self.assertIn('state', student_data[0])
@patch('lms.djangoapps.instructor_task.tasks_helper.grades.list_problem_responses') @patch('lms.djangoapps.instructor_task.tasks_helper.grades.list_problem_responses')
@patch('xmodule.capa_module.CapaDescriptor.generate_report_data', create=True) @patch('xmodule.capa_module.CapaDescriptor.generate_report_data', create=True)
@@ -624,11 +631,14 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskModuleTestCase):
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'): with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
with patch('lms.djangoapps.instructor_task.tasks_helper.grades' with patch('lms.djangoapps.instructor_task.tasks_helper.grades'
'.ProblemResponses._build_student_data') as mock_build_student_data: '.ProblemResponses._build_student_data') as mock_build_student_data:
mock_build_student_data.return_value = [ mock_build_student_data.return_value = (
{'username': 'user0', 'state': u'state0'}, [
{'username': 'user1', 'state': u'state1'}, {'username': 'user0', 'state': u'state0'},
{'username': 'user2', 'state': u'state2'}, {'username': 'user1', 'state': u'state1'},
] {'username': 'user2', 'state': u'state2'},
],
['username', 'state']
)
result = ProblemResponses.generate( result = ProblemResponses.generate(
None, None, self.course.id, task_input, 'calculated' None, None, self.course.id, task_input, 'calculated'
) )
@@ -1620,7 +1630,10 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
self.cohort_2 = CohortFactory(course_id=self.course.id, name='Cohort 2') self.cohort_2 = CohortFactory(course_id=self.course.id, name='Cohort 2')
self.student_1 = self.create_student(username=u'student_1\xec', email='student_1@example.com') self.student_1 = self.create_student(username=u'student_1\xec', email='student_1@example.com')
self.student_2 = self.create_student(username='student_2', email='student_2@example.com') self.student_2 = self.create_student(username='student_2', email='student_2@example.com')
self.csv_header_row = ['Cohort Name', 'Exists', 'Learners Added', 'Learners Not Found', 'Invalid Email Addresses', 'Preassigned Learners'] self.csv_header_row = [
'Cohort Name', 'Exists', 'Learners Added', 'Learners Not Found',
'Invalid Email Addresses', 'Preassigned Learners',
]
def _cohort_students_and_upload(self, csv_data): def _cohort_students_and_upload(self, csv_data):
""" """