diff --git a/common/djangoapps/util/decorators.py b/common/djangoapps/util/decorators.py new file mode 100644 index 0000000000..81ab747a3e --- /dev/null +++ b/common/djangoapps/util/decorators.py @@ -0,0 +1,36 @@ + + +def lazyproperty(fn): + """ + Use this decorator for lazy generation of properties that + are expensive to compute. From http://stackoverflow.com/a/3013910/86828 + + + Example: + class Test(object): + + @lazyproperty + def a(self): + print 'generating "a"' + return range(5) + + Interactive Session: + >>> t = Test() + >>> t.__dict__ + {} + >>> t.a + generating "a" + [0, 1, 2, 3, 4] + >>> t.__dict__ + {'_lazy_a': [0, 1, 2, 3, 4]} + >>> t.a + [0, 1, 2, 3, 4] + """ + + attr_name = '_lazy_' + fn.__name__ + @property + def _lazyprop(self): + if not hasattr(self, attr_name): + setattr(self, attr_name, fn(self)) + return getattr(self, attr_name) + return _lazyprop \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index acdc574220..6c5d875fbb 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -3,6 +3,7 @@ import time import dateutil.parser import logging +from util.decorators import lazyproperty from xmodule.graders import load_grading_policy from xmodule.modulestore import Location from xmodule.seq_module import SequenceDescriptor, SequenceModule @@ -16,9 +17,6 @@ class CourseDescriptor(SequenceDescriptor): def __init__(self, system, definition=None, **kwargs): super(CourseDescriptor, self).__init__(system, definition, **kwargs) - - self._grader = None - self._grade_cutoffs = None msg = None try: @@ -42,29 +40,82 @@ class CourseDescriptor(SequenceDescriptor): @property def grader(self): - self.__load_grading_policy() - return self._grader + return self.__grading_policy['GRADER'] @property def grade_cutoffs(self): - self.__load_grading_policy() - return self._grade_cutoffs + return self.__grading_policy['GRADE_CUTOFFS'] + + @lazyproperty + def __grading_policy(self): + policy_string = "" + + try: + with self.system.resources_fs.open("grading_policy.json") as grading_policy_file: + policy_string = grading_policy_file.read() + except (IOError, ResourceNotFoundError): + log.warning("Unable to load course settings file from grading_policy.json in course " + self.id) + + grading_policy = load_grading_policy(policy_string) + + return grading_policy - def __load_grading_policy(self): - if not self._grader or not self._grade_cutoffs: - policy_string = "" + @lazyproperty + def grading_context(self): + """ + This returns a dictionary with keys necessary for quickly grading + a student. They are used by grades.grade() + + The grading context has two keys: + graded_sections - This contains the sections that are graded, as + well as all possible children modules that can effect the + grading. This allows some sections to be skipped if the student + hasn't seen any part of it. - try: - with self.system.resources_fs.open("grading_policy.json") as grading_policy_file: - policy_string = grading_policy_file.read() - except (IOError, ResourceNotFoundError): - log.warning("Unable to load course settings file from grading_policy.json in course " + self.id) + The format is a dictionary keyed by section-type. The values are + arrays of dictionaries containing + "section_descriptor" : The section descriptor + "xmoduledescriptors" : An array of xmoduledescriptors that + could possibly be in the section, for any student + + all_descriptors - This contains a list of all xmodules that can + effect grading a student. This is used to efficiently fetch + all the xmodule state for a StudentModuleCache without walking + the descriptor tree again. - grading_policy = load_grading_policy(policy_string) - - self._grader = grading_policy['GRADER'] - self._grade_cutoffs = grading_policy['GRADE_CUTOFFS'] + + """ + + all_descriptors = [] + graded_sections = {} + + def yield_descriptor_descendents(module_descriptor): + for child in module_descriptor.get_children(): + yield child + for module_descriptor in yield_descriptor_descendents(child): + yield module_descriptor + + for c in self.get_children(): + sections = [] + for s in c.get_children(): + if s.metadata.get('graded', False): + + xmoduledescriptors = [] + for module in yield_descriptor_descendents(s): + # TODO: Only include modules that have a score here + xmoduledescriptors.append(module) + + section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : xmoduledescriptors} + + section_format = s.metadata.get('format', "") + graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description] + + all_descriptors.extend(xmoduledescriptors) + all_descriptors.append(s) + + return { 'graded_sections' : graded_sections, + 'all_descriptors' : all_descriptors,} @staticmethod diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index e736edef58..75e7fb35f3 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -3,6 +3,7 @@ import logging from django.conf import settings +from models import StudentModuleCache from module_render import get_module, get_instance_module from xmodule import graders from xmodule.graders import Score @@ -10,54 +11,28 @@ from models import StudentModule _log = logging.getLogger("mitx.courseware") -def get_graded_sections(course_descriptor): - """ - Arguments: - course_descriptor: a CourseDescriptor object for the course to be graded - - Returns: - A dictionary keyed by section-type. The values are arrays of dictionaries containing - "section_descriptor" : The section descriptor - "xmoduledescriptors" : An array of xmoduledescriptors that could possibly be in the section, for any student - """ - all_descriptors = [] - - graded_sections = {} - for c in course_descriptor.get_children(): - sections = [] - for s in c.get_children(): - if s.metadata.get('graded', False): - - xmoduledescriptors = [] - for module in yield_descriptor_descendents(s): - # TODO: Only include modules that have a score here - xmoduledescriptors.append(module) - - section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : xmoduledescriptors} - - section_format = s.metadata.get('format', "") - graded_sections[ section_format ] = graded_sections.get( section_format, [] ) + [section_description] - - all_descriptors.extend(xmoduledescriptors) - all_descriptors.append(s) - - return graded_sections, all_descriptors - -def yield_descriptor_descendents(module_descriptor): - for child in module_descriptor.get_children(): - yield child - for module_descriptor in yield_descriptor_descendents(child): - yield module_descriptor - def yield_module_descendents(module): for child in module.get_display_items(): yield child for module in yield_module_descendents(child): yield module -def fast_grade(student, request, course_graded_sections, grader, student_module_cache): +def grade(student, request, course, student_module_cache=None): + """ + This grades a student as quickly as possible. It reutns the + output from the course grader. More information on the format + is in the docstring for CourseGrader. + """ + + grading_context = course.grading_context + + if student_module_cache == None: + student_module_cache = StudentModuleCache(student, descriptors=grading_context['all_descriptors']) + totaled_scores = {} - for section_format, sections in course_graded_sections.iteritems(): + # This next complicated loop is just to collect the totaled_scores, which is + # passed to the grader + for section_format, sections in grading_context['graded_sections'].iteritems(): format_scores = [] for section in sections: section_descriptor = section['section_descriptor'] @@ -72,6 +47,8 @@ def fast_grade(student, request, course_graded_sections, grader, student_module_ if should_grade_section: scores = [] + # TODO: We need the request to pass into here. If we could forgo that, our arguments + # would be simpler section_module = get_module(student, request, section_descriptor.location, student_module_cache) # TODO: We may be able to speed this up by only getting a list of children IDs from section_module @@ -106,11 +83,11 @@ def fast_grade(student, request, course_graded_sections, grader, student_module_ totaled_scores[section_format] = format_scores - grade_summary = grade_summary = grader.grade(totaled_scores) + grade_summary = course.grader.grade(totaled_scores) return grade_summary -def grade_sheet(student, course, grader, student_module_cache): +def progress_summary(student, course, grader, student_module_cache): """ This pulls a summary of all problems in the course. diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 44c6db73e4..e60df34b95 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -77,21 +77,17 @@ def gradebook(request, course_id): if 'course_admin' not in user_groups(request.user): raise Http404 course = check_course(course_id) - - sections, all_descriptors = grades.get_graded_sections(course) - + student_objects = User.objects.all()[:100] student_info = [] #TODO: Only select students who are in the course - for student in student_objects: - student_module_cache = StudentModuleCache(student, descriptors=all_descriptors) - + for student in student_objects: student_info.append({ 'username': student.username, 'id': student.id, 'email': student.email, - 'grade_summary': grades.fast_grade(student, request, sections, course.grader, student_module_cache), + 'grade_summary': grades.grade(student, request, course), 'realname': UserProfile.objects.get(user=student).name }) @@ -117,9 +113,8 @@ def profile(request, course_id, student_id=None): student_module_cache = StudentModuleCache(request.user, course) course_module = get_module(request.user, request, course.location, student_module_cache) - courseware_summary = grades.grade_sheet(student, course_module, course.grader, student_module_cache) - sections, _ = grades.get_graded_sections(course) - grade_summary = grades.fast_grade(request.user, request, sections, course.grader, student_module_cache) + courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache) + grade_summary = grades.grade(request.user, request, course, student_module_cache) context = {'name': user_info.name, 'username': student.username,