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/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 7ce76def32..a384edf234 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -130,7 +130,7 @@ class CapaModule(XModule): if weight_string: self.weight = float(weight_string) else: - self.weight = 1 + self.weight = None if self.rerandomize == 'never': seed = 1 diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index acdc574220..7f4c204f45 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,79 @@ 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 affect 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): + # TODO: Only include modules that have a score here + xmoduledescriptors = [child for child in yield_descriptor_descendents(s)] + + 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/certificates/views.py b/lms/djangoapps/certificates/views.py index 6341133c52..472215ab5c 100644 --- a/lms/djangoapps/certificates/views.py +++ b/lms/djangoapps/certificates/views.py @@ -52,7 +52,7 @@ def certificate_request(request): return return_error(survey_response['error']) grade = None - student_gradesheet = grades.grade_sheet(request.user) + student_gradesheet = grades.grade(request.user, request, course) grade = student_gradesheet['grade'] if not grade: @@ -65,7 +65,7 @@ def certificate_request(request): else: #This is not a POST, we should render the page with the form - grade_sheet = grades.grade_sheet(request.user) + student_gradesheet = grades.grade(request.user, request, course) certificate_state = certificate_state_for_student(request.user, grade_sheet['grade']) if certificate_state['state'] != "requestable": diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 6e7a0ce102..925b8429c6 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -3,26 +3,135 @@ 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 from models import StudentModule -_log = logging.getLogger("mitx.courseware") +log = logging.getLogger("mitx.courseware") - -def grade_sheet(student, course, grader, student_module_cache): +def yield_module_descendents(module): + for child in module.get_display_items(): + yield child + for module in yield_module_descendents(child): + yield module + +def grade(student, request, course, student_module_cache=None): """ - This pulls a summary of all problems in the course. It returns a dictionary - with two datastructures: + This grades a student as quickly as possible. It retuns the + output from the course grader, augmented with the final letter + grade. The keys in the output are: + + - 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) + - grade_breakdown : A breakdown of the major components that + make up the final grade. (For display) + + 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, grading_context['all_descriptors']) + + totaled_scores = {} + # 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'] + section_name = section_descriptor.metadata.get('display_name') + + 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']: + if student_module_cache.lookup(moduledescriptor.category, moduledescriptor.location.url() ): + should_grade_section = True + break + + 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 + # Then, we may not need to instatiate any problems if they are already in the database + for module in yield_module_descendents(section_module): + (correct, total) = get_score(student, module, student_module_cache) + if correct is None and total is None: + continue + + if settings.GENERATE_PROFILE_SCORES: + if total > 1: + correct = random.randrange(max(total - 2, 1), total + 1) + else: + correct = total + + graded = module.metadata.get("graded", False) + if not total > 0: + #We simply cannot grade a problem that is 12/0, because we might need it as a percentage + graded = False + + scores.append(Score(correct, total, graded, module.metadata.get('display_name'))) + + section_total, graded_total = graders.aggregate_scores(scores, section_name) + else: + section_total = Score(0.0, 1.0, False, section_name) + graded_total = Score(0.0, 1.0, True, section_name) + + #Add the graded total to totaled_scores + if graded_total.possible > 0: + format_scores.append(graded_total) + else: + log.exception("Unable to grade a section with a total possible score of zero. " + str(section_descriptor.id)) + + totaled_scores[section_format] = format_scores + + grade_summary = course.grader.grade(totaled_scores) + + # We round the grade here, to make sure that the grade is an whole percentage and + # doesn't get displayed differently than it gets grades + grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100 + + letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent']) + grade_summary['grade'] = letter_grade + + return grade_summary - - courseware_summary is a summary of all sections with problems in the - course. It is organized as an array of chapters, each containing an array of - sections, each containing an array of scores. This contains information for - graded and ungraded problems, and is good for displaying a course summary - with due dates, etc. +def grade_for_percentage(grade_cutoffs, percentage): + """ + Returns a letter grade 'A' 'B' 'C' or None. + + Arguments + - grade_cutoffs is a dictionary mapping a grade to the lowest + possible percentage to earn that grade. + - percentage is the final percent across all problems in a course + """ + + letter_grade = None + for possible_grade in ['A', 'B', 'C']: + if percentage >= grade_cutoffs[possible_grade]: + letter_grade = possible_grade + break + + return letter_grade - - grade_summary is the output from the course grader. More information on - the format is in the docstring for CourseGrader. +def progress_summary(student, course, grader, student_module_cache): + """ + This pulls a summary of all problems in the course. + + Returns + - courseware_summary is a summary of all sections with problems in the course. + It is organized as an array of chapters, each containing an array of sections, + each containing an array of scores. This contains information for graded and + ungraded problems, and is good for displaying a course summary with due dates, + etc. Arguments: student: A User object for the student to grade @@ -30,49 +139,24 @@ def grade_sheet(student, course, grader, student_module_cache): student_module_cache: A StudentModuleCache initialized with all instance_modules for the student """ - totaled_scores = {} chapters = [] for c in course.get_children(): sections = [] for s in c.get_children(): - def yield_descendents(module): - yield module - for child in module.get_display_items(): - for module in yield_descendents(child): - yield module - graded = s.metadata.get('graded', False) scores = [] - for module in yield_descendents(s): + for module in yield_module_descendents(s): (correct, total) = get_score(student, module, student_module_cache) - if correct is None and total is None: continue - if settings.GENERATE_PROFILE_SCORES: - if total > 1: - correct = random.randrange(max(total - 2, 1), total + 1) - else: - correct = total - - if not total > 0: - #We simply cannot grade a problem that is 12/0, because we - #might need it as a percentage - graded = False - - scores.append(Score(correct, total, graded, - module.metadata.get('display_name'))) + scores.append(Score(correct, total, graded, + module.metadata.get('display_name'))) section_total, graded_total = graders.aggregate_scores( scores, s.metadata.get('display_name')) - #Add the graded total to totaled_scores format = s.metadata.get('format', "") - if format and graded_total.possible > 0: - format_scores = totaled_scores.get(format, []) - format_scores.append(graded_total) - totaled_scores[format] = format_scores - sections.append({ 'display_name': s.display_name, 'url_name': s.url_name, @@ -88,13 +172,10 @@ def grade_sheet(student, course, grader, student_module_cache): 'url_name': c.url_name, 'sections': sections}) - grade_summary = grader.grade(totaled_scores) - - return {'courseware_summary': chapters, - 'grade_summary': grade_summary} + return chapters -def get_score(user, problem, cache): +def get_score(user, problem, student_module_cache): """ Return the score for a user on a problem @@ -105,17 +186,18 @@ def get_score(user, problem, cache): correct = 0.0 # If the ID is not in the cache, add the item - instance_module = cache.lookup(problem.category, problem.id) - if instance_module is None: - instance_module = StudentModule(module_type=problem.category, - module_state_key=problem.id, - student=user, - state=None, - grade=0, - max_grade=problem.max_score(), - done='i') - cache.append(instance_module) - instance_module.save() + instance_module = get_instance_module(user, problem, student_module_cache) + # instance_module = student_module_cache.lookup(problem.category, problem.id) + # if instance_module is None: + # instance_module = StudentModule(module_type=problem.category, + # module_state_key=problem.id, + # student=user, + # state=None, + # grade=0, + # max_grade=problem.max_score(), + # done='i') + # cache.append(instance_module) + # instance_module.save() # If this problem is ungraded/ungradable, bail if instance_module.max_grade is None: @@ -126,8 +208,11 @@ def get_score(user, problem, cache): if correct is not None and total is not None: #Now we re-weight the problem, if specified - weight = getattr(problem, 'weight', 1) - if weight != 1: + weight = getattr(problem, 'weight', None) + if weight is not None: + if total == 0: + log.exception("Cannot reweight a problem with zero weight. Problem: " + str(instance_module)) + return (correct, total) correct = correct * weight / total total = weight diff --git a/lms/djangoapps/courseware/management/commands/check_course.py b/lms/djangoapps/courseware/management/commands/check_course.py index 6ccd6d5fe7..80f4b57983 100644 --- a/lms/djangoapps/courseware/management/commands/check_course.py +++ b/lms/djangoapps/courseware/management/commands/check_course.py @@ -78,8 +78,8 @@ class Command(BaseCommand): # TODO (cpennington): Get coursename in a legitimate way course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012' - student_module_cache = StudentModuleCache(sample_user, modulestore().get_item(course_location)) - (course, _, _, _) = get_module(sample_user, None, course_location, student_module_cache) + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(sample_user, modulestore().get_item(course_location)) + course = get_module(sample_user, None, course_location, student_module_cache) to_run = [ #TODO (vshnayder) : make check_rendering work (use module_render.py), diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 3b0ca7fdcf..631a750643 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -67,17 +67,19 @@ class StudentModuleCache(object): """ A cache of StudentModules for a specific student """ - def __init__(self, user, descriptor, depth=None): + def __init__(self, user, descriptors): ''' Find any StudentModule objects that are needed by any child modules of the - supplied descriptor. Avoids making multiple queries to the database - - descriptor: An XModuleDescriptor - depth is the number of levels of descendent modules to load StudentModules for, in addition to - the supplied descriptor. If depth is None, load all descendent StudentModules + supplied descriptor, or caches only the StudentModule objects specifically + for every descriptor in descriptors. Avoids making multiple queries to the + database. + + Arguments + user: The user for which to fetch maching StudentModules + descriptors: An array of XModuleDescriptors. ''' if user.is_authenticated(): - module_ids = self._get_module_state_keys(descriptor, depth) + module_ids = self._get_module_state_keys(descriptors) # This works around a limitation in sqlite3 on the number of parameters # that can be put into a single query @@ -91,27 +93,52 @@ class StudentModuleCache(object): else: self.cache = [] - - def _get_module_state_keys(self, descriptor, depth): - ''' - Get a list of the state_keys needed for StudentModules - required for this module descriptor - + + + @classmethod + def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True): + """ descriptor: An XModuleDescriptor depth is the number of levels of descendent modules to load StudentModules for, in addition to the supplied descriptor. If depth is None, load all descendent StudentModules + descriptor_filter is a function that accepts a descriptor and return wether the StudentModule + should be cached + """ + + def get_child_descriptors(descriptor, depth, descriptor_filter): + if descriptor_filter(descriptor): + descriptors = [descriptor] + else: + descriptors = [] + + if depth is None or depth > 0: + new_depth = depth - 1 if depth is not None else depth + + for child in descriptor.get_children(): + descriptors.extend(get_child_descriptors(child, new_depth, descriptor_filter)) + + return descriptors + + + descriptors = get_child_descriptors(descriptor, depth, descriptor_filter) + + return StudentModuleCache(user, descriptors) + + def _get_module_state_keys(self, descriptors): ''' - keys = [descriptor.location.url()] - - shared_state_key = getattr(descriptor, 'shared_state_key', None) - if shared_state_key is not None: - keys.append(shared_state_key) - - if depth is None or depth > 0: - new_depth = depth - 1 if depth is not None else depth - - for child in descriptor.get_children(): - keys.extend(self._get_module_state_keys(child, new_depth)) + Get a list of the state_keys needed for StudentModules + required for this module descriptor + + descriptor_filter is a function that accepts a descriptor and return wether the StudentModule + should be cached + ''' + keys = [] + for descriptor in descriptors: + keys.append(descriptor.location.url()) + + shared_state_key = getattr(descriptor, 'shared_state_key', None) + if shared_state_key is not None: + keys.append(shared_state_key) return keys diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 5b4dc602e2..5e74e6f42e 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -50,9 +50,9 @@ def toc_for_course(user, request, course, active_chapter, active_section): chapters with name 'hidden' are skipped. ''' - - student_module_cache = StudentModuleCache(user, course, depth=2) - (course, _, _, _) = get_module(user, request, course.location, student_module_cache) + + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2) + course = get_module(user, request, course.location, student_module_cache) chapters = list() for chapter in course.get_display_items(): @@ -121,25 +121,26 @@ def get_module(user, request, location, student_module_cache, position=None): - position : extra information from URL for user-specified position within module - Returns: - - a tuple (xmodule instance, instance_module, shared_module, module category). - instance_module is a StudentModule specific to this module for this student, - or None if this is an anonymous user - shared_module is a StudentModule specific to all modules with the same - 'shared_state_key' attribute, or None if the module does not elect to - share state + Returns: xmodule instance + ''' descriptor = modulestore().get_item(location) - - instance_module = student_module_cache.lookup(descriptor.category, - descriptor.location.url()) - - shared_state_key = getattr(descriptor, 'shared_state_key', None) - if shared_state_key is not None: - shared_module = student_module_cache.lookup(descriptor.category, - shared_state_key) + + #TODO Only check the cache if this module can possibly have state + if user.is_authenticated(): + instance_module = student_module_cache.lookup(descriptor.category, + descriptor.location.url()) + + shared_state_key = getattr(descriptor, 'shared_state_key', None) + if shared_state_key is not None: + shared_module = student_module_cache.lookup(descriptor.category, + shared_state_key) + else: + shared_module = None else: + instance_module = None shared_module = None + instance_state = instance_module.state if instance_module is not None else None shared_state = shared_module.state if shared_module is not None else None @@ -163,9 +164,8 @@ def get_module(user, request, location, student_module_cache, position=None): 'default_queuename': xqueue_default_queuename.replace(' ','_') } def _get_module(location): - (module, _, _, _) = get_module(user, request, location, + return get_module(user, request, location, student_module_cache, position) - return module # TODO (cpennington): When modules are shared between courses, the static # prefix is going to have to be specific to the module, not the directory @@ -198,30 +198,59 @@ def get_module(user, request, location, student_module_cache, position=None): if has_staff_access_to_course(user, module.location.course): module.get_html = add_histogram(module.get_html, module) - # If StudentModule for this instance wasn't already in the database, - # and this isn't a guest user, create it. + return module + +def get_instance_module(user, module, student_module_cache): + """ + Returns instance_module is a StudentModule specific to this module for this student, + or None if this is an anonymous user + """ if user.is_authenticated(): + instance_module = student_module_cache.lookup(module.category, + module.location.url()) + if not instance_module: instance_module = StudentModule( student=user, - module_type=descriptor.category, + module_type=module.category, module_state_key=module.id, state=module.get_instance_state(), max_grade=module.max_score()) instance_module.save() - # Add to cache. The caller and the system context have references - # to it, so the change persists past the return student_module_cache.append(instance_module) - if not shared_module and shared_state_key is not None: - shared_module = StudentModule( - student=user, - module_type=descriptor.category, - module_state_key=shared_state_key, - state=module.get_shared_state()) - shared_module.save() - student_module_cache.append(shared_module) - - return (module, instance_module, shared_module, descriptor.category) + + return instance_module + else: + return None + +def get_shared_instance_module(user, module, student_module_cache): + """ + Return shared_module is a StudentModule specific to all modules with the same + 'shared_state_key' attribute, or None if the module does not elect to + share state + """ + if user.is_authenticated(): + # To get the shared_state_key, we need to descriptor + descriptor = modulestore().get_item(module.location) + + shared_state_key = getattr(module, 'shared_state_key', None) + if shared_state_key is not None: + shared_module = student_module_cache.lookup(module.category, + shared_state_key) + if not shared_module: + shared_module = StudentModule( + student=user, + module_type=descriptor.category, + module_state_key=shared_state_key, + state=module.get_shared_state()) + shared_module.save() + student_module_cache.append(shared_module) + else: + shared_module = None + + return shared_module + else: + return None @csrf_exempt @@ -240,8 +269,10 @@ def xqueue_callback(request, userid, id, dispatch): # Retrieve target StudentModule user = User.objects.get(id=userid) - student_module_cache = StudentModuleCache(user, modulestore().get_item(id)) - instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id)) + instance = get_module(request.user, request, id, student_module_cache) + + instance_module = get_instance_module(request.user, instance, student_module_cache) if instance_module is None: log.debug("Couldn't find module '%s' for user '%s'", @@ -285,16 +316,18 @@ def modx_dispatch(request, dispatch=None, id=None): - id -- the module id. Used to look up the XModule instance ''' # ''' (fix emacs broken parsing) - # Check for submitted files p = request.POST.copy() if request.FILES: for inputfile_id in request.FILES.keys(): p[inputfile_id] = request.FILES[inputfile_id] - student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id)) - instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id)) + instance = get_module(request.user, request, id, student_module_cache) + instance_module = get_instance_module(request.user, instance, student_module_cache) + shared_module = get_shared_instance_module(request.user, instance, student_module_cache) + # Don't track state for anonymous users (who don't have student modules) if instance_module is not None: oldgrade = instance_module.grade diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index f014e3fcb5..ac00626063 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -34,7 +34,6 @@ log = logging.getLogger("mitx.courseware") template_imports = {'urllib': urllib} - def user_groups(user): if not user.is_authenticated(): return [] @@ -45,6 +44,8 @@ def user_groups(user): # Kill caching on dev machines -- we switch groups a lot group_names = cache.get(key) + if settings.DEBUG: + group_names = None if group_names is None: group_names = [u.name for u in UserTestGroup.objects.filter(users=user)] @@ -68,18 +69,17 @@ def gradebook(request, course_id): if 'course_admin' not in user_groups(request.user): raise Http404 course = check_course(course_id) - + student_objects = User.objects.all()[:100] student_info = [] - - for student in student_objects: - student_module_cache = StudentModuleCache(student, course) - course, _, _, _ = get_module(request.user, request, course.location, student_module_cache) + + #TODO: Only select students who are in the course + for student in student_objects: student_info.append({ 'username': student.username, 'id': student.id, 'email': student.email, - 'grade_info': grades.grade_sheet(student, course, student_module_cache), + 'grade_summary': grades.grade(student, request, course), 'realname': UserProfile.objects.get(user=student).name }) @@ -102,18 +102,23 @@ def profile(request, course_id, student_id=None): user_info = UserProfile.objects.get(user=student) - student_module_cache = StudentModuleCache(request.user, course) - course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache) - + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course) + course_module = get_module(request.user, request, course.location, 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, 'location': user_info.location, 'language': user_info.language, 'email': student.email, 'course': course, - 'csrf': csrf(request)['csrf_token'] + 'csrf': csrf(request)['csrf_token'], + 'courseware_summary' : courseware_summary, + 'grade_summary' : grade_summary } - context.update(grades.grade_sheet(student, course_module, course.grader, student_module_cache)) + context.update() return render_to_response('profile.html', context) @@ -184,11 +189,12 @@ def index(request, course_id, chapter=None, section=None, if look_for_module: section_descriptor = get_section(course, chapter, section) if section_descriptor is not None: - student_module_cache = StudentModuleCache(request.user, + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( + request.user, section_descriptor) - module, _, _, _ = get_module(request.user, request, - section_descriptor.location, - student_module_cache) + module = get_module(request.user, request, + section_descriptor.location, + student_module_cache) context['content'] = module.get_html() else: log.warning("Couldn't find a section descriptor for course_id '{0}'," diff --git a/lms/templates/gradebook.html b/lms/templates/gradebook.html index bcc0456711..e5721969e9 100644 --- a/lms/templates/gradebook.html +++ b/lms/templates/gradebook.html @@ -8,6 +8,7 @@ <%block name="headextra"> + <%static:css group='course'/>