Commit 6a954c1e by Nimisha Asthagiri Committed by GitHub

Merge pull request #14690 from edx/neem/fix-entrance-exam-grades

Fix grading for Entrance Exams
parents a8277a6a 54d938ca
......@@ -675,6 +675,9 @@ class CourseFields(object):
scope=Scope.settings,
)
# Note: Although users enter the entrance exam minimum score
# as a percentage value, it is internally converted and stored
# as a decimal value less than 1.
entrance_exam_minimum_score_pct = Float(
display_name=_("Entrance Exam Minimum Score (%)"),
help=_(
......
......@@ -43,118 +43,16 @@ def user_can_skip_entrance_exam(user, course):
return False
def user_has_passed_entrance_exam(request, course):
def user_has_passed_entrance_exam(user, course):
"""
Checks to see if the user has attained a sufficient score to pass the exam
Begin by short-circuiting if the course does not have an entrance exam
"""
if not course_has_entrance_exam(course):
return True
if not request.user.is_authenticated():
return False
entrance_exam_score = get_entrance_exam_score(request, course)
if entrance_exam_score >= course.entrance_exam_minimum_score_pct:
return True
return False
# pylint: disable=invalid-name
def user_must_complete_entrance_exam(request, user, course):
"""
Some courses can be gated on an Entrance Exam, which is a specially-configured chapter module which
presents users with a problem set which they must complete. This particular workflow determines
whether or not the user is allowed to clear the Entrance Exam gate and access the rest of the course.
"""
# First, let's see if the user is allowed to skip
if user_can_skip_entrance_exam(user, course):
return False
# If they can't actually skip the exam, we'll need to see if they've already passed it
if user_has_passed_entrance_exam(request, course):
if not user.is_authenticated():
return False
# Can't skip, haven't passed, must take the exam
return True
def _calculate_entrance_exam_score(user, course_descriptor, exam_modules):
"""
Calculates the score (percent) of the entrance exam using the provided modules
"""
student_module_dict = {}
scores_client = ScoresClient(course_descriptor.id, user.id)
# removing branch and version from exam modules locator
# otherwise student module would not return scores since module usage keys would not match
locations = [
BlockUsageLocator(
course_key=course_descriptor.id,
block_type=exam_module.location.block_type,
block_id=exam_module.location.block_id
)
if isinstance(exam_module.location, BlockUsageLocator) and exam_module.location.version
else exam_module.location
for exam_module in exam_modules
]
scores_client.fetch_scores(locations)
# Iterate over all of the exam modules to get score of user for each of them
for index, exam_module in enumerate(exam_modules):
exam_module_score = scores_client.get(locations[index])
if exam_module_score:
student_module_dict[unicode(locations[index])] = {
'grade': exam_module_score.correct,
'max_grade': exam_module_score.total
}
exam_percentage = 0
module_percentages = []
ignore_categories = ['course', 'chapter', 'sequential', 'vertical']
for index, module in enumerate(exam_modules):
if module.graded and module.category not in ignore_categories:
module_percentage = 0
module_location = unicode(locations[index])
if module_location in student_module_dict and student_module_dict[module_location]['max_grade']:
student_module = student_module_dict[module_location]
module_percentage = student_module['grade'] / student_module['max_grade']
module_percentages.append(module_percentage)
if module_percentages:
exam_percentage = sum(module_percentages) / float(len(module_percentages))
return exam_percentage
def get_entrance_exam_score(request, course):
"""
Gather the set of modules which comprise the entrance exam
Note that 'request' may not actually be a genuine request, due to the
circular nature of module_render calling entrance_exams and get_module_for_descriptor
being used here. In some use cases, the caller is actually mocking a request, although
in these scenarios the 'user' child object can be trusted and used as expected.
It's a much larger refactoring job to break this legacy mess apart, unfortunately.
"""
exam_key = UsageKey.from_string(course.entrance_exam_id)
exam_descriptor = modulestore().get_item(exam_key)
def inner_get_module(descriptor):
"""
Delegate to get_module_for_descriptor (imported here to avoid circular reference)
"""
from courseware.module_render import get_module_for_descriptor
field_data_cache = FieldDataCache([descriptor], course.id, request.user)
return get_module_for_descriptor(
request.user,
request,
descriptor,
field_data_cache,
course.id,
course=course
)
exam_module_generators = yield_dynamic_descriptor_descendants(
exam_descriptor,
request.user.id,
inner_get_module
)
exam_modules = [module for module in exam_module_generators]
return _calculate_entrance_exam_score(request.user, course, exam_modules)
return get_entrance_exam_content(user, course) is None
def get_entrance_exam_content(user, course):
......
......@@ -33,7 +33,7 @@ from xblock.reference.plugins import FSService
import static_replace
from courseware.access import has_access, get_user_role
from courseware.entrance_exams import (
user_must_complete_entrance_exam,
user_can_skip_entrance_exam,
user_has_passed_entrance_exam
)
from courseware.masquerade import (
......@@ -164,7 +164,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
required_content = milestones_helpers.get_required_content(course, user)
# The user may not actually have to complete the entrance exam, if one is required
if not user_must_complete_entrance_exam(request, user, course):
if user_can_skip_entrance_exam(user, course):
required_content = [content for content in required_content if not content == course.entrance_exam_id]
previous_of_active_section, next_of_active_section = None, None
......@@ -990,7 +990,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
and course \
and getattr(course, 'entrance_exam_enabled', False) \
and getattr(instance, 'in_entrance_exam', False):
ee_data = {'entrance_exam_passed': user_has_passed_entrance_exam(request, course)}
ee_data = {'entrance_exam_passed': user_has_passed_entrance_exam(request.user, course)}
resp = append_data_to_webob_response(resp, ee_data)
except NoSuchHandlerError:
......
......@@ -6,7 +6,7 @@ from django.conf import settings
from django.utils.translation import ugettext as _, ugettext_noop
from courseware.access import has_access
from courseware.entrance_exams import user_must_complete_entrance_exam
from courseware.entrance_exams import user_can_skip_entrance_exam
from openedx.core.lib.course_tabs import CourseTabPluginManager
from student.models import CourseEnrollment
from xmodule.tabs import CourseTab, CourseTabList, key_checker
......@@ -294,7 +294,7 @@ def get_course_tab_list(request, course):
# If the user has to take an entrance exam, we'll need to hide away all but the
# "Courseware" tab. The tab is then renamed as "Entrance Exam".
course_tab_list = []
must_complete_ee = user_must_complete_entrance_exam(request, user, course)
must_complete_ee = not user_can_skip_entrance_exam(user, course)
for tab in xmodule_tab_list:
if must_complete_ee:
# Hide all of the tabs except for 'Courseware'
......
......@@ -17,7 +17,6 @@ from courseware.tests.helpers import (
from courseware.entrance_exams import (
course_has_entrance_exam,
get_entrance_exam_content,
get_entrance_exam_score,
user_can_skip_entrance_exam,
user_has_passed_entrance_exam,
)
......@@ -281,32 +280,14 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
"""
exam_chapter = get_entrance_exam_content(self.request.user, self.course)
self.assertEqual(exam_chapter.url_name, self.entrance_exam.url_name)
self.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
self.assertFalse(user_has_passed_entrance_exam(self.request.user, self.course))
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
answer_entrance_exam_problem(self.course, self.request, self.problem_2)
exam_chapter = get_entrance_exam_content(self.request.user, self.course)
self.assertEqual(exam_chapter, None)
self.assertTrue(user_has_passed_entrance_exam(self.request, self.course))
def test_entrance_exam_score(self):
"""
test entrance exam score. we will hit the method get_entrance_exam_score to verify exam score.
"""
# One query is for getting the list of disabled XBlocks (which is
# then stored in the request).
with self.assertNumQueries(1):
exam_score = get_entrance_exam_score(self.request, self.course)
self.assertEqual(exam_score, 0)
answer_entrance_exam_problem(self.course, self.request, self.problem_1)
answer_entrance_exam_problem(self.course, self.request, self.problem_2)
with self.assertNumQueries(1):
exam_score = get_entrance_exam_score(self.request, self.course)
# 50 percent exam score should be achieved.
self.assertGreater(exam_score * 100, 50)
self.assertTrue(user_has_passed_entrance_exam(self.request.user, self.course))
def test_entrance_exam_requirement_message(self):
"""
......@@ -332,6 +313,10 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
minimum_score_pct = 29
self.course.entrance_exam_minimum_score_pct = float(minimum_score_pct) / 100
modulestore().update_item(self.course, self.request.user.id) # pylint: disable=no-member
# answer the problem so it results in only 20% correct.
answer_entrance_exam_problem(self.course, self.request, self.problem_1, value=1, max_value=5)
url = reverse(
'courseware_section',
kwargs={
......@@ -342,9 +327,11 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
)
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertIn('To access course materials, you must score {required_score}% or higher'.format(
required_score=minimum_score_pct
), resp.content)
self.assertIn(
'To access course materials, you must score {}% or higher'.format(minimum_score_pct),
resp.content
)
self.assertIn('Your current score is 20%.', resp.content)
def test_entrance_exam_requirement_message_hidden(self):
"""
......@@ -388,7 +375,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
resp = self.client.get(url)
self.assertNotIn('To access course materials, you must score', resp.content)
self.assertIn('You have passed the entrance exam.', resp.content)
self.assertIn('Your score is 100%. You have passed the entrance exam.', resp.content)
self.assertIn('Lesson 1', resp.content)
def test_entrance_exam_gating(self):
......@@ -450,7 +437,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
for toc_section in self.expected_unlocked_toc:
self.assertIn(toc_section, unlocked_toc)
@patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=False))
def test_courseware_page_access_without_passing_entrance_exam(self):
"""
Test courseware access page without passing entrance exam
......@@ -468,7 +454,6 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
})
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
@patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=False))
def test_courseinfo_page_access_without_passing_entrance_exam(self):
"""
Test courseware access page without passing entrance exam
......@@ -481,12 +466,11 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
exam_url = response.get('Location')
self.assertRedirects(response, exam_url)
@patch('courseware.entrance_exams.user_has_passed_entrance_exam', Mock(return_value=True))
@patch('courseware.entrance_exams.get_entrance_exam_content', Mock(return_value=None))
def test_courseware_page_access_after_passing_entrance_exam(self):
"""
Test courseware access page after passing entrance exam
"""
# Mocking get_required_content with empty list to assume user has passed entrance exam
self._assert_chapter_loaded(self.course, self.chapter)
@patch('util.milestones_helpers.get_required_content', Mock(return_value=['a value']))
......@@ -528,7 +512,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
Test has_passed_entrance_exam method with anonymous user
"""
self.request.user = self.anonymous_user
self.assertFalse(user_has_passed_entrance_exam(self.request, self.course))
self.assertFalse(user_has_passed_entrance_exam(self.request.user, self.course))
def test_course_has_entrance_exam_missing_exam_id(self):
course = CourseFactory.create(
......@@ -541,7 +525,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
def test_user_has_passed_entrance_exam_short_circuit_missing_exam(self):
course = CourseFactory.create(
)
self.assertTrue(user_has_passed_entrance_exam(self.request, course))
self.assertTrue(user_has_passed_entrance_exam(self.request.user, course))
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_MASQUERADE': False})
def test_entrance_exam_xblock_response(self):
......@@ -599,7 +583,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase, Milest
return toc['chapters']
def answer_entrance_exam_problem(course, request, problem, user=None):
def answer_entrance_exam_problem(course, request, problem, user=None, value=1, max_value=1):
"""
Takes a required milestone `problem` in a `course` and fulfills it.
......@@ -608,11 +592,13 @@ def answer_entrance_exam_problem(course, request, problem, user=None):
request (Request): request Object
problem (xblock): xblock object, the problem to be fulfilled
user (User): User object in case it is different from request.user
value (int): raw_earned value of the problem
max_value (int): raw_possible value of the problem
"""
if not user:
user = request.user
grade_dict = {'value': 1, 'max_value': 1, 'user_id': user.id}
grade_dict = {'value': value, 'max_value': max_value, 'user_id': user.id}
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id,
user,
......
......@@ -296,13 +296,11 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
"""
Returns SubsectionGrade for given url.
"""
# list of grade summaries for each section
sections_list = []
for chapter in self.get_course_grade().chapter_grades:
sections_list.extend(chapter['sections'])
# get the first section that matches the url (there should only be one)
return next(section for section in sections_list if section.url_name == hw_url_name)
for chapter in self.get_course_grade().chapter_grades.itervalues():
for section in chapter['sections']:
if section.url_name == hw_url_name:
return section
return None
def score_for_hw(self, hw_url_name):
"""
......
......@@ -22,7 +22,8 @@ import logging
import newrelic.agent
import urllib
from xblock.fragment import Fragment
from lms.djangoapps.gating.api import get_entrance_exam_score_ratio, get_entrance_exam_usage_key
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
......@@ -31,11 +32,12 @@ from shoppingcart.models import CourseRegistrationCode
from student.models import CourseEnrollment
from student.views import is_course_blocked
from student.roles import GlobalStaff
from survey.utils import must_answer_survey
from util.enterprise_helpers import get_enterprise_consent_url
from util.views import ensure_valid_course_key
from xblock.fragment import Fragment
from xmodule.modulestore.django import modulestore
from xmodule.x_module import STUDENT_VIEW
from survey.utils import must_answer_survey
from ..access import has_access, _adjust_start_date_for_beta_testers
from ..access_utils import in_preview_mode
......@@ -43,9 +45,8 @@ from ..courses import get_studio_url, get_course_with_access
from ..entrance_exams import (
course_has_entrance_exam,
get_entrance_exam_content,
get_entrance_exam_score,
user_has_passed_entrance_exam,
user_must_complete_entrance_exam,
user_can_skip_entrance_exam,
)
from ..exceptions import Redirect
from ..masquerade import setup_masquerade
......@@ -276,10 +277,7 @@ class CoursewareIndex(View):
"""
Check to see if an Entrance Exam is required for the user.
"""
if (
course_has_entrance_exam(self.course) and
user_must_complete_entrance_exam(self.request, self.effective_user, self.course)
):
if not user_can_skip_entrance_exam(self.effective_user, self.course):
exam_chapter = get_entrance_exam_content(self.effective_user, self.course)
if exam_chapter and exam_chapter.get_children():
exam_section = exam_chapter.get_children()[0]
......@@ -428,10 +426,7 @@ class CoursewareIndex(View):
)
# entrance exam data
if course_has_entrance_exam(self.course):
if getattr(self.chapter, 'is_entrance_exam', False):
courseware_context['entrance_exam_current_score'] = get_entrance_exam_score(self.request, self.course)
courseware_context['entrance_exam_passed'] = user_has_passed_entrance_exam(self.request, self.course)
self._add_entrance_exam_to_context(courseware_context)
# staff masquerading data
now = datetime.now(UTC())
......@@ -469,6 +464,17 @@ class CoursewareIndex(View):
return courseware_context
def _add_entrance_exam_to_context(self, courseware_context):
"""
Adds entrance exam related information to the given context.
"""
if course_has_entrance_exam(self.course) and getattr(self.chapter, 'is_entrance_exam', False):
courseware_context['entrance_exam_passed'] = user_has_passed_entrance_exam(self.effective_user, self.course)
courseware_context['entrance_exam_current_score'] = get_entrance_exam_score_ratio(
CourseGradeFactory().create(self.effective_user, self.course),
get_entrance_exam_usage_key(self.course),
)
def _create_section_context(self, previous_of_active_section, next_of_active_section):
"""
Returns and creates the rendering context for the section.
......
......@@ -101,7 +101,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from xmodule.tabs import CourseTabList
from xmodule.x_module import STUDENT_VIEW
from ..entrance_exams import user_must_complete_entrance_exam
from ..entrance_exams import user_can_skip_entrance_exam
from ..module_render import get_module_for_descriptor, get_module, get_module_by_usage_id
from web_fragments.fragment import Fragment
......@@ -336,7 +336,7 @@ def course_info(request, course_id):
# If the user needs to take an entrance exam to access this course, then we'll need
# to send them to that specific course module before allowing them into other areas
if user_must_complete_entrance_exam(request, user, course):
if not user_can_skip_entrance_exam(user, course):
return redirect(reverse('courseware', args=[unicode(course.id)]))
# check to see if there is a required survey that must be taken before
......@@ -857,7 +857,7 @@ def _progress(request, course_key, student_id):
student = User.objects.prefetch_related("groups").get(id=student.id)
course_grade = CourseGradeFactory().create(student, course)
courseware_summary = course_grade.chapter_grades
courseware_summary = course_grade.chapter_grades.values()
grade_summary = course_grade.summary
studio_url = get_studio_url(course, 'settings/grading')
......
......@@ -2,14 +2,12 @@
API for the gating djangoapp
"""
from collections import defaultdict
from django.test.client import RequestFactory
import json
import logging
from lms.djangoapps.courseware.entrance_exams import get_entrance_exam_score
from lms.djangoapps.courseware.entrance_exams import get_entrance_exam_content
from openedx.core.lib.gating import api as gating_api
from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.django import modulestore
from util import milestones_helpers
......@@ -53,7 +51,7 @@ def _get_minimum_required_percentage(milestone):
min_score = int(requirements.get('min_score'))
except (ValueError, TypeError):
log.warning(
'Failed to find minimum score for gating milestone %s, defaulting to 100',
u'Gating: Failed to find minimum score for gating milestone %s, defaulting to 100',
json.dumps(milestone)
)
return min_score
......@@ -63,35 +61,56 @@ def _get_subsection_percentage(subsection_grade):
"""
Returns the percentage value of the given subsection_grade.
"""
if subsection_grade.graded_total.possible:
return float(subsection_grade.graded_total.earned) / float(subsection_grade.graded_total.possible) * 100.0
else:
return 0
return _calculate_ratio(subsection_grade.graded_total.earned, subsection_grade.graded_total.possible) * 100.0
def evaluate_entrance_exam(course, subsection_grade, user):
def _calculate_ratio(earned, possible):
"""
Returns the percentage of the given earned and possible values.
"""
return float(earned) / float(possible) if possible else 0.0
def evaluate_entrance_exam(course_grade, user):
"""
Evaluates any entrance exam milestone relationships attached
to the given subsection. If the subsection_grade meets the
minimum score required, the dependent milestone will be marked
to the given course. If the course_grade meets the
minimum score required, the dependent milestones will be marked
fulfilled for the user.
"""
course = course_grade.course
if milestones_helpers.is_entrance_exams_enabled() and getattr(course, 'entrance_exam_enabled', False):
subsection = modulestore().get_item(subsection_grade.location)
in_entrance_exam = getattr(subsection, 'in_entrance_exam', False)
if in_entrance_exam:
# We don't have access to the true request object in this context, but we can use a mock
request = RequestFactory().request()
request.user = user
exam_pct = get_entrance_exam_score(request, course)
if exam_pct >= course.entrance_exam_minimum_score_pct:
exam_key = UsageKey.from_string(course.entrance_exam_id)
if get_entrance_exam_content(user, course):
exam_chapter_key = get_entrance_exam_usage_key(course)
exam_score_ratio = get_entrance_exam_score_ratio(course_grade, exam_chapter_key)
if exam_score_ratio >= course.entrance_exam_minimum_score_pct:
relationship_types = milestones_helpers.get_milestone_relationship_types()
content_milestones = milestones_helpers.get_course_content_milestones(
course.id,
exam_key,
exam_chapter_key,
relationship=relationship_types['FULFILLS']
)
# Mark each milestone dependent on the entrance exam as fulfilled by the user.
# Mark each entrance exam dependent milestone as fulfilled by the user.
for milestone in content_milestones:
milestones_helpers.add_user_milestone({'id': request.user.id}, milestone)
milestones_helpers.add_user_milestone({'id': user.id}, milestone)
def get_entrance_exam_usage_key(course):
"""
Returns the UsageKey of the entrance exam for the course.
"""
return UsageKey.from_string(course.entrance_exam_id).replace(course_key=course.id)
def get_entrance_exam_score_ratio(course_grade, exam_chapter_key):
"""
Returns the score for the given chapter as a ratio of the
aggregated earned over the possible points, resulting in a
decimal value less than 1.
"""
try:
earned, possible = course_grade.score_for_chapter(exam_chapter_key)
except KeyError:
earned, possible = 0.0, 0.0
log.warning(u'Gating: Unexpectedly failed to find chapter_grade for %s.', exam_chapter_key)
return _calculate_ratio(earned, possible)
......@@ -5,6 +5,7 @@ from django.dispatch import receiver
from gating import api as gating_api
from lms.djangoapps.grades.signals.signals import SUBSECTION_SCORE_CHANGED
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED
@receiver(SUBSECTION_SCORE_CHANGED)
......@@ -21,4 +22,18 @@ def evaluate_subsection_gated_milestones(**kwargs):
"""
subsection_grade = kwargs['subsection_grade']
gating_api.evaluate_prerequisite(kwargs['course'], subsection_grade, kwargs.get('user'))
gating_api.evaluate_entrance_exam(kwargs['course'], subsection_grade, kwargs.get('user'))
@receiver(COURSE_GRADE_CHANGED)
def evaluate_course_gated_milestones(**kwargs):
"""
Receives the COURSE_GRADE_CHANGED signal and triggers the
evaluation of any milestone relationships which are attached
to the course grade.
Arguments:
kwargs (dict): Contains user, course_grade
Returns:
None
"""
gating_api.evaluate_entrance_exam(kwargs['course_grade'], kwargs.get('user'))
......@@ -49,7 +49,7 @@ class CourseGrade(object):
a dict keyed by subsection format types.
"""
subsections_by_format = defaultdict(OrderedDict)
for chapter in self.chapter_grades:
for chapter in self.chapter_grades.itervalues():
for subsection_grade in chapter['sections']:
if subsection_grade.graded:
graded_total = subsection_grade.graded_total
......@@ -63,7 +63,7 @@ class CourseGrade(object):
Returns a dict of problem scores keyed by their locations.
"""
locations_to_scores = {}
for chapter in self.chapter_grades:
for chapter in self.chapter_grades.itervalues():
for subsection_grade in chapter['sections']:
locations_to_scores.update(subsection_grade.locations_to_scores)
return locations_to_scores
......@@ -88,10 +88,12 @@ class CourseGrade(object):
@lazy
def chapter_grades(self):
"""
Returns a list of chapters, each containing its subsection grades,
display name, and url name.
Returns a dictionary of dictionaries.
The primary dictionary is keyed by the chapter's usage_key.
The secondary dictionary contains the chapter's
subsection grades, display name, and url name.
"""
chapter_grades = []
chapter_grades = OrderedDict()
for chapter_key in self.course_structure.get_children(self.course.location):
chapter = self.course_structure[chapter_key]
chapter_subsection_grades = []
......@@ -101,11 +103,11 @@ class CourseGrade(object):
self._subsection_grade_factory.create(self.course_structure[subsection_key], read_only=True)
)
chapter_grades.append({
chapter_grades[chapter_key] = {
'display_name': block_metadata_utils.display_name_with_default_escaped(chapter),
'url_name': block_metadata_utils.url_name_for_block(chapter),
'sections': chapter_subsection_grades
})
}
return chapter_grades
@property
......@@ -152,7 +154,7 @@ class CourseGrade(object):
If read_only is True, doesn't save any updates to the grades.
"""
subsections_total = sum(len(chapter['sections']) for chapter in self.chapter_grades)
subsections_total = sum(len(chapter['sections']) for chapter in self.chapter_grades.itervalues())
total_graded_subsections = sum(len(x) for x in self.graded_subsections_by_format.itervalues())
subsections_created = len(self._subsection_grade_factory._unsaved_subsection_grades) # pylint: disable=protected-access
......@@ -187,6 +189,19 @@ class CourseGrade(object):
)
)
def score_for_chapter(self, chapter_key):
"""
Returns the aggregate weighted score for the given chapter.
Raises:
KeyError if the chapter is not found.
"""
earned, possible = 0.0, 0.0
chapter_grade = self.chapter_grades[chapter_key]
for section in chapter_grade['sections']:
earned += section.graded_total.earned
possible += section.graded_total.possible
return earned, possible
def score_for_module(self, location):
"""
Calculate the aggregate weighted score for any location in the course.
......@@ -201,8 +216,7 @@ class CourseGrade(object):
score = self.locations_to_scores[location]
return score.earned, score.possible
children = self.course_structure.get_children(location)
earned = 0.0
possible = 0.0
earned, possible = 0.0, 0.0
for child in children:
child_earned, child_possible = self.score_for_module(child)
earned += child_earned
......
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