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
...@@ -16,9 +17,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -16,9 +17,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:
...@@ -42,29 +40,79 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -42,29 +40,79 @@ 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:
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)
try: grading_policy = load_grading_policy(policy_string)
with self.system.resources_fs.open("grading_policy.json") as grading_policy_file:
policy_string = grading_policy_file.read() return grading_policy
except (IOError, ResourceNotFoundError):
log.warning("Unable to load course settings file from grading_policy.json in course " + self.id)
@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.
grading_policy = load_grading_policy(policy_string) 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.
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 @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":
......
...@@ -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
descriptor: An XModuleDescriptor database.
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 Arguments
user: The user for which to fetch maching 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
...@@ -91,27 +93,52 @@ class StudentModuleCache(object): ...@@ -91,27 +93,52 @@ class StudentModuleCache(object):
else: else:
self.cache = [] self.cache = []
def _get_module_state_keys(self, descriptor, depth):
''' @classmethod
Get a list of the state_keys needed for StudentModules def cache_for_descriptor_descendents(cls, user, descriptor, depth=None, descriptor_filter=lambda descriptor: True):
required for this module descriptor """
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
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()] Get a list of the state_keys needed for StudentModules
required for this module descriptor
shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None: descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
keys.append(shared_state_key) should be cached
'''
if depth is None or depth > 0: keys = []
new_depth = depth - 1 if depth is not None else depth for descriptor in descriptors:
keys.append(descriptor.location.url())
for child in descriptor.get_children():
keys.extend(self._get_module_state_keys(child, new_depth)) 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
......
...@@ -50,9 +50,9 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -50,9 +50,9 @@ 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,25 +121,26 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -121,25 +121,26 @@ 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)
instance_module = student_module_cache.lookup(descriptor.category, #TODO Only check the cache if this module can possibly have state
descriptor.location.url()) if user.is_authenticated():
instance_module = student_module_cache.lookup(descriptor.category,
shared_state_key = getattr(descriptor, 'shared_state_key', None) descriptor.location.url())
if shared_state_key is not None:
shared_module = student_module_cache.lookup(descriptor.category, shared_state_key = getattr(descriptor, 'shared_state_key', None)
shared_state_key) if shared_state_key is not None:
shared_module = student_module_cache.lookup(descriptor.category,
shared_state_key)
else:
shared_module = None
else: else:
instance_module = None
shared_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,30 +198,59 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -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): 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:
shared_module = StudentModule( return instance_module
student=user, else:
module_type=descriptor.category, return None
module_state_key=shared_state_key,
state=module.get_shared_state()) def get_shared_instance_module(user, module, student_module_cache):
shared_module.save() """
student_module_cache.append(shared_module) 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
return (module, instance_module, shared_module, descriptor.category) 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 @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,16 +316,18 @@ def modx_dispatch(request, dispatch=None, id=None): ...@@ -285,16 +316,18 @@ 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:
oldgrade = instance_module.grade oldgrade = instance_module.grade
......
...@@ -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)]
...@@ -68,18 +69,17 @@ def gradebook(request, course_id): ...@@ -68,18 +69,17 @@ def gradebook(request, course_id):
if 'course_admin' not in user_groups(request.user): if 'course_admin' not in user_groups(request.user):
raise Http404 raise Http404
course = check_course(course_id) course = check_course(course_id)
student_objects = User.objects.all()[:100] student_objects = User.objects.all()[:100]
student_info = [] student_info = []
for student in student_objects: #TODO: Only select students who are in the course
student_module_cache = StudentModuleCache(student, course) for student in student_objects:
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,18 +102,23 @@ def profile(request, course_id, student_id=None): ...@@ -102,18 +102,23 @@ 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,
'location': user_info.location, 'location': user_info.location,
'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,11 +189,12 @@ def index(request, course_id, chapter=None, section=None, ...@@ -184,11 +189,12 @@ 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()
else: else:
log.warning("Couldn't find a section descriptor for course_id '{0}'," log.warning("Couldn't find a section descriptor for course_id '{0}',"
......
...@@ -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'),
...@@ -119,7 +118,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -119,7 +118,7 @@ if settings.COURSEWARE_ENABLED:
#About the course #About the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
'courseware.views.course_about', name="about_course"), 'courseware.views.course_about', name="about_course"),
#Inside the course #Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
'courseware.views.course_info', name="info"), 'courseware.views.course_info', name="info"),
...@@ -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