From cf524906e0a3082f4b54424b3bc5c1fb92e787b6 Mon Sep 17 00:00:00 2001 From: Adam Palay <adam@edx.org> Date: Tue, 12 Nov 2013 13:18:27 -0500 Subject: [PATCH] more granular transactions in grading [LMS-1480] remove field_data_cache from grades.grade and grades.progress_summary cleans grading code by adding wrappers --- lms/djangoapps/courseware/grades.py | 183 ++++++++++-------- .../tests/test_submitting_problems.py | 26 +-- lms/djangoapps/courseware/views.py | 67 ++++--- 3 files changed, 154 insertions(+), 122 deletions(-) diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 438466c0eaa..4cc0f909431 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -4,6 +4,7 @@ from collections import defaultdict import random import logging +from contextlib import contextmanager from collections import defaultdict from django.conf import settings from django.contrib.auth.models import User @@ -126,8 +127,20 @@ def answer_distributions(request, course): return counts -def grade(student, request, course, field_data_cache=None, keep_raw_scores=False): +@transaction.commit_manually +def grade(student, request, course, keep_raw_scores=False): """ + Wraps "_grade" with the manual_transaction context manager just in case + there are unanticipated errors. + """ + with manual_transaction(): + return _grade(student, request, course, keep_raw_scores) + + +def _grade(student, request, course, keep_raw_scores): + """ + Unwrapped version of "grade" + This grades a student as quickly as possible. It returns the output from the course grader, augmented with the final letter grade. The keys in the output are: @@ -137,20 +150,17 @@ def grade(student, request, course, field_data_cache=None, keep_raw_scores=False - grade : A final letter grade. - percent : The final percent for the class (rounded up). - section_breakdown : A breakdown of each section that makes - up the grade. (For display) + up the grade. (For display) - grade_breakdown : A breakdown of the major components that - make up the final grade. (For display) - - keep_raw_scores : if True, then value for key 'raw_scores' contains scores for every graded module + make up the final grade. (For display) + - keep_raw_scores : if True, then value for key 'raw_scores' contains scores + for every graded module More information on the format is in the docstring for CourseGrader. """ - grading_context = course.grading_context raw_scores = [] - if field_data_cache is None: - field_data_cache = FieldDataCache(grading_context['all_descriptors'], course.id, student) - totaled_scores = {} # This next complicated loop is just to collect the totaled_scores, which is # passed to the grader @@ -160,26 +170,22 @@ def grade(student, request, course, field_data_cache=None, keep_raw_scores=False section_descriptor = section['section_descriptor'] section_name = section_descriptor.display_name_with_default - should_grade_section = False - # If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0% - for moduledescriptor in section['xmoduledescriptors']: - # some problems have state that is updated independently of interaction - # with the LMS, so they need to always be scored. (E.g. foldit.) - if moduledescriptor.always_recalculate_grades: - should_grade_section = True - break + # some problems have state that is updated independently of interaction + # with the LMS, so they need to always be scored. (E.g. foldit., + # combinedopenended) + should_grade_section = any( + descriptor.always_recalculate_grades for descriptor in section['xmoduledescriptors'] + ) - # Create a fake key to pull out a StudentModule object from the FieldDataCache - - key = DjangoKeyValueStore.Key( - Scope.user_state, - student.id, - moduledescriptor.location, - None - ) - if field_data_cache.find(key): - should_grade_section = True - break + # If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0% + if not should_grade_section: + with manual_transaction(): + should_grade_section = StudentModule.objects.filter( + student=student, + module_state_key__in=[ + descriptor.location for descriptor in section['xmoduledescriptors'] + ] + ).exists() if should_grade_section: scores = [] @@ -188,11 +194,13 @@ def grade(student, request, course, field_data_cache=None, keep_raw_scores=False '''creates an XModule instance given a descriptor''' # TODO: We need the request to pass into here. If we could forego that, our arguments # would be simpler + with manual_transaction(): + field_data_cache = FieldDataCache([descriptor], course.id, student) return get_module_for_descriptor(student, request, descriptor, field_data_cache, course.id) for module_descriptor in yield_dynamic_descriptor_descendents(section_descriptor, create_module): - (correct, total) = get_score(course.id, student, module_descriptor, create_module, field_data_cache) + (correct, total) = get_score(course.id, student, module_descriptor, create_module) if correct is None and total is None: continue @@ -261,11 +269,23 @@ def grade_for_percentage(grade_cutoffs, percentage): return letter_grade +@transaction.commit_manually +def progress_summary(student, request, course): + """ + Wraps "_progress_summary" with the manual_transaction context manager just + in case there are unanticipated errors. + """ + with manual_transaction(): + return _progress_summary(student, request, course) + + # TODO: This method is not very good. It was written in the old course style and # then converted over and performance is not good. Once the progress page is redesigned # to not have the progress summary this method should be deleted (so it won't be copied). -def progress_summary(student, request, course, field_data_cache): +def _progress_summary(student, request, course): """ + Unwrapped version of "progress_summary". + This pulls a summary of all problems in the course. Returns @@ -278,20 +298,21 @@ def progress_summary(student, request, course, field_data_cache): Arguments: student: A User object for the student to grade course: A Descriptor containing the course to grade - field_data_cache: A FieldDataCache initialized with all - instance_modules for the student If the student does not have access to load the course module, this function will return None. """ - - # TODO: We need the request to pass into here. If we could forego that, our arguments - # would be simpler - course_module = get_module_for_descriptor(student, request, course, field_data_cache, course.id) - if not course_module: - # This student must not have access to the course. - return None + with manual_transaction(): + field_data_cache = FieldDataCache.cache_for_descriptor_descendents( + course.id, student, course, depth=None + ) + # TODO: We need the request to pass into here. If we could + # forego that, our arguments would be simpler + course_module = get_module_for_descriptor(student, request, course, field_data_cache, course.id) + if not course_module: + # This student must not have access to the course. + return None chapters = [] # Don't include chapters that aren't displayable (e.g. due to error) @@ -301,50 +322,52 @@ def progress_summary(student, request, course, field_data_cache): continue sections = [] + for section_module in chapter_module.get_display_items(): # Skip if the section is hidden - if section_module.hide_from_toc: - continue - - # Same for sections - graded = section_module.graded - scores = [] - - module_creator = section_module.xmodule_runtime.get_module + with manual_transaction(): + if section_module.hide_from_toc: + continue - for module_descriptor in yield_dynamic_descriptor_descendents(section_module, module_creator): + graded = section_module.graded + scores = [] - course_id = course.id - (correct, total) = get_score(course_id, student, module_descriptor, module_creator, field_data_cache) - if correct is None and total is None: - continue + module_creator = section_module.xmodule_runtime.get_module - scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default)) + for module_descriptor in yield_dynamic_descriptor_descendents(section_module, module_creator): - scores.reverse() - section_total, _ = graders.aggregate_scores( - scores, section_module.display_name_with_default) + course_id = course.id + (correct, total) = get_score(course_id, student, module_descriptor, module_creator) + if correct is None and total is None: + continue - module_format = section_module.format if section_module.format is not None else '' - sections.append({ - 'display_name': section_module.display_name_with_default, - 'url_name': section_module.url_name, - 'scores': scores, - 'section_total': section_total, - 'format': module_format, - 'due': section_module.due, - 'graded': graded, - }) + scores.append(Score(correct, total, graded, module_descriptor.display_name_with_default)) - chapters.append({'course': course.display_name_with_default, - 'display_name': chapter_module.display_name_with_default, - 'url_name': chapter_module.url_name, - 'sections': sections}) + scores.reverse() + section_total, _ = graders.aggregate_scores( + scores, section_module.display_name_with_default) + + module_format = section_module.format if section_module.format is not None else '' + sections.append({ + 'display_name': section_module.display_name_with_default, + 'url_name': section_module.url_name, + 'scores': scores, + 'section_total': section_total, + 'format': module_format, + 'due': section_module.due, + 'graded': graded, + }) + + chapters.append({ + 'course': course.display_name_with_default, + 'display_name': chapter_module.display_name_with_default, + 'url_name': chapter_module.url_name, + 'sections': sections + }) return chapters - -def get_score(course_id, user, problem_descriptor, module_creator, field_data_cache): +def get_score(course_id, user, problem_descriptor, module_creator): """ Return the score for a user on a problem, as a tuple (correct, total). e.g. (5,7) if you got 5 out of 7 points. @@ -377,15 +400,14 @@ def get_score(course_id, user, problem_descriptor, module_creator, field_data_ca # These are not problems, and do not have a score return (None, None) - # Create a fake KeyValueStore key to pull out the StudentModule - key = DjangoKeyValueStore.Key( - Scope.user_state, - user.id, - problem_descriptor.location, - None - ) - - student_module = field_data_cache.find(key) + try: + student_module = StudentModule.objects.get( + student=user, + course_id=course_id, + module_state_key=problem_descriptor.location + ) + except StudentModule.DoesNotExist: + student_module = None if student_module is not None and student_module.max_grade is not None: correct = student_module.grade if student_module.grade is not None else 0 @@ -478,4 +500,3 @@ def iterate_grades_for(course_id, students): exc.message ) yield student, {}, exc.message - diff --git a/lms/djangoapps/courseware/tests/test_submitting_problems.py b/lms/djangoapps/courseware/tests/test_submitting_problems.py index 0736e08baf3..854750f8d1e 100644 --- a/lms/djangoapps/courseware/tests/test_submitting_problems.py +++ b/lms/djangoapps/courseware/tests/test_submitting_problems.py @@ -236,14 +236,11 @@ class TestCourseGrader(TestSubmittingProblems): make up the final grade. (For display) """ - field_data_cache = FieldDataCache.cache_for_descriptor_descendents( - self.course.id, self.student_user, self.course) - - fake_request = self.factory.get(reverse('progress', - kwargs={'course_id': self.course.id})) + fake_request = self.factory.get( + reverse('progress', kwargs={'course_id': self.course.id}) + ) - return grades.grade(self.student_user, fake_request, - self.course, field_data_cache) + return grades.grade(self.student_user, fake_request, self.course) def get_progress_summary(self): """ @@ -257,16 +254,13 @@ class TestCourseGrader(TestSubmittingProblems): etc. """ - field_data_cache = FieldDataCache.cache_for_descriptor_descendents( - self.course.id, self.student_user, self.course) - - fake_request = self.factory.get(reverse('progress', - kwargs={'course_id': self.course.id})) + fake_request = self.factory.get( + reverse('progress', kwargs={'course_id': self.course.id}) + ) - progress_summary = grades.progress_summary(self.student_user, - fake_request, - self.course, - field_data_cache) + progress_summary = grades.progress_summary( + self.student_user, fake_request, self.course + ) return progress_summary def check_grade_percent(self, percent): diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 819f65fca69..6646ea1e637 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -14,6 +14,7 @@ from django.shortcuts import redirect from mitxmako.shortcuts import render_to_response, render_to_string from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control +from django.db import transaction from markupsafe import escape from courseware import grades @@ -643,8 +644,9 @@ def mktg_course_about(request, course_id): except (ValueError, Http404) as e: # if a course does not exist yet, display a coming # soon button - return render_to_response('courseware/mktg_coming_soon.html', - {'course_id': course_id}) + return render_to_response( + 'courseware/mktg_coming_soon.html', {'course_id': course_id} + ) registered = registered_for_course(course, request.user) @@ -659,21 +661,36 @@ def mktg_course_about(request, course_id): settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION')) course_modes = CourseMode.modes_for_course(course.id) - return render_to_response('courseware/mktg_course_about.html', - { - 'course': course, - 'registered': registered, - 'allow_registration': allow_registration, - 'course_target': course_target, - 'show_courseware_link': show_courseware_link, - 'course_modes': course_modes, - }) + return render_to_response( + 'courseware/mktg_course_about.html', + { + 'course': course, + 'registered': registered, + 'allow_registration': allow_registration, + 'course_target': course_target, + 'show_courseware_link': show_courseware_link, + 'course_modes': course_modes, + } + ) @login_required @cache_control(no_cache=True, no_store=True, must_revalidate=True) +@transaction.commit_manually def progress(request, course_id, student_id=None): - """ User progress. We show the grade bar and every problem score. + """ + Wraps "_progress" with the manual_transaction context manager just in case + there are unanticipated errors. + """ + with grades.manual_transaction(): + return _progress(request, course_id, student_id) + + +def _progress(request, course_id, student_id): + """ + Unwrapped version of "progress". + + User progress. We show the grade bar and every problem score. Course staff are allowed to see the progress of students in their class. """ @@ -696,26 +713,26 @@ def progress(request, course_id, student_id=None): # additional DB lookup (this kills the Progress page in particular). student = User.objects.prefetch_related("groups").get(id=student.id) - field_data_cache = FieldDataCache.cache_for_descriptor_descendents( - course_id, student, course, depth=None) + courseware_summary = grades.progress_summary(student, request, course) - courseware_summary = grades.progress_summary(student, request, course, - field_data_cache) - grade_summary = grades.grade(student, request, course, field_data_cache) + grade_summary = grades.grade(student, request, course) if courseware_summary is None: #This means the student didn't have access to the course (which the instructor requested) raise Http404 - context = {'course': course, - 'courseware_summary': courseware_summary, - 'grade_summary': grade_summary, - 'staff_access': staff_access, - 'student': student, - } - context.update() + context = { + 'course': course, + 'courseware_summary': courseware_summary, + 'grade_summary': grade_summary, + 'staff_access': staff_access, + 'student': student, + } + + with grades.manual_transaction(): + response = render_to_response('courseware/progress.html', context) - return render_to_response('courseware/progress.html', context) + return response @login_required -- GitLab