Commit a5483c16 by David Ormsbee

Merge pull request #650 from MITx/feature/bridger/course_grading

Feature/bridger/course grading
parents 9e460321 a37e8e26
...@@ -47,11 +47,14 @@ class ABTestModule(XModule): ...@@ -47,11 +47,14 @@ class ABTestModule(XModule):
def get_shared_state(self): def get_shared_state(self):
return json.dumps({'group': self.group}) return json.dumps({'group': self.group})
def get_children_locations(self):
return self.definition['data']['group_content'][self.group]
def displayable_items(self): def displayable_items(self):
child_locations = self.definition['data']['group_content'][self.group] # Most modules return "self" as the displayable_item. We never display ourself
children = [self.system.get_module(loc) for loc in child_locations] # (which is why we don't implement get_html). We only display our children.
return [c for c in children if c is not None] return self.get_children()
# TODO (cpennington): Use Groups should be a first class object, rather than being # TODO (cpennington): Use Groups should be a first class object, rather than being
...@@ -158,3 +161,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ...@@ -158,3 +161,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
group_elem.append(etree.fromstring(child.export_to_xml(resource_fs))) group_elem.append(etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object return xml_object
def has_dynamic_children(self):
return True
...@@ -125,12 +125,6 @@ class CapaModule(XModule): ...@@ -125,12 +125,6 @@ class CapaModule(XModule):
self.name = only_one(dom2.xpath('/problem/@name')) self.name = only_one(dom2.xpath('/problem/@name'))
weight_string = only_one(dom2.xpath('/problem/@weight'))
if weight_string:
self.weight = float(weight_string)
else:
self.weight = None
if self.rerandomize == 'never': if self.rerandomize == 'never':
self.seed = 1 self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'id'): elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
...@@ -279,7 +273,7 @@ class CapaModule(XModule): ...@@ -279,7 +273,7 @@ class CapaModule(XModule):
content = {'name': self.display_name, content = {'name': self.display_name,
'html': html, 'html': html,
'weight': self.weight, 'weight': self.descriptor.weight,
} }
# We using strings as truthy values, because the terminology of the # We using strings as truthy values, because the terminology of the
...@@ -659,3 +653,12 @@ class CapaDescriptor(RawDescriptor): ...@@ -659,3 +653,12 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:], 'problems/' + path[8:],
path[8:], path[8:],
] ]
def __init__(self, *args, **kwargs):
super(CapaDescriptor, self).__init__(*args, **kwargs)
weight_string = self.metadata.get('weight', None)
if weight_string:
self.weight = float(weight_string)
else:
self.weight = None
...@@ -189,6 +189,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -189,6 +189,7 @@ class CourseDescriptor(SequenceDescriptor):
for s in c.get_children(): for s in c.get_children():
if s.metadata.get('graded', False): if s.metadata.get('graded', False):
xmoduledescriptors = list(yield_descriptor_descendents(s)) xmoduledescriptors = list(yield_descriptor_descendents(s))
xmoduledescriptors.append(s)
# The xmoduledescriptors included here are only the ones that have scores. # The xmoduledescriptors included here are only the ones that have scores.
section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) } section_description = { 'section_descriptor' : s, 'xmoduledescriptors' : filter(lambda child: child.has_score, xmoduledescriptors) }
......
...@@ -219,13 +219,28 @@ class XModule(HTMLSnippet): ...@@ -219,13 +219,28 @@ class XModule(HTMLSnippet):
Return module instances for all the children of this module. Return module instances for all the children of this module.
''' '''
if self._loaded_children is None: if self._loaded_children is None:
child_locations = self.definition.get('children', []) child_locations = self.get_children_locations()
children = [self.system.get_module(loc) for loc in child_locations] children = [self.system.get_module(loc) for loc in child_locations]
# get_module returns None if the current user doesn't have access # get_module returns None if the current user doesn't have access
# to the location. # to the location.
self._loaded_children = [c for c in children if c is not None] self._loaded_children = [c for c in children if c is not None]
return self._loaded_children return self._loaded_children
def get_children_locations(self):
'''
Returns the locations of each of child modules.
Overriding this changes the behavior of get_children and
anything that uses get_children, such as get_display_items.
This method will not instantiate the modules of the children
unless absolutely necessary, so it is cheaper to call than get_children
These children will be the same children returned by the
descriptor unless descriptor.has_dynamic_children() is true.
'''
return self.definition.get('children', [])
def get_display_items(self): def get_display_items(self):
''' '''
...@@ -489,6 +504,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -489,6 +504,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
self, self,
metadata=self.metadata metadata=self.metadata
) )
def has_dynamic_children(self):
"""
Returns True if this descriptor has dynamic children for a given
student when the module is created.
Returns False if the children of this descriptor are the same
children that the module will return for any student.
"""
return False
# ================================= JSON PARSING =========================== # ================================= JSON PARSING ===========================
@staticmethod @staticmethod
......
This is a very very simple course, useful for initial debugging of processing code.
roots/2012_Fall.xml
\ No newline at end of file
<course>
<chapter url_name="GradedChapter">
<vertical url_name="Homework1">
<problem url_name="H1P1">
<optionresponse>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
</optionresponse>
</problem>
<abtest experiment="HiddenProblem">
<group name="SeeProblem" portion="1">
<problem url_name="H1P2">
<optionresponse>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
</optionresponse>
</problem>
</group>
<group name="HiddenProblem" portion="0">
<problem url_name="H1P3">
<optionresponse>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
</optionresponse>
</problem>
</group>
</abtest>
</vertical>
<videosequence url_name="Homework2">
<vertical url_name="Homework2Inner">
<problem url_name="H2P1">
<optionresponse>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
</optionresponse>
</problem>
<problem url_name="H2P2">
<optionresponse>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
</optionresponse>
</problem>
</vertical>
</videosequence>
<videosequence url_name="Homework3">
<vertical url_name="Homework3Inner">
<problem url_name="H3P1">
<optionresponse>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
</optionresponse>
</problem>
<problem url_name="H3P2">
<optionresponse>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
</optionresponse>
</problem>
</vertical>
</videosequence>
<problem url_name="FinalQuestion">
<optionresponse>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
</optionresponse>
</problem>
</chapter>
</course>
{
"GRADER" : [
{
"type" : "Homework",
"min_count" : 3,
"drop_count" : 1,
"short_label" : "HW",
"weight" : 0.5
},
{
"type" : "Final",
"name" : "Final Question",
"short_label" : "Final",
"weight" : 0.5
}
],
"GRADE_CUTOFFS" : {
"A" : 0.8,
"B" : 0.7,
"C" : 0.6
}
}
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2010-07-17T12:00",
"display_name": "Graded Course",
"graded": "true"
},
"vertical/Homework1": {
"display_name": "Homework 1",
"graded": true,
"format": "Homework"
},
"videosequence/Homework2": {
"display_name": "Homework 2",
"graded": true,
"format": "Homework"
},
"problem/H2P1": {
"weight": 4
},
"videosequence/Homework3": {
"display_name": "Homework 3",
"graded": true,
"format": "Homework"
},
"vertical/Homework1": {
"display_name": "Homework 1",
"graded": true,
"format": "Homework"
},
"problem/FinalQuestion": {
"display_name": "Final Question",
"graded": true,
"format": "Final"
},
"chapter/Overview": {
"display_name": "Overview"
}
}
<course org="edX" course="graded" url_name="2012_Fall"/>
\ No newline at end of file
...@@ -27,6 +27,29 @@ def yield_module_descendents(module): ...@@ -27,6 +27,29 @@ def yield_module_descendents(module):
stack.extend( next_module.get_display_items() ) stack.extend( next_module.get_display_items() )
yield next_module yield next_module
def yield_dynamic_descriptor_descendents(descriptor, module_creator):
"""
This returns all of the descendants of a descriptor. If the descriptor
has dynamic children, the module will be created using module_creator
and the children (as descriptors) of that module will be returned.
"""
def get_dynamic_descriptor_children(descriptor):
if descriptor.has_dynamic_children():
module = module_creator(descriptor)
child_locations = module.get_children_locations()
return [descriptor.system.load_item(child_location) for child_location in child_locations ]
else:
return descriptor.get_children()
stack = [descriptor]
while len(stack) > 0:
next_descriptor = stack.pop()
stack.extend( get_dynamic_descriptor_children(next_descriptor) )
yield next_descriptor
def yield_problems(request, course, student): def yield_problems(request, course, student):
""" """
Return an iterator over capa_modules that this student has Return an iterator over capa_modules that this student has
...@@ -128,7 +151,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F ...@@ -128,7 +151,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
section_name = section_descriptor.metadata.get('display_name') section_name = section_descriptor.metadata.get('display_name')
should_grade_section = False 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% # 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']: for moduledescriptor in section['xmoduledescriptors']:
if student_module_cache.lookup( if student_module_cache.lookup(
course.id, moduledescriptor.category, moduledescriptor.location.url()): course.id, moduledescriptor.category, moduledescriptor.location.url()):
...@@ -137,20 +160,16 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F ...@@ -137,20 +160,16 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
if should_grade_section: if should_grade_section:
scores = [] scores = []
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler def create_module(descriptor):
section_module = get_module(student, request, # TODO: We need the request to pass into here. If we could forgo that, our arguments
section_descriptor.location, student_module_cache, # would be simpler
course.id) return get_module(student, request, descriptor.location,
if section_module is None: student_module_cache, course.id)
# student doesn't have access to this module, or something else
# went wrong. for module_descriptor in yield_dynamic_descriptor_descendents(section_descriptor, create_module):
continue
(correct, total) = get_score(course.id, student, module_descriptor, create_module, 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(course.id, student, module, student_module_cache)
if correct is None and total is None: if correct is None and total is None:
continue continue
...@@ -160,12 +179,12 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F ...@@ -160,12 +179,12 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
else: else:
correct = total correct = total
graded = module.metadata.get("graded", False) graded = module_descriptor.metadata.get("graded", False)
if not total > 0: if not total > 0:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage #We simply cannot grade a problem that is 12/0, because we might need it as a percentage
graded = False graded = False
scores.append(Score(correct, total, graded, module.metadata.get('display_name'))) scores.append(Score(correct, total, graded, module_descriptor.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores(scores, section_name) section_total, graded_total = graders.aggregate_scores(scores, section_name)
if keep_raw_scores: if keep_raw_scores:
...@@ -214,7 +233,11 @@ def grade_for_percentage(grade_cutoffs, percentage): ...@@ -214,7 +233,11 @@ def grade_for_percentage(grade_cutoffs, percentage):
return letter_grade return letter_grade
def progress_summary(student, course, grader, student_module_cache):
# TODO: This method is not very good. It was written in the old course style and
# then converted over and performance is not good. Once the progress page is redesigned
# to not have the progress summary this method should be deleted (so it won't be copied).
def progress_summary(student, request, course, student_module_cache):
""" """
This pulls a summary of all problems in the course. This pulls a summary of all problems in the course.
...@@ -230,58 +253,76 @@ def progress_summary(student, course, grader, student_module_cache): ...@@ -230,58 +253,76 @@ def progress_summary(student, course, grader, student_module_cache):
course: An XModule containing the course to grade course: An XModule containing the course to grade
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
If the student does not have access to load the course module, this function
will return None.
""" """
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler
course_module = get_module(student, request,
course.location, student_module_cache,
course.id)
if not course_module:
# This student must not have access to the course.
return None
chapters = [] chapters = []
# Don't include chapters that aren't displayable (e.g. due to error) # Don't include chapters that aren't displayable (e.g. due to error)
for c in course.get_display_items(): for chapter_module in course_module.get_display_items():
# Skip if the chapter is hidden # Skip if the chapter is hidden
hidden = c.metadata.get('hide_from_toc','false') hidden = chapter_module.metadata.get('hide_from_toc','false')
if hidden.lower() == 'true': if hidden.lower() == 'true':
continue continue
sections = [] sections = []
for s in c.get_display_items(): for section_module in chapter_module.get_display_items():
# Skip if the section is hidden # Skip if the section is hidden
hidden = s.metadata.get('hide_from_toc','false') hidden = section_module.metadata.get('hide_from_toc','false')
if hidden.lower() == 'true': if hidden.lower() == 'true':
continue continue
# Same for sections # Same for sections
graded = s.metadata.get('graded', False) graded = section_module.metadata.get('graded', False)
scores = [] scores = []
for module in yield_module_descendents(s):
# course is a module, not a descriptor... module_creator = lambda descriptor : section_module.system.get_module(descriptor.location)
course_id = course.descriptor.id
(correct, total) = get_score(course_id, student, module, student_module_cache) for module_descriptor in yield_dynamic_descriptor_descendents(section_module.descriptor, module_creator):
course_id = course.id
(correct, total) = get_score(course_id, student, module_descriptor, module_creator, student_module_cache)
if correct is None and total is None: if correct is None and total is None:
continue continue
scores.append(Score(correct, total, graded, scores.append(Score(correct, total, graded,
module.metadata.get('display_name'))) module_descriptor.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, section_module.metadata.get('display_name'))
format = s.metadata.get('format', "") format = section_module.metadata.get('format', "")
sections.append({ sections.append({
'display_name': s.display_name, 'display_name': section_module.display_name,
'url_name': s.url_name, 'url_name': section_module.url_name,
'scores': scores, 'scores': scores,
'section_total': section_total, 'section_total': section_total,
'format': format, 'format': format,
'due': s.metadata.get("due", ""), 'due': section_module.metadata.get("due", ""),
'graded': graded, 'graded': graded,
}) })
chapters.append({'course': course.display_name, chapters.append({'course': course.display_name,
'display_name': c.display_name, 'display_name': chapter_module.display_name,
'url_name': c.url_name, 'url_name': chapter_module.url_name,
'sections': sections}) 'sections': sections})
return chapters return chapters
def get_score(course_id, user, problem, student_module_cache): def get_score(course_id, user, problem_descriptor, module_creator, student_module_cache):
""" """
Return the score for a user on a problem, as a tuple (correct, total). Return the score for a user on a problem, as a tuple (correct, total).
...@@ -289,29 +330,23 @@ def get_score(course_id, user, problem, student_module_cache): ...@@ -289,29 +330,23 @@ def get_score(course_id, user, problem, student_module_cache):
problem: an XModule problem: an XModule
cache: A StudentModuleCache cache: A StudentModuleCache
""" """
if not (problem.descriptor.stores_state and problem.descriptor.has_score): if not (problem_descriptor.stores_state and problem_descriptor.has_score):
# These are not problems, and do not have a score # These are not problems, and do not have a score
return (None, None) return (None, None)
correct = 0.0 correct = 0.0
# If the ID is not in the cache, add the item instance_module = student_module_cache.lookup(
instance_module = get_instance_module(course_id, user, problem, student_module_cache) course_id, problem_descriptor.category, problem_descriptor.location.url())
# instance_module = student_module_cache.lookup(problem.category, problem.id)
# if instance_module is None: if not instance_module:
# instance_module = StudentModule(module_type=problem.category, # If the problem was not in the cache, we need to instantiate the problem.
# course_id=????, # Otherwise, the max score (cached in instance_module) won't be available
# module_state_key=problem.id, problem = module_creator(problem_descriptor)
# student=user, instance_module = get_instance_module(course_id, user, problem, student_module_cache)
# 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 this problem is ungraded/ungradable, bail
if instance_module.max_grade is None: if not instance_module or instance_module.max_grade is None:
return (None, None) return (None, None)
correct = instance_module.grade if instance_module.grade is not None else 0 correct = instance_module.grade if instance_module.grade is not None else 0
...@@ -319,7 +354,7 @@ def get_score(course_id, user, problem, student_module_cache): ...@@ -319,7 +354,7 @@ def get_score(course_id, user, problem, student_module_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', None) weight = getattr(problem_descriptor, 'weight', None)
if weight is not None: if weight is not None:
if total == 0: if total == 0:
log.exception("Cannot reweight a problem with zero weight. Problem: " + str(instance_module)) log.exception("Cannot reweight a problem with zero weight. Problem: " + str(instance_module))
......
...@@ -10,8 +10,9 @@ from pprint import pprint ...@@ -10,8 +10,9 @@ from pprint import pprint
from urlparse import urlsplit, urlunsplit from urlparse import urlsplit, urlunsplit
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client, RequestFactory
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from mock import patch, Mock from mock import patch, Mock
...@@ -20,7 +21,9 @@ from override_settings import override_settings ...@@ -20,7 +21,9 @@ from override_settings import override_settings
import xmodule.modulestore.django import xmodule.modulestore.django
# Need access to internal func to put users in the right group # Need access to internal func to put users in the right group
from courseware import grades
from courseware.access import _course_staff_group_name from courseware.access import _course_staff_group_name
from courseware.models import StudentModuleCache
from student.models import Registration from student.models import Registration
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -640,3 +643,133 @@ class RealCoursesLoadTestCase(PageLoader): ...@@ -640,3 +643,133 @@ class RealCoursesLoadTestCase(PageLoader):
# ========= TODO: check ajax interaction here too? # ========= TODO: check ajax interaction here too?
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCourseGrader(PageLoader):
"""Check that a course gets graded properly"""
# NOTE: setUpClass() runs before override_settings takes effect, so
# can't do imports there without manually hacking settings.
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses()
def find_course(course_id):
"""Assumes the course is present"""
return [c for c in courses if c.id==course_id][0]
self.graded_course = find_course("edX/graded/2012_Fall")
# create a test student
self.student = 'view@test.com'
self.password = 'foo'
self.create_account('u1', self.student, self.password)
self.activate_user(self.student)
self.enroll(self.graded_course)
self.student_user = user(self.student)
self.factory = RequestFactory()
def check_grade_percent(self, percent):
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
self.graded_course.id, self.student_user, self.graded_course)
fake_request = self.factory.get(reverse('progress',
kwargs={'course_id': self.graded_course.id}))
grade_summary = grades.grade(self.student_user, fake_request,
self.graded_course, student_module_cache)
self.assertEqual(grade_summary['percent'], percent)
def submit_question_answer(self, problem_url_name, responses):
"""
The field names of a problem are hard to determine. This method only works
for the problems used in the edX/graded course, which has fields named in the
following form:
input_i4x-edX-graded-problem-H1P3_2_1
input_i4x-edX-graded-problem-H1P3_2_2
"""
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name)
modx_url = reverse('modx_dispatch',
kwargs={
'course_id' : self.graded_course.id,
'location' : problem_location,
'dispatch' : 'problem_check', }
)
resp = self.client.post(modx_url, {
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0],
'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1],
})
print "modx_url" , modx_url, "responses" , responses
print "resp" , resp
return resp
def reset_question_answer(self, problem_url_name):
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name)
modx_url = reverse('modx_dispatch',
kwargs={
'course_id' : self.graded_course.id,
'location' : problem_location,
'dispatch' : 'problem_reset', }
)
resp = self.client.post(modx_url)
return resp
def test_get_graded(self):
#### Check that the grader shows we have 0% in the course
self.check_grade_percent(0)
#### Submit the answers to a few problems as ajax calls
# Only get half of the first problem correct
self.submit_question_answer('H1P1', ['Correct', 'Incorrect'])
self.check_grade_percent(0.06)
# Get both parts of the first problem correct
self.reset_question_answer('H1P1')
self.submit_question_answer('H1P1', ['Correct', 'Correct'])
self.check_grade_percent(0.13)
# This problem is shown in an ABTest
self.submit_question_answer('H1P2', ['Correct', 'Correct'])
self.check_grade_percent(0.25)
# This problem is hidden in an ABTest. Getting it correct doesn't change total grade
self.submit_question_answer('H1P3', ['Correct', 'Correct'])
self.check_grade_percent(0.25)
# On the second homework, we only answer half of the questions.
# Then it will be dropped when homework three becomes the higher percent
# This problem is also weighted to be 4 points (instead of default of 2)
# If the problem was unweighted the percent would have been 0.38 so we
# know it works.
self.submit_question_answer('H2P1', ['Correct', 'Correct'])
self.check_grade_percent(0.42)
# Third homework
self.submit_question_answer('H3P1', ['Correct', 'Correct'])
self.check_grade_percent(0.42) # Score didn't change
self.submit_question_answer('H3P2', ['Correct', 'Correct'])
self.check_grade_percent(0.5) # Now homework2 dropped. Score changes
# Now we answer the final question (worth half of the grade)
self.submit_question_answer('FinalQuestion', ['Correct', 'Correct'])
self.check_grade_percent(1.0) # Hooray! We got 100%
...@@ -23,7 +23,7 @@ from courseware import grades ...@@ -23,7 +23,7 @@ from courseware import grades
from courseware.access import has_access from courseware.access import has_access
from courseware.courses import (get_course_with_access, get_courses_by_university) from courseware.courses import (get_course_with_access, get_courses_by_university)
import courseware.tabs as tabs import courseware.tabs as tabs
from models import StudentModuleCache from courseware.models import StudentModuleCache
from module_render import toc_for_course, get_module, get_instance_module from module_render import toc_for_course, get_module, get_instance_module
from student.models import UserProfile from student.models import UserProfile
...@@ -484,16 +484,14 @@ def progress(request, course_id, student_id=None): ...@@ -484,16 +484,14 @@ def progress(request, course_id, student_id=None):
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
course_id, student, course) course_id, student, course)
course_module = get_module(student, request, course.location,
student_module_cache, course_id)
# The course_module should be accessible, but check anyway just in case something went wrong: courseware_summary = grades.progress_summary(student, request, course,
if course_module is None: student_module_cache)
raise Http404("Course does not exist")
courseware_summary = grades.progress_summary(student, course_module,
course.grader, student_module_cache)
grade_summary = grades.grade(student, request, course, student_module_cache) grade_summary = grades.grade(student, request, course, student_module_cache)
if courseware_summary is None:
#This means the student didn't have access to the course (which the instructor requested)
raise Http404
context = {'course': course, context = {'course': course,
'courseware_summary': courseware_summary, 'courseware_summary': courseware_summary,
......
...@@ -295,7 +295,7 @@ def gradebook(request, course_id): ...@@ -295,7 +295,7 @@ def gradebook(request, course_id):
""" """
course = get_course_with_access(request.user, course_id, 'staff') course = get_course_with_access(request.user, course_id, 'staff')
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username') enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username').select_related("profile")
# TODO (vshnayder): implement pagination. # TODO (vshnayder): implement pagination.
enrolled_students = enrolled_students[:1000] # HACK! enrolled_students = enrolled_students[:1000] # HACK!
......
...@@ -50,7 +50,11 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph") ...@@ -50,7 +50,11 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph")
%> %>
<h3><a href="${reverse('courseware_section', kwargs=dict(course_id=course.id, chapter=chapter['url_name'], section=section['url_name']))}"> <h3><a href="${reverse('courseware_section', kwargs=dict(course_id=course.id, chapter=chapter['url_name'], section=section['url_name']))}">
${ section['display_name'] }</a><span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span></h3> ${ section['display_name'] }</a>
%if total > 0 or earned > 0:
<span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span>
%endif
</h3>
<p> <p>
${section['format']} ${section['format']}
......
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