Commit e14ebaaf by Calen Pennington

Merge pull request #345 from MITx/MITx/feature/bridger/fast_course_grading

Fast grading
parents 49b1678b 3a52e86a
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
...@@ -130,7 +130,7 @@ class CapaModule(XModule): ...@@ -130,7 +130,7 @@ class CapaModule(XModule):
if weight_string: if weight_string:
self.weight = float(weight_string) self.weight = float(weight_string)
else: else:
self.weight = 1 self.weight = None
if self.rerandomize == 'never': if self.rerandomize == 'never':
seed = 1 seed = 1
......
...@@ -3,6 +3,7 @@ import time ...@@ -3,6 +3,7 @@ import time
import dateutil.parser import dateutil.parser
import logging import logging
from util.decorators import lazyproperty
from xmodule.graders import load_grading_policy from xmodule.graders import load_grading_policy
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule from xmodule.seq_module import SequenceDescriptor, SequenceModule
...@@ -17,9 +18,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -17,9 +18,6 @@ class CourseDescriptor(SequenceDescriptor):
def __init__(self, system, definition=None, **kwargs): def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs) super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self._grader = None
self._grade_cutoffs = None
msg = None msg = None
try: try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
...@@ -42,17 +40,14 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -42,17 +40,14 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def grader(self): def grader(self):
self.__load_grading_policy() return self.__grading_policy['GRADER']
return self._grader
@property @property
def grade_cutoffs(self): def grade_cutoffs(self):
self.__load_grading_policy() return self.__grading_policy['GRADE_CUTOFFS']
return self._grade_cutoffs
def __load_grading_policy(self): @lazyproperty
if not self._grader or not self._grade_cutoffs: def __grading_policy(self):
policy_string = "" policy_string = ""
try: try:
...@@ -63,8 +58,61 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -63,8 +58,61 @@ class CourseDescriptor(SequenceDescriptor):
grading_policy = load_grading_policy(policy_string) grading_policy = load_grading_policy(policy_string)
self._grader = grading_policy['GRADER'] return grading_policy
self._grade_cutoffs = grading_policy['GRADE_CUTOFFS']
@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.
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.
"""
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 @staticmethod
......
...@@ -52,7 +52,7 @@ def certificate_request(request): ...@@ -52,7 +52,7 @@ def certificate_request(request):
return return_error(survey_response['error']) return return_error(survey_response['error'])
grade = None grade = None
student_gradesheet = grades.grade_sheet(request.user) student_gradesheet = grades.grade(request.user, request, course)
grade = student_gradesheet['grade'] grade = student_gradesheet['grade']
if not grade: if not grade:
...@@ -65,7 +65,7 @@ def certificate_request(request): ...@@ -65,7 +65,7 @@ def certificate_request(request):
else: else:
#This is not a POST, we should render the page with the form #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']) certificate_state = certificate_state_for_student(request.user, grade_sheet['grade'])
if certificate_state['state'] != "requestable": if certificate_state['state'] != "requestable":
......
...@@ -3,26 +3,135 @@ import logging ...@@ -3,26 +3,135 @@ import logging
from django.conf import settings from django.conf import settings
from models import StudentModuleCache
from module_render import get_module, get_instance_module
from xmodule import graders from xmodule import graders
from xmodule.graders import Score from xmodule.graders import Score
from models import StudentModule from models import StudentModule
_log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
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 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))
def grade_sheet(student, course, grader, student_module_cache): 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
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
""" """
This pulls a summary of all problems in the course. It returns a dictionary
with two datastructures:
- courseware_summary is a summary of all sections with problems in the letter_grade = None
course. It is organized as an array of chapters, each containing an array of for possible_grade in ['A', 'B', 'C']:
sections, each containing an array of scores. This contains information for if percentage >= grade_cutoffs[possible_grade]:
graded and ungraded problems, and is good for displaying a course summary letter_grade = possible_grade
with due dates, etc. break
- grade_summary is the output from the course grader. More information on return letter_grade
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: Arguments:
student: A User object for the student to grade student: A User object for the student to grade
...@@ -30,49 +139,24 @@ def grade_sheet(student, course, grader, student_module_cache): ...@@ -30,49 +139,24 @@ def grade_sheet(student, course, grader, student_module_cache):
student_module_cache: A StudentModuleCache initialized with all student_module_cache: A StudentModuleCache initialized with all
instance_modules for the student instance_modules for the student
""" """
totaled_scores = {}
chapters = [] chapters = []
for c in course.get_children(): for c in course.get_children():
sections = [] sections = []
for s in c.get_children(): 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) graded = s.metadata.get('graded', False)
scores = [] scores = []
for module in yield_descendents(s): for module in yield_module_descendents(s):
(correct, total) = get_score(student, module, student_module_cache) (correct, total) = get_score(student, module, student_module_cache)
if correct is None and total is None: if correct is None and total is None:
continue 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, scores.append(Score(correct, total, graded,
module.metadata.get('display_name'))) module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores( section_total, graded_total = graders.aggregate_scores(
scores, s.metadata.get('display_name')) scores, s.metadata.get('display_name'))
#Add the graded total to totaled_scores
format = s.metadata.get('format', "") 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({ sections.append({
'display_name': s.display_name, 'display_name': s.display_name,
'url_name': s.url_name, 'url_name': s.url_name,
...@@ -88,13 +172,10 @@ def grade_sheet(student, course, grader, student_module_cache): ...@@ -88,13 +172,10 @@ def grade_sheet(student, course, grader, student_module_cache):
'url_name': c.url_name, 'url_name': c.url_name,
'sections': sections}) 'sections': sections})
grade_summary = grader.grade(totaled_scores) return chapters
return {'courseware_summary': chapters,
'grade_summary': grade_summary}
def get_score(user, problem, cache): def get_score(user, problem, student_module_cache):
""" """
Return the score for a user on a problem Return the score for a user on a problem
...@@ -105,17 +186,18 @@ def get_score(user, problem, cache): ...@@ -105,17 +186,18 @@ def get_score(user, problem, cache):
correct = 0.0 correct = 0.0
# If the ID is not in the cache, add the item # If the ID is not in the cache, add the item
instance_module = cache.lookup(problem.category, problem.id) instance_module = get_instance_module(user, problem, student_module_cache)
if instance_module is None: # instance_module = student_module_cache.lookup(problem.category, problem.id)
instance_module = StudentModule(module_type=problem.category, # if instance_module is None:
module_state_key=problem.id, # instance_module = StudentModule(module_type=problem.category,
student=user, # module_state_key=problem.id,
state=None, # student=user,
grade=0, # state=None,
max_grade=problem.max_score(), # grade=0,
done='i') # max_grade=problem.max_score(),
cache.append(instance_module) # done='i')
instance_module.save() # cache.append(instance_module)
# instance_module.save()
# If this problem is ungraded/ungradable, bail # If this problem is ungraded/ungradable, bail
if instance_module.max_grade is None: if instance_module.max_grade is None:
...@@ -126,8 +208,11 @@ def get_score(user, problem, cache): ...@@ -126,8 +208,11 @@ def get_score(user, problem, cache):
if correct is not None and total is not None: if correct is not None and total is not None:
#Now we re-weight the problem, if specified #Now we re-weight the problem, if specified
weight = getattr(problem, 'weight', 1) weight = getattr(problem, 'weight', None)
if weight != 1: 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 correct = correct * weight / total
total = weight total = weight
......
...@@ -78,8 +78,8 @@ class Command(BaseCommand): ...@@ -78,8 +78,8 @@ class Command(BaseCommand):
# TODO (cpennington): Get coursename in a legitimate way # TODO (cpennington): Get coursename in a legitimate way
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012' course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
student_module_cache = StudentModuleCache(sample_user, modulestore().get_item(course_location)) 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) course = get_module(sample_user, None, course_location, student_module_cache)
to_run = [ to_run = [
#TODO (vshnayder) : make check_rendering work (use module_render.py), #TODO (vshnayder) : make check_rendering work (use module_render.py),
......
...@@ -67,17 +67,19 @@ class StudentModuleCache(object): ...@@ -67,17 +67,19 @@ class StudentModuleCache(object):
""" """
A cache of StudentModules for a specific student 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 Find any StudentModule objects that are needed by any child modules of the
supplied descriptor. Avoids making multiple queries to the database supplied descriptor, or caches only the StudentModule objects specifically
for every descriptor in descriptors. Avoids making multiple queries to the
database.
descriptor: An XModuleDescriptor Arguments
depth is the number of levels of descendent modules to load StudentModules for, in addition to user: The user for which to fetch maching StudentModules
the supplied descriptor. If depth is None, load all descendent StudentModules descriptors: An array of XModuleDescriptors.
''' '''
if user.is_authenticated(): 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 # This works around a limitation in sqlite3 on the number of parameters
# that can be put into a single query # that can be put into a single query
...@@ -92,26 +94,51 @@ class StudentModuleCache(object): ...@@ -92,26 +94,51 @@ class StudentModuleCache(object):
else: else:
self.cache = [] 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 descriptor: An XModuleDescriptor
depth is the number of levels of descendent modules to load StudentModules for, in addition to 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 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
keys = [descriptor.location.url()] should be cached
"""
shared_state_key = getattr(descriptor, 'shared_state_key', None) def get_child_descriptors(descriptor, depth, descriptor_filter):
if shared_state_key is not None: if descriptor_filter(descriptor):
keys.append(shared_state_key) descriptors = [descriptor]
else:
descriptors = []
if depth is None or depth > 0: if depth is None or depth > 0:
new_depth = depth - 1 if depth is not None else depth new_depth = depth - 1 if depth is not None else depth
for child in descriptor.get_children(): for child in descriptor.get_children():
keys.extend(self._get_module_state_keys(child, new_depth)) 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):
'''
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 return keys
......
...@@ -51,8 +51,8 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -51,8 +51,8 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters with name 'hidden' are skipped. chapters with name 'hidden' are skipped.
''' '''
student_module_cache = StudentModuleCache(user, course, depth=2) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
(course, _, _, _) = get_module(user, request, course.location, student_module_cache) course = get_module(user, request, course.location, student_module_cache)
chapters = list() chapters = list()
for chapter in course.get_display_items(): for chapter in course.get_display_items():
...@@ -121,16 +121,13 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -121,16 +121,13 @@ def get_module(user, request, location, student_module_cache, position=None):
- position : extra information from URL for user-specified - position : extra information from URL for user-specified
position within module position within module
Returns: Returns: xmodule instance
- 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
''' '''
descriptor = modulestore().get_item(location) descriptor = modulestore().get_item(location)
#TODO Only check the cache if this module can possibly have state
if user.is_authenticated():
instance_module = student_module_cache.lookup(descriptor.category, instance_module = student_module_cache.lookup(descriptor.category,
descriptor.location.url()) descriptor.location.url())
...@@ -140,6 +137,10 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -140,6 +137,10 @@ def get_module(user, request, location, student_module_cache, position=None):
shared_state_key) shared_state_key)
else: else:
shared_module = None shared_module = None
else:
instance_module = None
shared_module = None
instance_state = instance_module.state if instance_module is not None else 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 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): ...@@ -163,9 +164,8 @@ def get_module(user, request, location, student_module_cache, position=None):
'default_queuename': xqueue_default_queuename.replace(' ','_') } 'default_queuename': xqueue_default_queuename.replace(' ','_') }
def _get_module(location): def _get_module(location):
(module, _, _, _) = get_module(user, request, location, return get_module(user, request, location,
student_module_cache, position) student_module_cache, position)
return module
# TODO (cpennington): When modules are shared between courses, the static # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # prefix is going to have to be specific to the module, not the directory
...@@ -198,21 +198,46 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -198,21 +198,46 @@ def get_module(user, request, location, student_module_cache, position=None):
if has_staff_access_to_course(user, module.location.course): if has_staff_access_to_course(user, module.location.course):
module.get_html = add_histogram(module.get_html, module) module.get_html = add_histogram(module.get_html, module)
# If StudentModule for this instance wasn't already in the database, return module
# and this isn't a guest user, create it.
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(): if user.is_authenticated():
instance_module = student_module_cache.lookup(module.category,
module.location.url())
if not instance_module: if not instance_module:
instance_module = StudentModule( instance_module = StudentModule(
student=user, student=user,
module_type=descriptor.category, module_type=module.category,
module_state_key=module.id, module_state_key=module.id,
state=module.get_instance_state(), state=module.get_instance_state(),
max_grade=module.max_score()) max_grade=module.max_score())
instance_module.save() 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) student_module_cache.append(instance_module)
if not shared_module and shared_state_key is not None:
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( shared_module = StudentModule(
student=user, student=user,
module_type=descriptor.category, module_type=descriptor.category,
...@@ -220,8 +245,12 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -220,8 +245,12 @@ def get_module(user, request, location, student_module_cache, position=None):
state=module.get_shared_state()) state=module.get_shared_state())
shared_module.save() shared_module.save()
student_module_cache.append(shared_module) student_module_cache.append(shared_module)
else:
shared_module = None
return (module, instance_module, shared_module, descriptor.category) return shared_module
else:
return None
@csrf_exempt @csrf_exempt
...@@ -240,8 +269,10 @@ def xqueue_callback(request, userid, id, dispatch): ...@@ -240,8 +269,10 @@ def xqueue_callback(request, userid, id, dispatch):
# Retrieve target StudentModule # Retrieve target StudentModule
user = User.objects.get(id=userid) user = User.objects.get(id=userid)
student_module_cache = StudentModuleCache(user, modulestore().get_item(id)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id))
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) 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: if instance_module is None:
log.debug("Couldn't find module '%s' for user '%s'", log.debug("Couldn't find module '%s' for user '%s'",
...@@ -285,15 +316,17 @@ def modx_dispatch(request, dispatch=None, id=None): ...@@ -285,15 +316,17 @@ def modx_dispatch(request, dispatch=None, id=None):
- id -- the module id. Used to look up the XModule instance - id -- the module id. Used to look up the XModule instance
''' '''
# ''' (fix emacs broken parsing) # ''' (fix emacs broken parsing)
# Check for submitted files # Check for submitted files
p = request.POST.copy() p = request.POST.copy()
if request.FILES: if request.FILES:
for inputfile_id in request.FILES.keys(): for inputfile_id in request.FILES.keys():
p[inputfile_id] = request.FILES[inputfile_id] p[inputfile_id] = request.FILES[inputfile_id]
student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id)) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) 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) # Don't track state for anonymous users (who don't have student modules)
if instance_module is not None: if instance_module is not None:
......
...@@ -34,7 +34,6 @@ log = logging.getLogger("mitx.courseware") ...@@ -34,7 +34,6 @@ log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
def user_groups(user): def user_groups(user):
if not user.is_authenticated(): if not user.is_authenticated():
return [] return []
...@@ -45,6 +44,8 @@ def user_groups(user): ...@@ -45,6 +44,8 @@ def user_groups(user):
# Kill caching on dev machines -- we switch groups a lot # Kill caching on dev machines -- we switch groups a lot
group_names = cache.get(key) group_names = cache.get(key)
if settings.DEBUG:
group_names = None
if group_names is None: if group_names is None:
group_names = [u.name for u in UserTestGroup.objects.filter(users=user)] group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
...@@ -72,14 +73,13 @@ def gradebook(request, course_id): ...@@ -72,14 +73,13 @@ def gradebook(request, course_id):
student_objects = User.objects.all()[:100] student_objects = User.objects.all()[:100]
student_info = [] student_info = []
#TODO: Only select students who are in the course
for student in student_objects: for student in student_objects:
student_module_cache = StudentModuleCache(student, course)
course, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
student_info.append({ student_info.append({
'username': student.username, 'username': student.username,
'id': student.id, 'id': student.id,
'email': student.email, '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 'realname': UserProfile.objects.get(user=student).name
}) })
...@@ -102,8 +102,11 @@ def profile(request, course_id, student_id=None): ...@@ -102,8 +102,11 @@ def profile(request, course_id, student_id=None):
user_info = UserProfile.objects.get(user=student) user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache(request.user, course) student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache) 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, context = {'name': user_info.name,
'username': student.username, 'username': student.username,
...@@ -111,9 +114,11 @@ def profile(request, course_id, student_id=None): ...@@ -111,9 +114,11 @@ def profile(request, course_id, student_id=None):
'language': user_info.language, 'language': user_info.language,
'email': student.email, 'email': student.email,
'course': course, '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) return render_to_response('profile.html', context)
...@@ -184,9 +189,10 @@ def index(request, course_id, chapter=None, section=None, ...@@ -184,9 +189,10 @@ def index(request, course_id, chapter=None, section=None,
if look_for_module: if look_for_module:
section_descriptor = get_section(course, chapter, section) section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None: if section_descriptor is not None:
student_module_cache = StudentModuleCache(request.user, student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
request.user,
section_descriptor) section_descriptor)
module, _, _, _ = get_module(request.user, request, module = get_module(request.user, request,
section_descriptor.location, section_descriptor.location,
student_module_cache) student_module_cache)
context['content'] = module.get_html() context['content'] = module.get_html()
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
</%block> </%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/>
<style type="text/css"> <style type="text/css">
.grade_a {color:green;} .grade_a {color:green;}
...@@ -19,7 +20,8 @@ ...@@ -19,7 +20,8 @@
</%block> </%block>
<%include file="navigation.html" args="active_page=''" /> <%include file="course_navigation.html" args="active_page=''" />
<section class="container"> <section class="container">
<div class="gradebook-wrapper"> <div class="gradebook-wrapper">
<section class="gradebook-content"> <section class="gradebook-content">
...@@ -28,7 +30,7 @@ ...@@ -28,7 +30,7 @@
%if len(students) > 0: %if len(students) > 0:
<table> <table>
<% <%
templateSummary = students[0]['grade_info']['grade_summary'] templateSummary = students[0]['grade_summary']
%> %>
...@@ -42,15 +44,15 @@ ...@@ -42,15 +44,15 @@
<%def name="percent_data(percentage)"> <%def name="percent_data(percentage)">
<% <%
data_class = "grade_none" letter_grade = 'None'
if percentage > .87: if percentage > 0:
data_class = "grade_a" letter_grade = 'F'
elif percentage > .70: for grade in ['A', 'B', 'C']:
data_class = "grade_b" if percentage >= course.grade_cutoffs[grade]:
elif percentage > .6: letter_grade = grade
data_class = "grade_c" break
elif percentage > 0:
data_class = "grade_f" data_class = "grade_" + letter_grade
%> %>
<td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td> <td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td>
</%def> </%def>
...@@ -58,10 +60,10 @@ ...@@ -58,10 +60,10 @@
%for student in students: %for student in students:
<tr> <tr>
<td><a href="/profile/${student['id']}/">${student['username']}</a></td> <td><a href="/profile/${student['id']}/">${student['username']}</a></td>
%for section in student['grade_info']['grade_summary']['section_breakdown']: %for section in student['grade_summary']['section_breakdown']:
${percent_data( section['percent'] )} ${percent_data( section['percent'] )}
%endfor %endfor
<th>${percent_data( student['grade_info']['grade_summary']['percent'])}</th> <th>${percent_data( student['grade_summary']['percent'])}</th>
</tr> </tr>
%endfor %endfor
</table> </table>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
<script> <script>
${profile_graphs.body(grade_summary, "grade-detail-graph")} ${profile_graphs.body(grade_summary, course.grade_cutoffs, "grade-detail-graph")}
</script> </script>
<script> <script>
......
<%page args="grade_summary, graph_div_id, **kwargs"/> <%page args="grade_summary, grade_cutoffs, graph_div_id, **kwargs"/>
<%! <%!
import json import json
import math
%> %>
$(function () { $(function () {
...@@ -89,8 +90,16 @@ $(function () { ...@@ -89,8 +90,16 @@ $(function () {
ticks += [ [overviewBarX, "Total"] ] ticks += [ [overviewBarX, "Total"] ]
tickIndex += 1 + sectionSpacer tickIndex += 1 + sectionSpacer
totalScore = grade_summary['percent'] totalScore = math.floor(grade_summary['percent'] * 100) / 100 #We floor it to the nearest percent, 80.9 won't show up like a 90 (an A)
detail_tooltips['Dropped Scores'] = dropped_score_tooltips detail_tooltips['Dropped Scores'] = dropped_score_tooltips
## ----------------------------- Grade cutoffs ------------------------- ##
grade_cutoff_ticks = [ [1, "100%"], [0, "0%"] ]
for grade in ['A', 'B', 'C']:
percent = grade_cutoffs[grade]
grade_cutoff_ticks.append( [ percent, "{0} {1:.0%}".format(grade, percent) ] )
%> %>
var series = ${ json.dumps( series ) }; var series = ${ json.dumps( series ) };
...@@ -98,6 +107,7 @@ $(function () { ...@@ -98,6 +107,7 @@ $(function () {
var bottomTicks = ${ json.dumps(bottomTicks) }; var bottomTicks = ${ json.dumps(bottomTicks) };
var detail_tooltips = ${ json.dumps(detail_tooltips) }; var detail_tooltips = ${ json.dumps(detail_tooltips) };
var droppedScores = ${ json.dumps(droppedScores) }; var droppedScores = ${ json.dumps(droppedScores) };
var grade_cutoff_ticks = ${ json.dumps(grade_cutoff_ticks) }
//Alwasy be sure that one series has the xaxis set to 2, or the second xaxis labels won't show up //Alwasy be sure that one series has the xaxis set to 2, or the second xaxis labels won't show up
series.push( {label: 'Dropped Scores', data: droppedScores, points: {symbol: "cross", show: true, radius: 3}, bars: {show: false}, color: "#333"} ); series.push( {label: 'Dropped Scores', data: droppedScores, points: {symbol: "cross", show: true, radius: 3}, bars: {show: false}, color: "#333"} );
...@@ -107,10 +117,10 @@ $(function () { ...@@ -107,10 +117,10 @@ $(function () {
lines: {show: false, steps: false }, lines: {show: false, steps: false },
bars: {show: true, barWidth: 0.8, align: 'center', lineWidth: 0, fill: .8 },}, bars: {show: true, barWidth: 0.8, align: 'center', lineWidth: 0, fill: .8 },},
xaxis: {tickLength: 0, min: 0.0, max: ${tickIndex - sectionSpacer}, ticks: ticks, labelAngle: 90}, xaxis: {tickLength: 0, min: 0.0, max: ${tickIndex - sectionSpacer}, ticks: ticks, labelAngle: 90},
yaxis: {ticks: [[1, "100%"], [0.87, "A 87%"], [0.7, "B 70%"], [0.6, "C 60%"], [0, "0%"]], min: 0.0, max: 1.0, labelWidth: 50}, yaxis: {ticks: grade_cutoff_ticks, min: 0.0, max: 1.0, labelWidth: 50},
grid: { hoverable: true, clickable: true, borderWidth: 1, grid: { hoverable: true, clickable: true, borderWidth: 1,
markings: [ {yaxis: {from: 0.87, to: 1 }, color: "#ddd"}, {yaxis: {from: 0.7, to: 0.87 }, color: "#e9e9e9"}, markings: [ {yaxis: {from: ${grade_cutoffs['A']}, to: 1 }, color: "#ddd"}, {yaxis: {from: ${grade_cutoffs['B']}, to: ${grade_cutoffs['A']} }, color: "#e9e9e9"},
{yaxis: {from: 0.6, to: 0.7 }, color: "#f3f3f3"}, ] }, {yaxis: {from: ${grade_cutoffs['C']}, to: ${grade_cutoffs['B']} }, color: "#f3f3f3"}, ] },
legend: {show: false}, legend: {show: false},
}; };
......
...@@ -107,7 +107,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -107,7 +107,6 @@ if settings.COURSEWARE_ENABLED:
# TODO: These views need to be updated before they work # TODO: These views need to be updated before they work
# url(r'^calculate$', 'util.views.calculate'), # url(r'^calculate$', 'util.views.calculate'),
# url(r'^gradebook$', 'courseware.views.gradebook'),
# TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki # TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki
# url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'), # url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'),
# url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'), # url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'),
...@@ -137,6 +136,10 @@ if settings.COURSEWARE_ENABLED: ...@@ -137,6 +136,10 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.profile', name="profile"), 'courseware.views.profile', name="profile"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$',
'courseware.views.profile'), 'courseware.views.profile'),
# For the instructor
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$',
'courseware.views.gradebook'),
) )
# Multicourse wiki # Multicourse wiki
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment