Commit 51133c5a by Eric Fischer Committed by GitHub

Merge pull request #14771 from edx/neem/grade-only-for-engaged

Grade only engaged learners
parents 02f67150 1503e5f7
......@@ -115,7 +115,7 @@ class CourseEndingTest(TestCase):
)
cert_status = {'status': 'generating', 'grade': '67', 'mode': 'honor'}
with patch('lms.djangoapps.grades.new.course_grade.CourseGradeFactory.get_persisted') as patch_persisted_grade:
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.read') as patch_persisted_grade:
patch_persisted_grade.return_value = Mock(percent=100)
self.assertEqual(
_cert_info(user, course, cert_status, course_mode),
......
......@@ -68,7 +68,7 @@ from certificates.api import ( # pylint: disable=import-error
get_certificate_url,
has_html_certificates_enabled,
)
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from xmodule.modulestore.django import modulestore
from opaque_keys import InvalidKeyError
......@@ -423,7 +423,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
)
if status in {'generating', 'ready', 'notpassing', 'restricted', 'auditing', 'unverified'}:
persisted_grade = CourseGradeFactory().get_persisted(user, course_overview)
persisted_grade = CourseGradeFactory().read(user, course=course_overview)
if persisted_grade is not None:
status_dict['grade'] = unicode(persisted_grade.percent)
elif 'grade' in cert_status:
......
......@@ -14,7 +14,7 @@ class ProgressPage(CoursePage):
def is_browser_on_page(self):
is_present = (
self.q(css='.course-info').present and
self.q(css='#grade-detail-graph').present
self.q(css='.grade-detail-graph').present
)
return is_present
......@@ -115,7 +115,7 @@ class ProgressPage(CoursePage):
Return the CSS index of the chapter with `title`.
Returns `None` if it cannot find such a chapter.
"""
chapter_css = '.chapters section .hd'
chapter_css = '.chapters>section h3'
chapter_titles = self.q(css=chapter_css).map(lambda el: el.text.lower().strip()).results
try:
......
......@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default,
# # of mongo queries,
# )
('no_overrides', 1, True, False): (24, 6),
('no_overrides', 2, True, False): (24, 6),
('no_overrides', 3, True, False): (24, 6),
('ccx', 1, True, False): (24, 6),
('ccx', 2, True, False): (24, 6),
('ccx', 3, True, False): (24, 6),
('no_overrides', 1, False, False): (24, 6),
('no_overrides', 2, False, False): (24, 6),
('no_overrides', 3, False, False): (24, 6),
('ccx', 1, False, False): (24, 6),
('ccx', 2, False, False): (24, 6),
('ccx', 3, False, False): (24, 6),
('no_overrides', 1, True, False): (25, 1),
('no_overrides', 2, True, False): (25, 1),
('no_overrides', 3, True, False): (25, 1),
('ccx', 1, True, False): (25, 1),
('ccx', 2, True, False): (25, 1),
('ccx', 3, True, False): (25, 1),
('no_overrides', 1, False, False): (25, 1),
('no_overrides', 2, False, False): (25, 1),
('no_overrides', 3, False, False): (25, 1),
('ccx', 1, False, False): (25, 1),
('ccx', 2, False, False): (25, 1),
('ccx', 3, False, False): (25, 1),
}
......@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True
TEST_DATA = {
('no_overrides', 1, True, False): (24, 3),
('no_overrides', 2, True, False): (24, 3),
('no_overrides', 3, True, False): (24, 3),
('ccx', 1, True, False): (24, 3),
('ccx', 2, True, False): (24, 3),
('ccx', 3, True, False): (24, 3),
('ccx', 1, True, True): (25, 3),
('ccx', 2, True, True): (25, 3),
('ccx', 3, True, True): (25, 3),
('no_overrides', 1, False, False): (24, 3),
('no_overrides', 2, False, False): (24, 3),
('no_overrides', 3, False, False): (24, 3),
('ccx', 1, False, False): (24, 3),
('ccx', 2, False, False): (24, 3),
('ccx', 3, False, False): (24, 3),
('no_overrides', 1, True, False): (25, 3),
('no_overrides', 2, True, False): (25, 3),
('no_overrides', 3, True, False): (25, 3),
('ccx', 1, True, False): (25, 3),
('ccx', 2, True, False): (25, 3),
('ccx', 3, True, False): (25, 3),
('ccx', 1, True, True): (26, 3),
('ccx', 2, True, True): (26, 3),
('ccx', 3, True, True): (26, 3),
('no_overrides', 1, False, False): (25, 3),
('no_overrides', 2, False, False): (25, 3),
('no_overrides', 3, False, False): (25, 3),
('ccx', 1, False, False): (25, 3),
('ccx', 2, False, False): (25, 3),
('ccx', 3, False, False): (25, 3),
}
......@@ -33,7 +33,7 @@ from courseware.field_overrides import disable_overrides
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR, assign_role
from django_comment_common.utils import seed_permissions_roles
from edxmako.shortcuts import render_to_response
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from opaque_keys.edx.keys import CourseKey
from ccx_keys.locator import CCXLocator
from student.roles import CourseCcxCoachRole
......
......@@ -7,7 +7,7 @@ from optparse import make_option
from certificates.models import GeneratedCertificate
from courseware import courses
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
log = logging.getLogger(__name__)
......
......@@ -11,7 +11,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from requests.auth import HTTPBasicAuth
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from xmodule.modulestore.django import modulestore
from capa.xqueue_interface import XQueueInterface
from capa.xqueue_interface import make_xheader, make_hashkey
......@@ -271,7 +271,7 @@ class XQueueCertInterface(object):
self.request.session = {}
is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
grade = CourseGradeFactory().create(student, course).summary
course_grade = CourseGradeFactory().create(student, course)
enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
......@@ -295,8 +295,6 @@ class XQueueCertInterface(object):
else:
# honor code and audit students
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
if forced_grade:
grade['grade'] = forced_grade
LOGGER.info(
(
......@@ -317,13 +315,13 @@ class XQueueCertInterface(object):
cert.mode = cert_mode
cert.user = student
cert.grade = grade['percent']
cert.grade = course_grade.percent
cert.course_id = course_id
cert.name = profile_name
cert.download_url = ''
# Strip HTML from grade range label
grade_contents = grade.get('grade', None)
grade_contents = forced_grade or course_grade.letter_grade
try:
grade_contents = lxml.html.fromstring(grade_contents).text_content()
passing = True
......
......@@ -22,7 +22,7 @@ from capa.tests.response_xml_factory import (
from course_modes.models import CourseMode
from courseware.models import StudentModule, BaseStudentModuleHistory
from courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from openedx.core.djangoapps.credit.api import (
set_credit_requirements, get_credit_requirement_status
)
......
......@@ -86,7 +86,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
# The student progress tab is not accessible to a student
# before launch, so the instructor view-as-student feature
# should return a 403.
# should return a 404.
# TODO (vshnayder): If this is not the behavior we want, will need
# to make access checking smarter and understand both the effective
# user (the student), and the requesting user (the prof)
......@@ -97,7 +97,7 @@ class TestViewAuth(ModuleStoreTestCase, LoginEnrollmentTestCase):
'student_id': self.enrolled_user.id,
}
)
self.assert_request_status_code(403, url)
self.assert_request_status_code(404, url)
# The courseware url should redirect, not 200
url = self._reverse_urls(['courseware'], course)[0]
......
......@@ -46,6 +46,7 @@ from courseware.tests.factories import StudentModuleFactory, GlobalStaffFactory
from courseware.url_helpers import get_redirect_url
from courseware.user_state_client import DjangoXBlockUserStateClient
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
from lms.djangoapps.grades.config.waffle import waffle as grades_waffle, ASSUME_ZERO_GRADE_IF_ABSENT
from milestones.tests.utils import MilestonesTestCaseMixin
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, CourseRunFactory
......@@ -1440,19 +1441,25 @@ class ProgressPageTests(ModuleStoreTestCase):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced)
with self.assertNumQueries(41), check_mongo_calls(4):
with self.assertNumQueries(42), check_mongo_calls(1):
self._get_progress_page()
def test_progress_queries(self):
@ddt.data(
(False, 42, 28),
(True, 35, 24)
)
@ddt.unpack
def test_progress_queries(self, enable_waffle, initial, subsequent):
self.setup_course()
with self.assertNumQueries(41), check_mongo_calls(4):
self._get_progress_page()
# subsequent accesses to the progress page require fewer queries.
for _ in range(2):
with self.assertNumQueries(27), check_mongo_calls(4):
with grades_waffle().override_in_model(ASSUME_ZERO_GRADE_IF_ABSENT, active=enable_waffle):
with self.assertNumQueries(initial), check_mongo_calls(1):
self._get_progress_page()
# subsequent accesses to the progress page require fewer queries.
for _ in range(2):
with self.assertNumQueries(subsequent), check_mongo_calls(1):
self._get_progress_page()
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
......
......@@ -26,7 +26,7 @@ import urllib
import waffle
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 lms.djangoapps.grades.new.course_grade_factory 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
......
......@@ -40,7 +40,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
from rest_framework import status
from lms.djangoapps.instructor.views.api import require_global_staff
from lms.djangoapps.ccx.utils import prep_course_for_grading
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from lms.djangoapps.instructor.enrollment import uses_shib
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
......@@ -832,8 +832,7 @@ def _progress(request, course_key, student_id):
except ValueError:
raise Http404
course = get_course_with_access(request.user, 'load', course_key, depth=None, check_if_enrolled=True)
prep_course_for_grading(course, request)
course = get_course_with_access(request.user, 'load', course_key)
# check to see if there is a required survey that must be taken before
# the user can access the course.
......@@ -861,12 +860,16 @@ def _progress(request, course_key, student_id):
except User.DoesNotExist:
raise Http404
# NOTE: To make sure impersonation by instructor works, use
# student instead of request.user in the rest of the function.
# The pre-fetching of groups is done to make auth checks not require an
# additional DB lookup (this kills the Progress page in particular).
student = User.objects.prefetch_related("groups").get(id=student.id)
if request.user.id != student.id:
# refetch the course as the assumed student
course = get_course_with_access(student, 'load', course_key, check_if_enrolled=True)
prep_course_for_grading(course, request)
# NOTE: To make sure impersonation by instructor works, use
# student instead of request.user in the rest of the function.
course_grade = CourseGradeFactory().create(student, course)
courseware_summary = course_grade.chapter_grades.values()
......
......@@ -78,7 +78,7 @@ def evaluate_entrance_exam(course_grade, user):
minimum score required, the dependent milestones will be marked
fulfilled for the user.
"""
course = course_grade.course
course = course_grade.course_data.course
if milestones_helpers.is_entrance_exams_enabled() and getattr(course, 'entrance_exam_enabled', False):
if get_entrance_exam_content(user, course):
exam_chapter_key = get_entrance_exam_usage_key(course)
......
......@@ -7,7 +7,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.grades.tests.utils import answer_problem
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from milestones import api as milestones_api
from milestones.tests.utils import MilestonesTestCaseMixin
from openedx.core.djangolib.testing.utils import get_mock_request
......
......@@ -14,7 +14,7 @@ from courseware.access import has_access
from lms.djangoapps.ccx.utils import prep_course_for_grading
from lms.djangoapps.courseware import courses
from lms.djangoapps.grades.api.serializers import GradingPolicySerializer
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
from student.roles import CourseStaffRole
......
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
from lms.djangoapps.grades.config.waffle import waffle, ASSUME_ZERO_GRADE_IF_ABSENT
def assume_zero_if_absent(course_key):
"""
Returns whether an absent grade should be assumed to be zero.
"""
return should_persist_grades(course_key) and waffle().is_enabled(ASSUME_ZERO_GRADE_IF_ABSENT)
def should_persist_grades(course_key):
"""
Returns whether grades should be persisted.
"""
return PersistentGradesEnabledFlag.feature_enabled(course_key)
"""
This module contains various configuration settings via
waffle switches for the Grades app.
"""
from openedx.core.djangolib.waffle_utils import WaffleSwitchPlus
# Namespace
WAFFLE_NAMESPACE = u'grades'
# Switches
WRITE_ONLY_IF_ENGAGED = u'write_only_if_engaged'
ASSUME_ZERO_GRADE_IF_ABSENT = u'assume_zero_grade_if_absent'
def waffle():
"""
Returns the namespaced, cached, audited Waffle class for Grades.
"""
return WaffleSwitchPlus(namespace=WAFFLE_NAMESPACE, log_prefix=u'Grades: ')
......@@ -101,7 +101,7 @@ class Command(BaseCommand):
kwargs=kwargs,
options=task_options,
)
log.info("Persistent grades: Created {task_name}[{task_id}] with arguments {kwargs}".format(
log.info("Grades: Created {task_name}[{task_id}] with arguments {kwargs}".format(
task_name=tasks.compute_grades_for_course.name,
task_id=result.task_id,
kwargs=kwargs,
......
......@@ -7,7 +7,7 @@ from django.core.management.base import BaseCommand, CommandError
import os
from lms.djangoapps.courseware import courses
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
......
from lms.djangoapps.course_blocks.api import get_course_blocks
from xmodule.modulestore.django import modulestore
from ..transformer import GradesTransformer
class CourseData(object):
"""
Utility access layer to intelligently get and cache the
requested course data as long as at least one property is
provided upon initialization.
This is an in-memory object that maintains its own internal
cache during its lifecycle.
"""
def __init__(self, user, course=None, collected_block_structure=None, structure=None, course_key=None):
if not any([course, collected_block_structure, structure, course_key]):
raise ValueError(
"You must specify one of course, collected_block_structure, structure, or course_key to this method."
)
self.user = user
self._collected_block_structure = collected_block_structure
self._structure = structure
self._course = course
self._course_key = course_key
self._location = None
@property
def course_key(self):
if not self._course_key:
if self._course:
self._course_key = self._course.id
else:
structure = self.effective_structure
self._course_key = structure.root_block_usage_key.course_key
return self._course_key
@property
def location(self):
if not self._location:
structure = self.effective_structure
if structure:
self._location = structure.root_block_usage_key
elif self._course:
self._location = self._course.location
else:
self._location = modulestore().make_course_usage_key(self.course_key)
return self._location
@property
def structure(self):
if not self._structure:
self._structure = get_course_blocks(
self.user,
self.location,
collected_block_structure=self._collected_block_structure,
)
return self._structure
@property
def course(self):
if not self._course:
self._course = modulestore().get_course(self.course_key)
return self._course
@property
def grading_policy_hash(self):
structure = self.effective_structure
if structure:
return structure.get_transformer_block_field(
structure.root_block_usage_key,
GradesTransformer,
'grading_policy_hash',
)
else:
return GradesTransformer.grading_policy_hash(self.course)
@property
def version(self):
structure = self.effective_structure
course_block = structure[self.location] if structure else self.course
return getattr(course_block, 'course_version', None)
@property
def edited_on(self):
# get course block from structure only; subtree_edited_on field on modulestore's course block isn't optimized.
course_block = self.structure[self.location]
return getattr(course_block, 'subtree_edited_on', None)
@property
def effective_structure(self):
return self._structure or self._collected_block_structure
def __unicode__(self):
return u'Course: course_key: {}'.format(self.course_key)
def full_string(self):
if self.effective_structure:
return u'Course: course_key: {}, version: {}, edited_on: {}, grading_policy: {}'.format(
self.course_key, self.version, self.edited_on, self.grading_policy_hash,
)
else:
return u'Course: course_key: {}, empty course structure'.format(self.course_key)
from collections import namedtuple
import dogstats_wrapper as dog_stats_api
from logging import getLogger
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED
from ..config import assume_zero_if_absent, should_persist_grades
from ..config.waffle import waffle, WRITE_ONLY_IF_ENGAGED
from ..models import PersistentCourseGrade
from .course_data import CourseData
from .course_grade import CourseGrade, ZeroCourseGrade
log = getLogger(__name__)
class CourseGradeFactory(object):
"""
Factory class to create Course Grade objects.
"""
GradeResult = namedtuple('GradeResult', ['student', 'course_grade', 'err_msg'])
def create(self, user, course=None, collected_block_structure=None, course_structure=None, course_key=None):
"""
Returns the CourseGrade for the given user in the course.
Reads the value from storage and validates that the grading
policy hasn't changed since the grade was last computed.
If not in storage, returns a ZeroGrade if ASSUME_ZERO_GRADE_IF_ABSENT.
Else, if changed or not in storage, computes and returns a new value.
At least one of course, collected_block_structure, course_structure,
or course_key should be provided.
"""
course_data = CourseData(user, course, collected_block_structure, course_structure, course_key)
try:
course_grade, read_policy_hash = self._read(user, course_data)
if read_policy_hash == course_data.grading_policy_hash:
return course_grade
read_only = False # update the persisted grade since the policy changed; TODO(TNL-6786) remove soon
except PersistentCourseGrade.DoesNotExist:
if assume_zero_if_absent(course_data.course_key):
return self._create_zero(user, course_data)
read_only = True # keep the grade un-persisted; TODO(TNL-6786) remove once all grades are backfilled
return self._update(user, course_data, read_only)
def read(self, user, course=None, collected_block_structure=None, course_structure=None, course_key=None):
"""
Returns the CourseGrade for the given user in the course as
persisted in storage. Does NOT verify whether the grading
policy is still valid since the grade was last computed.
If not in storage, returns a ZeroGrade if ASSUME_ZERO_GRADE_IF_ABSENT
else returns None.
At least one of course, collected_block_structure, course_structure,
or course_key should be provided.
"""
course_data = CourseData(user, course, collected_block_structure, course_structure, course_key)
try:
course_grade, _ = self._read(user, course_data)
return course_grade
except PersistentCourseGrade.DoesNotExist:
if assume_zero_if_absent(course_data.course_key):
return self._create_zero(user, course_data)
else:
return None
def update(self, user, course=None, collected_block_structure=None, course_structure=None, course_key=None):
"""
Computes, updates, and returns the CourseGrade for the given
user in the course.
At least one of course, collected_block_structure, course_structure,
or course_key should be provided.
"""
course_data = CourseData(user, course, collected_block_structure, course_structure, course_key)
return self._update(user, course_data, read_only=False)
def iter(self, course, students, force_update=False):
"""
Given a course and an iterable of students (User), yield a GradeResult
for every student enrolled in the course. GradeResult is a named tuple of:
(student, course_grade, err_msg)
If an error occurred, course_grade will be None and err_msg will be an
exception message. If there was no error, err_msg is an empty string.
"""
# Pre-fetch the collected course_structure so:
# 1. Correctness: the same version of the course is used to
# compute the grade for all students.
# 2. Optimization: the collected course_structure is not
# retrieved from the data store multiple times.
collected_block_structure = get_block_structure_manager(course.id).get_collected()
for student in students:
with dog_stats_api.timer('lms.grades.CourseGradeFactory.iter', tags=[u'action:{}'.format(course.id)]):
try:
operation = CourseGradeFactory().update if force_update else CourseGradeFactory().create
course_grade = operation(student, course, collected_block_structure)
yield self.GradeResult(student, course_grade, "")
except Exception as exc: # pylint: disable=broad-except
# Keep marching on even if this student couldn't be graded for
# some reason, but log it for future reference.
log.exception(
'Cannot grade student %s (%s) in course %s because of exception: %s',
student.username,
student.id,
course.id,
exc.message
)
yield self.GradeResult(student, None, exc.message)
@staticmethod
def _create_zero(user, course_data):
"""
Returns a ZeroCourseGrade object for the given user and course.
"""
log.info(u'Grades: CreateZero, %s, User: %s', unicode(course_data), user.id)
return ZeroCourseGrade(user, course_data)
@staticmethod
def _read(user, course_data):
"""
Returns a CourseGrade object based on stored grade information
for the given user and course.
"""
if not should_persist_grades(course_data.course_key):
raise PersistentCourseGrade.DoesNotExist
persistent_grade = PersistentCourseGrade.read_course_grade(user.id, course_data.course_key)
course_grade = CourseGrade(
user,
course_data,
persistent_grade.percent_grade,
persistent_grade.letter_grade,
persistent_grade.passed_timestamp is not None,
)
log.info(u'Grades: Read, %s, User: %s, %s', unicode(course_data), user.id, persistent_grade)
return course_grade, persistent_grade.grading_policy_hash
@staticmethod
def _update(user, course_data, read_only):
"""
Computes, saves, and returns a CourseGrade object for the
given user and course.
Sends a COURSE_GRADE_CHANGED signal to listeners.
"""
course_grade = CourseGrade(user, course_data)
course_grade.update()
should_persist = (
not read_only and # TODO(TNL-6786) Remove the read_only boolean once all grades are back-filled.
should_persist_grades(course_data.course_key) and
not waffle().is_enabled(WRITE_ONLY_IF_ENGAGED) or course_grade.attempted
)
if should_persist:
course_grade._subsection_grade_factory.bulk_create_unsaved()
PersistentCourseGrade.update_or_create_course_grade(
user_id=user.id,
course_id=course_data.course_key,
course_version=course_data.version,
course_edited_timestamp=course_data.edited_on,
grading_policy_hash=course_data.grading_policy_hash,
percent_grade=course_grade.percent,
letter_grade=course_grade.letter_grade or "",
passed=course_grade.passed,
)
COURSE_GRADE_CHANGED.send_robust(
sender=None,
user=user,
course_grade=course_grade,
course_key=course_data.course_key,
deadline=course_data.course.end,
)
log.info(
u'Grades: Update, %s, User: %s, %s, persisted: %s',
course_data.full_string(), user.id, course_grade, should_persist,
)
return course_grade
from lazy import lazy
from logging import getLogger
from courseware.model_data import ScoresClient
from openedx.core.lib.grade_utils import is_score_higher_or_equal
from student.models import anonymous_id_for_user
from submissions import api as submissions_api
from lms.djangoapps.grades.config import should_persist_grades, assume_zero_if_absent
from lms.djangoapps.grades.models import PersistentSubsectionGrade
from lms.djangoapps.grades.scores import possibly_scored
from .course_data import CourseData
from .subsection_grade import SubsectionGrade, ZeroSubsectionGrade
log = getLogger(__name__)
class SubsectionGradeFactory(object):
"""
Factory for Subsection Grades.
"""
def __init__(self, student, course=None, course_structure=None, course_data=None):
self.student = student
self.course_data = course_data or CourseData(student, course=course, structure=course_structure)
self._cached_subsection_grades = None
self._unsaved_subsection_grades = []
def create(self, subsection, read_only=False):
"""
Returns the SubsectionGrade object for the student and subsection.
If read_only is True, doesn't save any updates to the grades.
"""
self._log_event(
log.debug, u"create, read_only: {0}, subsection: {1}".format(read_only, subsection.location), subsection,
)
subsection_grade = self._get_bulk_cached_grade(subsection)
if not subsection_grade:
if assume_zero_if_absent(self.course_data.course_key):
subsection_grade = ZeroSubsectionGrade(subsection, self.course_data)
else:
subsection_grade = SubsectionGrade(subsection).init_from_structure(
self.student, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
if should_persist_grades(self.course_data.course_key):
if read_only:
self._unsaved_subsection_grades.append(subsection_grade)
else:
grade_model = subsection_grade.create_model(self.student)
self._update_saved_subsection_grade(subsection.location, grade_model)
return subsection_grade
def bulk_create_unsaved(self):
"""
Bulk creates all the unsaved subsection_grades to this point.
"""
SubsectionGrade.bulk_create_models(self.student, self._unsaved_subsection_grades, self.course_data.course_key)
self._unsaved_subsection_grades = []
def update(self, subsection, only_if_higher=None):
"""
Updates the SubsectionGrade object for the student and subsection.
"""
# Save ourselves the extra queries if the course does not persist
# subsection grades.
self._log_event(log.warning, u"update, subsection: {}".format(subsection.location), subsection)
calculated_grade = SubsectionGrade(subsection).init_from_structure(
self.student, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
if should_persist_grades(self.course_data.course_key):
if only_if_higher:
try:
grade_model = PersistentSubsectionGrade.read_grade(self.student.id, subsection.location)
except PersistentSubsectionGrade.DoesNotExist:
pass
else:
orig_subsection_grade = SubsectionGrade(subsection).init_from_model(
self.student, grade_model, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
if not is_score_higher_or_equal(
orig_subsection_grade.graded_total.earned,
orig_subsection_grade.graded_total.possible,
calculated_grade.graded_total.earned,
calculated_grade.graded_total.possible,
):
return orig_subsection_grade
grade_model = calculated_grade.update_or_create_model(self.student)
self._update_saved_subsection_grade(subsection.location, grade_model)
return calculated_grade
@lazy
def _csm_scores(self):
"""
Lazily queries and returns all the scores stored in the user
state (in CSM) for the course, while caching the result.
"""
scorable_locations = [block_key for block_key in self.course_data.structure if possibly_scored(block_key)]
return ScoresClient.create_for_locations(self.course_data.course_key, self.student.id, scorable_locations)
@lazy
def _submissions_scores(self):
"""
Lazily queries and returns the scores stored by the
Submissions API for the course, while caching the result.
"""
anonymous_user_id = anonymous_id_for_user(self.student, self.course_data.course_key)
return submissions_api.get_scores(str(self.course_data.course_key), anonymous_user_id)
def _get_bulk_cached_grade(self, subsection):
"""
Returns the student's SubsectionGrade for the subsection,
while caching the results of a bulk retrieval for the
course, for future access of other subsections.
Returns None if not found.
"""
if should_persist_grades(self.course_data.course_key):
saved_subsection_grades = self._get_bulk_cached_subsection_grades()
subsection_grade = saved_subsection_grades.get(subsection.location)
if subsection_grade:
return SubsectionGrade(subsection).init_from_model(
self.student, subsection_grade, self.course_data.structure, self._submissions_scores, self._csm_scores,
)
def _get_bulk_cached_subsection_grades(self):
"""
Returns and caches (for future access) the results of
a bulk retrieval of all subsection grades in the course.
"""
if self._cached_subsection_grades is None:
self._cached_subsection_grades = {
record.full_usage_key: record
for record in PersistentSubsectionGrade.bulk_read_grades(self.student.id, self.course_data.course_key)
}
return self._cached_subsection_grades
def _update_saved_subsection_grade(self, subsection_usage_key, subsection_model):
"""
Updates (or adds) the subsection grade for the given
subsection usage key in the local cache, iff the cache
is populated.
"""
if self._cached_subsection_grades is not None:
self._cached_subsection_grades[subsection_usage_key] = subsection_model
def _log_event(self, log_func, log_statement, subsection):
"""
Logs the given statement, for this instance.
"""
log_func(u"Grades: SGF.{}, course: {}, version: {}, edit: {}, user: {}".format(
log_statement,
self.course_data.course_key,
getattr(subsection, 'course_version', None),
getattr(subsection, 'subtree_edited_on', None),
self.student.id,
))
......@@ -5,7 +5,6 @@ from logging import getLogger
from openedx.core.lib.cache_utils import memoized
from xblock.core import XBlock
from xmodule.block_metadata_utils import display_name_with_default_escaped
from xmodule.graders import ProblemScore
from .transformer import GradesTransformer
......
......@@ -26,7 +26,7 @@ from .signals import (
SCORE_PUBLISHED,
)
from ..constants import ScoreDatabaseTableEnum
from ..new.course_grade import CourseGradeFactory
from ..new.course_grade_factory import CourseGradeFactory
from ..scores import weighted_score
from ..tasks import recalculate_subsection_grade_v3, RECALCULATE_GRADE_DELAY
......@@ -238,7 +238,7 @@ def recalculate_course_grade(sender, course, course_structure, user, **kwargs):
"""
Updates a saved course grade.
"""
CourseGradeFactory().update(user, course, course_structure)
CourseGradeFactory().update(user, course=course, course_structure=course_structure)
def _emit_problem_submitted_event(kwargs):
......
......@@ -31,8 +31,8 @@ from util.date_utils import from_timestamp
from xmodule.modulestore.django import modulestore
from .constants import ScoreDatabaseTableEnum
from .new.subsection_grade import SubsectionGradeFactory
from .new.course_grade import CourseGradeFactory
from .new.subsection_grade_factory import SubsectionGradeFactory
from .new.course_grade_factory import CourseGradeFactory
from .signals.signals import SUBSECTION_SCORE_CHANGED
from .transformer import GradesTransformer
......@@ -73,11 +73,7 @@ def compute_grades_for_course(course_key, offset, batch_size):
course = courses.get_course_by_id(CourseKey.from_string(course_key))
enrollments = CourseEnrollment.objects.filter(course_id=course.id).order_by('created')
student_iter = (enrollment.user for enrollment in enrollments[offset:offset + batch_size])
list(CourseGradeFactory().iter(
course,
students=student_iter,
read_only=False,
))
list(CourseGradeFactory().iter(course, students=student_iter, force_update=True))
@task(bind=True, base=_BaseTask, default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY)
......@@ -182,7 +178,7 @@ def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs):
if not db_is_updated:
log.info(
u"Persistent Grades: tasks._has_database_updated_with_new_score is False. Task ID: {}. Kwargs: {}. Found "
u"Grades: tasks._has_database_updated_with_new_score is False. Task ID: {}. Kwargs: {}. Found "
u"modified time: {}".format(
self.request.id,
kwargs,
......
......@@ -15,7 +15,7 @@ from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum
from ...new.subsection_grade import SubsectionGradeFactory
from ...new.subsection_grade_factory import SubsectionGradeFactory
class GradesAccessIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTestCase):
......
"""
Tests for CourseData utility class.
"""
from lms.djangoapps.course_blocks.api import get_course_blocks
from mock import patch
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ..new.course_data import CourseData
class CourseDataTest(ModuleStoreTestCase):
"""
Simple tests to ensure CourseData works as advertised.
"""
def setUp(self):
super(CourseDataTest, self).setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create()
self.one_true_structure = get_course_blocks(self.user, self.course.location)
self.expected_results = {
'course': self.course,
'collected_block_structure': self.one_true_structure,
'structure': self.one_true_structure,
'course_key': self.course.id,
'location': self.course.location,
}
@patch('lms.djangoapps.grades.new.course_data.get_course_blocks')
def test_fill_course_data(self, mock_get_blocks):
"""
Tests to ensure that course data is fully filled with just a single input.
"""
mock_get_blocks.return_value = self.one_true_structure
for kwarg in self.expected_results: # We iterate instead of ddt due to dependence on 'self'
if kwarg == 'location':
continue # This property is purely output; it's never able to be used as input
kwargs = {kwarg: self.expected_results[kwarg]}
course_data = CourseData(self.user, **kwargs)
for arg in self.expected_results:
# No point validating the data we used as input, and c_b_s is input-only
if arg != kwarg and arg != "collected_block_structure":
expected = self.expected_results[arg]
actual = getattr(course_data, arg)
self.assertEqual(expected, actual)
def test_no_data(self):
"""
Tests to ensure ??? happens when none of the data are provided.
Maybe a dict pairing asked-for properties to resulting exceptions? Or an exception on init?
"""
with self.assertRaises(ValueError):
_ = CourseData(self.user)
......@@ -19,8 +19,8 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from .utils import answer_problem
from ..new.course_grade import CourseGradeFactory
from ..new.subsection_grade import SubsectionGradeFactory
from ..new.course_grade_factory import CourseGradeFactory
from ..new.subsection_grade_factory import SubsectionGradeFactory
@attr(shard=1)
......@@ -78,7 +78,7 @@ class TestGradeIteration(SharedModuleStoreTestCase):
self.assertIsNone(course_grade.letter_grade)
self.assertEqual(course_grade.percent, 0.0)
@patch('lms.djangoapps.grades.new.course_grade.CourseGradeFactory.create')
@patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create')
def test_grading_exception(self, mock_course_grade):
"""Test that we correctly capture exception messages that bubble up from
grading. Note that we only see errors at this level if the grading
......
......@@ -154,10 +154,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertEquals(mock_block_structure_create.call_count, 1)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 26, True),
(ModuleStoreEnum.Type.mongo, 1, 23, False),
(ModuleStoreEnum.Type.split, 3, 25, True),
(ModuleStoreEnum.Type.split, 3, 22, False),
(ModuleStoreEnum.Type.mongo, 1, 28, True),
(ModuleStoreEnum.Type.mongo, 1, 24, False),
(ModuleStoreEnum.Type.split, 3, 27, True),
(ModuleStoreEnum.Type.split, 3, 23, False),
)
@ddt.unpack
def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections):
......@@ -169,8 +169,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self._apply_recalculate_subsection_grade()
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 26),
(ModuleStoreEnum.Type.split, 3, 25),
(ModuleStoreEnum.Type.mongo, 1, 28),
(ModuleStoreEnum.Type.split, 3, 27),
)
@ddt.unpack
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
......@@ -230,8 +230,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertEqual(len(PersistentSubsectionGrade.bulk_read_grades(self.user.id, self.course.id)), 0)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 24),
(ModuleStoreEnum.Type.split, 3, 23),
(ModuleStoreEnum.Type.mongo, 1, 25),
(ModuleStoreEnum.Type.split, 3, 24),
)
@ddt.unpack
def test_persistent_grades_enabled_on_course(self, default_store, num_mongo_queries, num_sql_queries):
......@@ -244,7 +244,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertGreater(len(PersistentSubsectionGrade.bulk_read_grades(self.user.id, self.course.id)), 0)
@patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
@patch('lms.djangoapps.grades.new.subsection_grade_factory.SubsectionGradeFactory.update')
def test_retry_first_time_only(self, mock_update, mock_course_signal):
"""
Ensures that a task retry completes after a one-time failure.
......@@ -255,7 +255,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertEquals(mock_course_signal.call_count, 1)
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.retry')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
@patch('lms.djangoapps.grades.new.subsection_grade_factory.SubsectionGradeFactory.update')
def test_retry_on_integrity_error(self, mock_update, mock_retry):
"""
Ensures that tasks will be retried if IntegrityErrors are encountered.
......@@ -287,7 +287,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self._assert_retry_called(mock_retry)
self.assertIn(
u"Persistent Grades: tasks._has_database_updated_with_new_score is False.",
u"Grades: tasks._has_database_updated_with_new_score is False.",
mock_log.info.call_args_list[0][0][0]
)
......@@ -319,13 +319,13 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
else:
self._assert_retry_called(mock_retry)
self.assertIn(
u"Persistent Grades: tasks._has_database_updated_with_new_score is False.",
u"Grades: tasks._has_database_updated_with_new_score is False.",
mock_log.info.call_args_list[0][0][0]
)
@patch('lms.djangoapps.grades.tasks.log')
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.retry')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
@patch('lms.djangoapps.grades.new.subsection_grade_factory.SubsectionGradeFactory.update')
def test_log_unknown_error(self, mock_update, mock_retry, mock_log):
"""
Ensures that unknown errors are logged before a retry.
......@@ -338,7 +338,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
@patch('lms.djangoapps.grades.tasks.log')
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.retry')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
@patch('lms.djangoapps.grades.new.subsection_grade_factory.SubsectionGradeFactory.update')
def test_no_log_known_error(self, mock_update, mock_retry, mock_log):
"""
Ensures that known errors are not logged before a retry.
......@@ -395,7 +395,7 @@ class ComputeGradesForCourseTest(HasCourseWithProblemsMixin, ModuleStoreTestCase
result = compute_grades_for_course.delay(
course_key=six.text_type(self.course.id),
batch_size=batch_size,
offset=4
offset=4,
)
self.assertTrue(result.successful)
self.assertEqual(
......@@ -409,8 +409,8 @@ class ComputeGradesForCourseTest(HasCourseWithProblemsMixin, ModuleStoreTestCase
@ddt.data(*xrange(1, 12, 3))
def test_database_calls(self, batch_size):
per_user_queries = 18 * min(batch_size, 6) # No more than 6 due to offset
with self.assertNumQueries(3 + per_user_queries):
per_user_queries = 17 * min(batch_size, 6) # No more than 6 due to offset
with self.assertNumQueries(5 + per_user_queries):
with check_mongo_calls(1):
compute_grades_for_course.delay(
course_key=six.text_type(self.course.id),
......
......@@ -14,7 +14,7 @@ def mock_passing_grade(grade_pass='Pass', percent=0.75, ):
Mock the grading function to always return a passing grade.
"""
with patch('lms.djangoapps.grades.new.course_grade.CourseGrade._compute_letter_grade') as mock_letter_grade:
with patch('lms.djangoapps.grades.new.course_grade.CourseGrade._calc_percent') as mock_percent_grade:
with patch('lms.djangoapps.grades.new.course_grade.CourseGrade._compute_percent') as mock_percent_grade:
mock_letter_grade.return_value = grade_pass
mock_percent_grade.return_value = percent
yield
......
......@@ -73,6 +73,18 @@ class GradesTransformer(BlockStructureTransformer):
pass
@classmethod
def grading_policy_hash(cls, course):
"""
Returns the grading policy hash for the given course.
"""
ordered_policy = json.dumps(
course.grading_policy,
separators=(',', ':'), # Remove spaces from separators for more compact representation
sort_keys=True,
)
return b64encode(sha1(ordered_policy).digest())
@classmethod
def _collect_explicit_graded(cls, block_structure):
"""
Collect the 'explicit_graded' field for every block.
......@@ -137,27 +149,13 @@ class GradesTransformer(BlockStructureTransformer):
Collect a hash of the course's grading policy, storing it as a
`transformer_block_field` associated with the `GradesTransformer`.
"""
def _hash_grading_policy(policy):
"""
Creates a hash from the course grading policy.
The keys are sorted in order to make the hash
agnostic to the ordering of the policy coming in.
"""
ordered_policy = json.dumps(
policy,
separators=(',', ':'), # Remove spaces from separators for more compact representation
sort_keys=True,
)
return b64encode(sha1(ordered_policy).digest())
course_location = block_structure.root_block_usage_key
course_block = block_structure.get_xblock(course_location)
grading_policy = course_block.grading_policy
block_structure.set_transformer_block_field(
course_block.location,
cls,
"grading_policy_hash",
_hash_grading_policy(grading_policy)
cls.grading_policy_hash(course_block),
)
@staticmethod
......
......@@ -17,7 +17,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from ccx_keys.locator import CCXLocator
from courseware.models import StudentModule
from grades.new.subsection_grade import SubsectionGradeFactory
from grades.new.subsection_grade_factory import SubsectionGradeFactory
from grades.tests.utils import answer_problem
from lms.djangoapps.ccx.tests.factories import CcxFactory
from lms.djangoapps.course_blocks.api import get_course_blocks
......
......@@ -14,7 +14,7 @@ from opaque_keys.edx.keys import CourseKey
from edxmako.shortcuts import render_to_response
from courseware.courses import get_course_with_access
from lms.djangoapps.instructor.views.api import require_level
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from xmodule.modulestore.django import modulestore
......
......@@ -42,7 +42,7 @@ from certificates.models import (
)
from courseware.courses import get_course_by_id, get_problems_in_section
from lms.djangoapps.grades.context import grading_context_for_course
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from courseware.model_data import DjangoKeyValueStore, FieldDataCache
from courseware.models import StudentModule
from courseware.module_render import get_module_for_descriptor_internal
......@@ -835,7 +835,7 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input,
student,
course_id,
course_grade.percent,
course_grade.course.grade_cutoffs,
course.grade_cutoffs,
student.profile.allow_certificate,
student.id in whitelisted_user_ids
)
......@@ -855,7 +855,9 @@ def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input,
else:
grade_results.append([u'Not Attempted'])
if assignment_info['use_subsection_headers']:
assignment_average = course_grade.grade_value['grade_breakdown'].get(assignment_type, {}).get('percent')
assignment_average = course_grade.grader_result['grade_breakdown'].get(assignment_type, {}).get(
'percent'
)
grade_results.append([assignment_average])
grade_results = list(chain.from_iterable(grade_results))
......
......@@ -40,7 +40,7 @@ from lms.djangoapps.instructor_task.tests.test_base import (
OPTION_2,
)
from capa.responsetypes import StudentInputError
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from openedx.core.lib.url_utils import quote_slashes
......
......@@ -117,7 +117,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
self.assertDictContainsSubset({'attempted': num_students, 'succeeded': num_students, 'failed': 0}, result)
@patch('lms.djangoapps.instructor_task.tasks_helper._get_current_task')
@patch('lms.djangoapps.grades.new.course_grade.CourseGradeFactory.iter')
@patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.iter')
def test_grading_failure(self, mock_grades_iter, _mock_current_task):
"""
Test that any grading errors are properly reported in the
......@@ -294,7 +294,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
)
@patch('lms.djangoapps.instructor_task.tasks_helper._get_current_task')
@patch('lms.djangoapps.grades.new.course_grade.CourseGradeFactory.iter')
@patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.iter')
def test_unicode_in_csv_header(self, mock_grades_iter, _mock_current_task):
"""
Tests that CSV grade report works if unicode in headers.
......@@ -650,7 +650,7 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
])
@patch('lms.djangoapps.instructor_task.tasks_helper._get_current_task')
@patch('lms.djangoapps.grades.new.course_grade.CourseGradeFactory.iter')
@patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.iter')
@ddt.data(u'Cannot grade student', '')
def test_grading_failure(self, error_message, mock_grades_iter, _mock_current_task):
"""
......@@ -1775,7 +1775,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 3,
'skipped': 2
}
with self.assertNumQueries(184):
with self.assertNumQueries(186):
self.assertCertificatesGenerated(task_input, expected_results)
expected_results = {
......
......@@ -7,7 +7,7 @@ from django.contrib.auth.models import User
from django.dispatch import receiver
import logging
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED
from lms import CELERY_APP
from lti_provider.models import GradedAssignment
......
......@@ -5,8 +5,7 @@ from abc import ABCMeta
from contextlib import contextmanager
import logging
from waffle.models import Switch
from waffle.utils import get_setting as waffle_setting
from waffle.testutils import override_switch as waffle_override_switch
from waffle import switch_is_active
from request_cache import get_cache as get_request_cache
......@@ -81,6 +80,18 @@ class WaffleSwitchPlus(WafflePlus):
self._cached_switches[namespaced_switch_name] = active
log.info(u"%sSwitch '%s' set to %s for request.", self.log_prefix, namespaced_switch_name, active)
@contextmanager
def override_in_model(self, switch_name, active=True):
"""
Overrides the active value for the given switch for the duration of this
contextmanager.
Note: The value is overridden in the request cache AND in the model.
"""
with self.override(switch_name, active):
namespaced_switch_name = self._namespaced_setting_name(switch_name)
with waffle_override_switch(namespaced_switch_name, active):
yield
@property
def _cached_switches(self):
"""
......
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