Commit 8721dad5 by Nimisha Asthagiri Committed by GitHub

Merge pull request #16083 from edx/naa/grades-remove-read-only

Grades: remove read_only param and create method
parents bfe2b67e 1febdbfa
...@@ -413,7 +413,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa ...@@ -413,7 +413,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
if status in {'generating', 'ready', 'notpassing', 'restricted', 'auditing', 'unverified'}: if status in {'generating', 'ready', 'notpassing', 'restricted', 'auditing', 'unverified'}:
cert_grade_percent = -1 cert_grade_percent = -1
persisted_grade_percent = -1 persisted_grade_percent = -1
persisted_grade = CourseGradeFactory().read(user, course=course_overview) persisted_grade = CourseGradeFactory().read(user, course=course_overview, create_if_needed=False)
if persisted_grade is not None: if persisted_grade is not None:
persisted_grade_percent = persisted_grade.percent persisted_grade_percent = persisted_grade.percent
......
...@@ -237,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): ...@@ -237,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default, # # of sql queries to default,
# # of mongo queries, # # of mongo queries,
# ) # )
('no_overrides', 1, True, False): (26, 1), ('no_overrides', 1, True, False): (19, 1),
('no_overrides', 2, True, False): (26, 1), ('no_overrides', 2, True, False): (19, 1),
('no_overrides', 3, True, False): (26, 1), ('no_overrides', 3, True, False): (19, 1),
('ccx', 1, True, False): (26, 1), ('ccx', 1, True, False): (19, 1),
('ccx', 2, True, False): (26, 1), ('ccx', 2, True, False): (19, 1),
('ccx', 3, True, False): (26, 1), ('ccx', 3, True, False): (19, 1),
('no_overrides', 1, False, False): (26, 1), ('no_overrides', 1, False, False): (19, 1),
('no_overrides', 2, False, False): (26, 1), ('no_overrides', 2, False, False): (19, 1),
('no_overrides', 3, False, False): (26, 1), ('no_overrides', 3, False, False): (19, 1),
('ccx', 1, False, False): (26, 1), ('ccx', 1, False, False): (19, 1),
('ccx', 2, False, False): (26, 1), ('ccx', 2, False, False): (19, 1),
('ccx', 3, False, False): (26, 1), ('ccx', 3, False, False): (19, 1),
} }
...@@ -260,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): ...@@ -260,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True __test__ = True
TEST_DATA = { TEST_DATA = {
('no_overrides', 1, True, False): (26, 3), ('no_overrides', 1, True, False): (19, 3),
('no_overrides', 2, True, False): (26, 3), ('no_overrides', 2, True, False): (19, 3),
('no_overrides', 3, True, False): (26, 3), ('no_overrides', 3, True, False): (19, 3),
('ccx', 1, True, False): (26, 3), ('ccx', 1, True, False): (19, 3),
('ccx', 2, True, False): (26, 3), ('ccx', 2, True, False): (19, 3),
('ccx', 3, True, False): (26, 3), ('ccx', 3, True, False): (19, 3),
('ccx', 1, True, True): (27, 3), ('ccx', 1, True, True): (20, 3),
('ccx', 2, True, True): (27, 3), ('ccx', 2, True, True): (20, 3),
('ccx', 3, True, True): (27, 3), ('ccx', 3, True, True): (20, 3),
('no_overrides', 1, False, False): (26, 3), ('no_overrides', 1, False, False): (19, 3),
('no_overrides', 2, False, False): (26, 3), ('no_overrides', 2, False, False): (19, 3),
('no_overrides', 3, False, False): (26, 3), ('no_overrides', 3, False, False): (19, 3),
('ccx', 1, False, False): (26, 3), ('ccx', 1, False, False): (19, 3),
('ccx', 2, False, False): (26, 3), ('ccx', 2, False, False): (19, 3),
('ccx', 3, False, False): (26, 3), ('ccx', 3, False, False): (19, 3),
} }
...@@ -36,6 +36,7 @@ from lms.djangoapps.ccx.tests.factories import CcxFactory ...@@ -36,6 +36,7 @@ from lms.djangoapps.ccx.tests.factories import CcxFactory
from lms.djangoapps.ccx.tests.utils import CcxTestCase, flatten from lms.djangoapps.ccx.tests.utils import CcxTestCase, flatten
from lms.djangoapps.ccx.utils import ccx_course, is_email from lms.djangoapps.ccx.utils import ccx_course, is_email
from lms.djangoapps.ccx.views import get_date from lms.djangoapps.ccx.views import get_date
from lms.djangoapps.grades.tasks import compute_all_grades_for_course
from lms.djangoapps.instructor.access import allow_access, list_with_level from lms.djangoapps.instructor.access import allow_access, list_with_level
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
from student.models import CourseEnrollment, CourseEnrollmentAllowed from student.models import CourseEnrollment, CourseEnrollmentAllowed
...@@ -110,6 +111,8 @@ def setup_students_and_grades(context): ...@@ -110,6 +111,8 @@ def setup_students_and_grades(context):
module_state_key=problem.location module_state_key=problem.location
) )
compute_all_grades_for_course.apply_async(kwargs={'course_key': unicode(context.course.id)})
def unhide(unit): def unhide(unit):
""" """
......
...@@ -265,12 +265,6 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke ...@@ -265,12 +265,6 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke
return errors return errors
def prep_course_for_grading(course, request):
"""Set up course module for overrides to function properly"""
course._field_data_cache = {} # pylint: disable=protected-access
course.set_grading_policy(course.grading_policy)
@contextmanager @contextmanager
def ccx_course(ccx_locator): def ccx_course(ccx_locator):
"""Create a context in which the course identified by course_locator exists """Create a context in which the course identified by course_locator exists
......
...@@ -46,7 +46,6 @@ from lms.djangoapps.ccx.utils import ( ...@@ -46,7 +46,6 @@ from lms.djangoapps.ccx.utils import (
get_ccx_for_coach, get_ccx_for_coach,
get_date, get_date,
parse_date, parse_date,
prep_course_for_grading
) )
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.instructor.enrollment import enroll_email, get_email_params from lms.djangoapps.instructor.enrollment import enroll_email, get_email_params
...@@ -519,7 +518,6 @@ def ccx_gradebook(request, course, ccx=None): ...@@ -519,7 +518,6 @@ def ccx_gradebook(request, course, ccx=None):
ccx_key = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) ccx_key = CCXLocator.from_course_locator(course.id, unicode(ccx.id))
with ccx_course(ccx_key) as course: with ccx_course(ccx_key) as course:
prep_course_for_grading(course, request)
student_info, page = get_grade_book_page(request, course, course_key=ccx_key) student_info, page = get_grade_book_page(request, course, course_key=ccx_key)
return render_to_response('courseware/gradebook.html', { return render_to_response('courseware/gradebook.html', {
...@@ -547,7 +545,6 @@ def ccx_grades_csv(request, course, ccx=None): ...@@ -547,7 +545,6 @@ def ccx_grades_csv(request, course, ccx=None):
ccx_key = CCXLocator.from_course_locator(course.id, unicode(ccx.id)) ccx_key = CCXLocator.from_course_locator(course.id, unicode(ccx.id))
with ccx_course(ccx_key) as course: with ccx_course(ccx_key) as course:
prep_course_for_grading(course, request)
enrolled_students = User.objects.filter( enrolled_students = User.objects.filter(
courseenrollment__course_id=ccx_key, courseenrollment__course_id=ccx_key,
......
...@@ -51,7 +51,7 @@ class Command(BaseCommand): ...@@ -51,7 +51,7 @@ class Command(BaseCommand):
course = courses.get_course_by_id(course_id) course = courses.get_course_by_id(course_id)
for cert in ungraded: for cert in ungraded:
# grade the student # grade the student
grade = CourseGradeFactory().create(cert.user, course) grade = CourseGradeFactory().read(cert.user, course)
log.info('grading %s - %s', cert.user, grade.percent) log.info('grading %s - %s', cert.user, grade.percent)
cert.grade = grade.percent cert.grade = grade.percent
if not options['noop']: if not options['noop']:
......
...@@ -269,7 +269,7 @@ class XQueueCertInterface(object): ...@@ -269,7 +269,7 @@ class XQueueCertInterface(object):
self.request.session = {} self.request.session = {}
is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
course_grade = CourseGradeFactory().create(student, course) course_grade = CourseGradeFactory().read(student, course)
enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
......
...@@ -265,7 +265,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -265,7 +265,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
) )
# Run grading/cert generation again # Run grading/cert generation again
with mock_passing_grade(grade_pass=grade): with mock_passing_grade(letter_grade=grade):
with patch.object(XQueueInterface, 'send_to_queue') as mock_send: with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None) mock_send.return_value = (0, None)
self.xqueue.add_cert(self.user_2, self.course.id) self.xqueue.add_cert(self.user_2, self.course.id)
......
...@@ -29,6 +29,7 @@ from course_modes.models import CourseMode ...@@ -29,6 +29,7 @@ from course_modes.models import CourseMode
from courseware.models import BaseStudentModuleHistory, StudentModule from courseware.models import BaseStudentModuleHistory, StudentModule
from courseware.tests.helpers import LoginEnrollmentTestCase from courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.tasks import compute_all_grades_for_course
from openedx.core.djangoapps.credit.api import get_credit_requirement_status, set_credit_requirements from openedx.core.djangoapps.credit.api import get_credit_requirement_status, set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory
...@@ -143,6 +144,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl ...@@ -143,6 +144,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
COURSE_NAME = "test_course" COURSE_NAME = "test_course"
ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache']
ENABLED_SIGNALS = ['course_published']
def setUp(self): def setUp(self):
super(TestSubmittingProblems, self).setUp() super(TestSubmittingProblems, self).setUp()
...@@ -156,25 +158,6 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl ...@@ -156,25 +158,6 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
self.enroll(self.course) self.enroll(self.course)
self.student_user = User.objects.get(email=self.student) self.student_user = User.objects.get(email=self.student)
self.factory = RequestFactory() self.factory = RequestFactory()
# Disable the score change signal to prevent other components from being pulled into tests.
self.score_changed_signal_patch = patch(
'lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send'
)
self.score_changed_signal_patch.start()
def tearDown(self):
super(TestSubmittingProblems, self).tearDown()
self._stop_signal_patch()
def _stop_signal_patch(self):
"""
Stops the signal patch for the PROBLEM_WEIGHTED_SCORE_CHANGED event.
In case a test wants to test with the event actually
firing.
"""
if self.score_changed_signal_patch:
self.score_changed_signal_patch.stop()
self.score_changed_signal_patch = None
def add_dropdown_to_section(self, section_location, name, num_inputs=2): def add_dropdown_to_section(self, section_location, name, num_inputs=2):
""" """
...@@ -278,7 +261,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl ...@@ -278,7 +261,7 @@ class TestSubmittingProblems(ModuleStoreTestCase, LoginEnrollmentTestCase, Probl
""" """
Return CourseGrade for current user and course. Return CourseGrade for current user and course.
""" """
return CourseGradeFactory().create(self.student_user, self.course) return CourseGradeFactory().read(self.student_user, self.course)
def check_grade_percent(self, percent): def check_grade_percent(self, percent):
""" """
...@@ -424,6 +407,7 @@ class TestCourseGrader(TestSubmittingProblems): ...@@ -424,6 +407,7 @@ class TestCourseGrader(TestSubmittingProblems):
] ]
} }
self.add_grading_policy(grading_policy) self.add_grading_policy(grading_policy)
compute_all_grades_for_course.apply_async(kwargs={'course_key': unicode(self.course.id)})
def dropping_setup(self): def dropping_setup(self):
""" """
...@@ -597,10 +581,6 @@ class TestCourseGrader(TestSubmittingProblems): ...@@ -597,10 +581,6 @@ class TestCourseGrader(TestSubmittingProblems):
self.check_grade_percent(0.67) self.check_grade_percent(0.67)
self.assertEqual(self.get_course_grade().letter_grade, 'B') self.assertEqual(self.get_course_grade().letter_grade, 'B')
# But now, set the score with the submissions API and watch
# as it overrides the score read from StudentModule and our
# student gets an A instead.
self._stop_signal_patch()
student_item = { student_item = {
'student_id': anonymous_id_for_user(self.student_user, self.course.id), 'student_id': anonymous_id_for_user(self.student_user, self.course.id),
'course_id': unicode(self.course.id), 'course_id': unicode(self.course.id),
...@@ -619,7 +599,6 @@ class TestCourseGrader(TestSubmittingProblems): ...@@ -619,7 +599,6 @@ class TestCourseGrader(TestSubmittingProblems):
self.basic_setup() self.basic_setup()
self.submit_question_answer('p1', {'2_1': 'Correct'}) self.submit_question_answer('p1', {'2_1': 'Correct'})
self.submit_question_answer('p2', {'2_1': 'Correct'}) self.submit_question_answer('p2', {'2_1': 'Correct'})
self.submit_question_answer('p3', {'2_1': 'Incorrect'})
with patch('submissions.api.get_scores') as mock_get_scores: with patch('submissions.api.get_scores') as mock_get_scores:
mock_get_scores.return_value = { mock_get_scores.return_value = {
...@@ -629,7 +608,7 @@ class TestCourseGrader(TestSubmittingProblems): ...@@ -629,7 +608,7 @@ class TestCourseGrader(TestSubmittingProblems):
'created_at': now(), 'created_at': now(),
}, },
} }
self.get_course_grade() self.submit_question_answer('p3', {'2_1': 'Incorrect'})
# Verify that the submissions API was sent an anonymized student ID # Verify that the submissions API was sent an anonymized student ID
mock_get_scores.assert_called_with( mock_get_scores.assert_called_with(
...@@ -764,7 +743,6 @@ class TestCourseGrader(TestSubmittingProblems): ...@@ -764,7 +743,6 @@ class TestCourseGrader(TestSubmittingProblems):
req_status = get_credit_requirement_status(self.course.id, self.student_user.username, 'grade', 'grade') req_status = get_credit_requirement_status(self.course.id, self.student_user.username, 'grade', 'grade')
self.assertEqual(req_status[0]["status"], None) self.assertEqual(req_status[0]["status"], None)
self._stop_signal_patch()
self.submit_question_answer('p1', {'2_1': 'Correct'}) self.submit_question_answer('p1', {'2_1': 'Correct'})
self.submit_question_answer('p2', {'2_1': 'Correct'}) self.submit_question_answer('p2', {'2_1': 'Correct'})
......
...@@ -42,8 +42,6 @@ from django.test.utils import override_settings ...@@ -42,8 +42,6 @@ from django.test.utils import override_settings
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
from lms.djangoapps.grades.config.waffle import waffle as grades_waffle from lms.djangoapps.grades.config.waffle import waffle as grades_waffle
from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.tests.utils import mock_get_score
from milestones.tests.utils import MilestonesTestCaseMixin from milestones.tests.utils import MilestonesTestCaseMixin
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
...@@ -1399,7 +1397,7 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1399,7 +1397,7 @@ class ProgressPageTests(ProgressPageBaseTests):
self.course.save() self.course.save()
self.store.update_item(self.course, self.user.id) self.store.update_item(self.course, self.user.id)
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') as mock_create: with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
course_grade = mock_create.return_value course_grade = mock_create.return_value
course_grade.passed = True course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}
...@@ -1442,7 +1440,7 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1442,7 +1440,7 @@ class ProgressPageTests(ProgressPageBaseTests):
# Enable certificate generation for this course # Enable certificate generation for this course
certs_api.set_cert_generation_enabled(self.course.id, True) certs_api.set_cert_generation_enabled(self.course.id, True)
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') as mock_create: with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
course_grade = mock_create.return_value course_grade = mock_create.return_value
course_grade.passed = True course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}
...@@ -1458,9 +1456,10 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1458,9 +1456,10 @@ class ProgressPageTests(ProgressPageBaseTests):
"""Test that query counts remain the same for self-paced and instructor-paced courses.""" """Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save() SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced) self.setup_course(self_paced=self_paced)
with self.assertNumQueries(43, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1): with self.assertNumQueries(36, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1):
self._get_progress_page() self._get_progress_page()
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.data( @ddt.data(
(False, 43, 27), (False, 43, 27),
(True, 36, 23) (True, 36, 23)
...@@ -1504,7 +1503,7 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1504,7 +1503,7 @@ class ProgressPageTests(ProgressPageBaseTests):
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified' 'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
) as user_verify: ) as user_verify:
user_verify.return_value = user_verified user_verify.return_value = user_verified
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') as mock_create: with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
course_grade = mock_create.return_value course_grade = mock_create.return_value
course_grade.passed = True course_grade.passed = True
course_grade.summary = { course_grade.summary = {
...@@ -1548,7 +1547,7 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1548,7 +1547,7 @@ class ProgressPageTests(ProgressPageBaseTests):
self.course.save() self.course.save()
self.store.update_item(self.course, self.user.id) self.store.update_item(self.course, self.user.id)
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') as mock_create: with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
course_grade = mock_create.return_value course_grade = mock_create.return_value
course_grade.passed = True course_grade.passed = True
course_grade.summary = { course_grade.summary = {
...@@ -1568,7 +1567,7 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1568,7 +1567,7 @@ class ProgressPageTests(ProgressPageBaseTests):
"http://www.example.com/certificate.pdf", "honor" "http://www.example.com/certificate.pdf", "honor"
) )
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') as mock_create: with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
course_grade = mock_create.return_value course_grade = mock_create.return_value
course_grade.passed = True course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}
...@@ -1586,7 +1585,7 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1586,7 +1585,7 @@ class ProgressPageTests(ProgressPageBaseTests):
self.assertTrue(self.client.login(username=user.username, password='test')) self.assertTrue(self.client.login(username=user.username, password='test'))
CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT) CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT)
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') as mock_create: with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
course_grade = mock_create.return_value course_grade = mock_create.return_value
course_grade.passed = True course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}
...@@ -2090,7 +2089,7 @@ class GenerateUserCertTests(ModuleStoreTestCase): ...@@ -2090,7 +2089,7 @@ class GenerateUserCertTests(ModuleStoreTestCase):
status=CertificateStatuses.generating, status=CertificateStatuses.generating,
mode='verified' mode='verified'
) )
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') as mock_create: with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
course_grade = mock_create.return_value course_grade = mock_create.return_value
course_grade.passed = True course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75} course_grade.summary = {'grade': 'Pass', 'percent': 0.75}
...@@ -2111,7 +2110,7 @@ class GenerateUserCertTests(ModuleStoreTestCase): ...@@ -2111,7 +2110,7 @@ class GenerateUserCertTests(ModuleStoreTestCase):
mode='verified' mode='verified'
) )
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') as mock_create: with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create:
course_grade = mock_create.return_value course_grade = mock_create.return_value
course_grade.passed = True course_grade.passed = True
course_grade.summay = {'grade': 'Pass', 'percent': 0.75} course_grade.summay = {'grade': 'Pass', 'percent': 0.75}
......
...@@ -425,7 +425,7 @@ class CoursewareIndex(View): ...@@ -425,7 +425,7 @@ class CoursewareIndex(View):
if course_has_entrance_exam(self.course) and getattr(self.chapter, 'is_entrance_exam', False): 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_passed'] = user_has_passed_entrance_exam(self.effective_user, self.course)
courseware_context['entrance_exam_current_score'] = get_entrance_exam_score_ratio( courseware_context['entrance_exam_current_score'] = get_entrance_exam_score_ratio(
CourseGradeFactory().create(self.effective_user, self.course), CourseGradeFactory().read(self.effective_user, self.course),
get_entrance_exam_usage_key(self.course), get_entrance_exam_usage_key(self.course),
) )
......
...@@ -60,7 +60,6 @@ from enrollment.api import add_enrollment ...@@ -60,7 +60,6 @@ from enrollment.api import add_enrollment
from eventtracking import tracker from eventtracking import tracker
from ipware.ip import get_ip from ipware.ip import get_ip
from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
from lms.djangoapps.ccx.utils import prep_course_for_grading
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
...@@ -922,12 +921,11 @@ def _progress(request, course_key, student_id): ...@@ -922,12 +921,11 @@ def _progress(request, course_key, student_id):
if request.user.id != student.id: if request.user.id != student.id:
# refetch the course as the assumed student # refetch the course as the assumed student
course = get_course_with_access(student, 'load', course_key, check_if_enrolled=True) 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 # NOTE: To make sure impersonation by instructor works, use
# student instead of request.user in the rest of the function. # student instead of request.user in the rest of the function.
course_grade = CourseGradeFactory().create(student, course) course_grade = CourseGradeFactory().read(student, course)
courseware_summary = course_grade.chapter_grades.values() courseware_summary = course_grade.chapter_grades.values()
studio_url = get_studio_url(course, 'settings/grading') studio_url = get_studio_url(course, 'settings/grading')
...@@ -1015,7 +1013,7 @@ def _get_cert_data(student, course, enrollment_mode, course_grade=None): ...@@ -1015,7 +1013,7 @@ def _get_cert_data(student, course, enrollment_mode, course_grade=None):
certificates_enabled_for_course = certs_api.cert_generation_enabled(course.id) certificates_enabled_for_course = certs_api.cert_generation_enabled(course.id)
if course_grade is None: if course_grade is None:
course_grade = CourseGradeFactory().create(student, course) course_grade = CourseGradeFactory().read(student, course)
if not auto_certs_api.can_show_certificate_message(course, student, course_grade, certificates_enabled_for_course): if not auto_certs_api.can_show_certificate_message(course, student, course_grade, certificates_enabled_for_course):
return return
...@@ -1290,7 +1288,7 @@ def is_course_passed(student, course, course_grade=None): ...@@ -1290,7 +1288,7 @@ def is_course_passed(student, course, course_grade=None):
returns bool value returns bool value
""" """
if course_grade is None: if course_grade is None:
course_grade = CourseGradeFactory().create(student, course) course_grade = CourseGradeFactory().read(student, course)
return course_grade.passed return course_grade.passed
......
...@@ -149,7 +149,7 @@ class TestGatedContent(MilestonesTestCaseMixin, SharedModuleStoreTestCase): ...@@ -149,7 +149,7 @@ class TestGatedContent(MilestonesTestCaseMixin, SharedModuleStoreTestCase):
all problems in the course, whether or not they are currently all problems in the course, whether or not they are currently
gated. gated.
""" """
course_grade = CourseGradeFactory().create(user, self.course) course_grade = CourseGradeFactory().read(user, self.course)
for prob in [self.gating_prob1, self.gated_prob2, self.prob3]: for prob in [self.gating_prob1, self.gated_prob2, self.prob3]:
self.assertIn(prob.location, course_grade.problem_scores) self.assertIn(prob.location, course_grade.problem_scores)
......
...@@ -15,7 +15,7 @@ from rest_framework.test import APITestCase ...@@ -15,7 +15,7 @@ from rest_framework.test import APITestCase
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, StaffFactory from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, StaffFactory
from lms.djangoapps.grades.tests.utils import mock_get_score from lms.djangoapps.grades.tests.utils import mock_passing_grade
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
...@@ -149,7 +149,7 @@ class CurrentGradeViewTest(SharedModuleStoreTestCase, APITestCase): ...@@ -149,7 +149,7 @@ class CurrentGradeViewTest(SharedModuleStoreTestCase, APITestCase):
""" """
Test that a user can successfully request her own grade. Test that a user can successfully request her own grade.
""" """
with check_mongo_calls(6): with check_mongo_calls(3):
resp = self.client.get(self.get_url(self.student.username)) resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
...@@ -286,22 +286,21 @@ class CurrentGradeViewTest(SharedModuleStoreTestCase, APITestCase): ...@@ -286,22 +286,21 @@ class CurrentGradeViewTest(SharedModuleStoreTestCase, APITestCase):
self.assertEqual(resp.data, expected_data) # pylint: disable=no-member self.assertEqual(resp.data, expected_data) # pylint: disable=no-member
@ddt.data( @ddt.data(
((2, 5), {'letter_grade': None, 'percent': 0.4, 'passed': False}), ({'letter_grade': None, 'percent': 0.4, 'passed': False}),
((5, 5), {'letter_grade': 'Pass', 'percent': 1, 'passed': True}), ({'letter_grade': 'Pass', 'percent': 1, 'passed': True}),
) )
@ddt.unpack def test_grade(self, grade):
def test_grade(self, grade, result):
""" """
Test that the user gets her grade in case she answered tests with an insufficient score. Test that the user gets her grade in case she answered tests with an insufficient score.
""" """
with mock_get_score(*grade): with mock_passing_grade(letter_grade=grade['letter_grade'], percent=grade['percent']):
resp = self.client.get(self.get_url(self.student.username)) resp = self.client.get(self.get_url(self.student.username))
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
expected_data = { expected_data = {
'username': self.student.username, 'username': self.student.username,
'course_key': str(self.course_key), 'course_key': str(self.course_key),
} }
expected_data.update(result) expected_data.update(grade)
self.assertEqual(resp.data, [expected_data]) # pylint: disable=no-member self.assertEqual(resp.data, [expected_data]) # pylint: disable=no-member
......
...@@ -11,7 +11,6 @@ from rest_framework.generics import GenericAPIView, ListAPIView ...@@ -11,7 +11,6 @@ from rest_framework.generics import GenericAPIView, ListAPIView
from rest_framework.response import Response from rest_framework.response import Response
from courseware.access import has_access 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.courseware import courses
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
from lms.djangoapps.grades.api.serializers import GradingPolicySerializer from lms.djangoapps.grades.api.serializers import GradingPolicySerializer
...@@ -184,8 +183,7 @@ class UserGradeView(GradeViewMixin, GenericAPIView): ...@@ -184,8 +183,7 @@ class UserGradeView(GradeViewMixin, GenericAPIView):
# or a 404 if the requested user does not exist. # or a 404 if the requested user does not exist.
return grade_user return grade_user
prep_course_for_grading(course, request) course_grade = CourseGradeFactory().read(grade_user, course)
course_grade = CourseGradeFactory().create(grade_user, course)
return Response([{ return Response([{
'username': grade_user.username, 'username': grade_user.username,
'course_key': course_id, 'course_key': course_id,
......
from django.conf import settings
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
from lms.djangoapps.grades.config.waffle import waffle as waffle_func, ASSUME_ZERO_GRADE_IF_ABSENT from lms.djangoapps.grades.config.waffle import waffle as waffle_func, ASSUME_ZERO_GRADE_IF_ABSENT
...@@ -6,7 +8,12 @@ def assume_zero_if_absent(course_key): ...@@ -6,7 +8,12 @@ def assume_zero_if_absent(course_key):
""" """
Returns whether an absent grade should be assumed to be zero. Returns whether an absent grade should be assumed to be zero.
""" """
return should_persist_grades(course_key) and waffle_func().is_enabled(ASSUME_ZERO_GRADE_IF_ABSENT) return (
should_persist_grades(course_key) and (
settings.FEATURES.get('ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS') or
waffle_func().is_enabled(ASSUME_ZERO_GRADE_IF_ABSENT)
)
)
def should_persist_grades(course_key): def should_persist_grades(course_key):
......
...@@ -92,6 +92,7 @@ class CourseData(object): ...@@ -92,6 +92,7 @@ class CourseData(object):
def edited_on(self): def edited_on(self):
# get course block from structure only; subtree_edited_on field on modulestore's course block isn't optimized. # get course block from structure only; subtree_edited_on field on modulestore's course block isn't optimized.
structure = self._effective_structure structure = self._effective_structure
if structure:
course_block = structure[self.location] course_block = structure[self.location]
return getattr(course_block, 'subtree_edited_on', None) return getattr(course_block, 'subtree_edited_on', None)
......
...@@ -7,6 +7,7 @@ from collections import OrderedDict, defaultdict ...@@ -7,6 +7,7 @@ from collections import OrderedDict, defaultdict
from django.conf import settings from django.conf import settings
from lazy import lazy from lazy import lazy
from ccx_keys.locator import CCXLocator
from xmodule import block_metadata_utils from xmodule import block_metadata_utils
from .subsection_grade import ZeroSubsectionGrade from .subsection_grade import ZeroSubsectionGrade
...@@ -21,7 +22,7 @@ class CourseGradeBase(object): ...@@ -21,7 +22,7 @@ class CourseGradeBase(object):
""" """
Base class for Course Grades. Base class for Course Grades.
""" """
def __init__(self, user, course_data, percent=0, letter_grade=None, passed=False, force_update_subsections=False): def __init__(self, user, course_data, percent=0.0, letter_grade=None, passed=False, force_update_subsections=False):
self.user = user self.user = user
self.course_data = course_data self.course_data = course_data
...@@ -137,8 +138,7 @@ class CourseGradeBase(object): ...@@ -137,8 +138,7 @@ class CourseGradeBase(object):
""" """
Returns the result from the course grader. Returns the result from the course grader.
""" """
course = self.course_data.course course = self._prep_course_for_grading()
course.set_grading_policy(course.grading_policy)
return course.grader.grade( return course.grader.grade(
self.graded_subsections_by_format, self.graded_subsections_by_format,
generate_random_scores=settings.GENERATE_PROFILE_SCORES, generate_random_scores=settings.GENERATE_PROFILE_SCORES,
...@@ -156,6 +156,27 @@ class CourseGradeBase(object): ...@@ -156,6 +156,27 @@ class CourseGradeBase(object):
grade_summary['grade'] = self.letter_grade grade_summary['grade'] = self.letter_grade
return grade_summary return grade_summary
def _prep_course_for_grading(self):
"""
Make sure any overrides to the grading policy are used.
This is most relevant for CCX courses.
Right now, we still access the grading policy from the course
object. Once we get the grading policy from the BlockStructure
this will no longer be needed - since BlockStructure correctly
retrieves/uses all field overrides.
"""
course = self.course_data.course
if isinstance(self.course_data.course_key, CCXLocator):
# clean out any field values that may have been set from the
# parent course of the CCX course.
course._field_data_cache = {} # pylint: disable=protected-access
# this is "magic" code that automatically retrieves any overrides
# to the grading policy and updates the course object.
course.set_grading_policy(course.grading_policy)
return course
def _get_chapter_grade_info(self, chapter, course_structure): def _get_chapter_grade_info(self, chapter, course_structure):
""" """
Helper that returns a dictionary of chapter grade information. Helper that returns a dictionary of chapter grade information.
...@@ -212,6 +233,7 @@ class CourseGrade(CourseGradeBase): ...@@ -212,6 +233,7 @@ class CourseGrade(CourseGradeBase):
self.percent = self._compute_percent(self.grader_result) self.percent = self._compute_percent(self.grader_result)
self.letter_grade = self._compute_letter_grade(grade_cutoffs, self.percent) self.letter_grade = self._compute_letter_grade(grade_cutoffs, self.percent)
self.passed = self._compute_passed(grade_cutoffs, self.percent) self.passed = self._compute_passed(grade_cutoffs, self.percent)
return self
@lazy @lazy
def attempted(self): def attempted(self):
...@@ -226,10 +248,10 @@ class CourseGrade(CourseGradeBase): ...@@ -226,10 +248,10 @@ class CourseGrade(CourseGradeBase):
return False return False
def _get_subsection_grade(self, subsection): def _get_subsection_grade(self, subsection):
# Pass read_only here so the subsection grades can be persisted in bulk at the end.
if self.force_update_subsections: if self.force_update_subsections:
return self._subsection_grade_factory.update(subsection) return self._subsection_grade_factory.update(subsection)
else: else:
# Pass read_only here so the subsection grades can be persisted in bulk at the end.
return self._subsection_grade_factory.create(subsection, read_only=True) return self._subsection_grade_factory.create(subsection, read_only=True)
@staticmethod @staticmethod
......
...@@ -20,48 +20,33 @@ class CourseGradeFactory(object): ...@@ -20,48 +20,33 @@ class CourseGradeFactory(object):
""" """
GradeResult = namedtuple('GradeResult', ['student', 'course_grade', 'error']) GradeResult = namedtuple('GradeResult', ['student', 'course_grade', 'error'])
def create(self, user, course=None, collected_block_structure=None, course_structure=None, course_key=None): def read(
self,
user,
course=None,
collected_block_structure=None,
course_structure=None,
course_key=None,
create_if_needed=True,
):
""" """
Returns the CourseGrade for the given user in the course. Returns the CourseGrade for the given user in the course.
Reads the value from storage and validates that the grading Reads the value from storage.
policy hasn't changed since the grade was last computed.
If not in storage, returns a ZeroGrade if ASSUME_ZERO_GRADE_IF_ABSENT. 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. Else if create_if_needed, computes and returns a new value.
Else, returns None.
At least one of course, collected_block_structure, course_structure, At least one of course, collected_block_structure, course_structure,
or course_key should be provided. or course_key should be provided.
""" """
course_data = CourseData(user, course, collected_block_structure, course_structure, course_key) course_data = CourseData(user, course, collected_block_structure, course_structure, course_key)
try: try:
course_grade, read_policy_hash = self._read(user, course_data) return 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: except PersistentCourseGrade.DoesNotExist:
if assume_zero_if_absent(course_data.course_key): if assume_zero_if_absent(course_data.course_key):
return self._create_zero(user, course_data) return self._create_zero(user, course_data)
elif create_if_needed:
return self._update(user, course_data)
else: else:
return None return None
...@@ -82,15 +67,7 @@ class CourseGradeFactory(object): ...@@ -82,15 +67,7 @@ class CourseGradeFactory(object):
or course_key should be provided. or course_key should be provided.
""" """
course_data = CourseData(user, course, collected_block_structure, course_structure, course_key) course_data = CourseData(user, course, collected_block_structure, course_structure, course_key)
return self._update(user, course_data, read_only=False, force_update_subsections=force_update_subsections) return self._update(user, course_data, force_update_subsections=force_update_subsections)
@contextmanager
def _course_transaction(self, course_key):
"""
Provides a transaction context in which GradeResults are created.
"""
yield
VisibleBlocks.clear_cache(course_key)
def iter( def iter(
self, self,
...@@ -123,6 +100,14 @@ class CourseGradeFactory(object): ...@@ -123,6 +100,14 @@ class CourseGradeFactory(object):
with dog_stats_api.timer('lms.grades.CourseGradeFactory.iter', tags=stats_tags): with dog_stats_api.timer('lms.grades.CourseGradeFactory.iter', tags=stats_tags):
yield self._iter_grade_result(user, course_data, force_update) yield self._iter_grade_result(user, course_data, force_update)
@contextmanager
def _course_transaction(self, course_key):
"""
Provides a transaction context in which GradeResults are created.
"""
yield
VisibleBlocks.clear_cache(course_key)
def _iter_grade_result(self, user, course_data, force_update): def _iter_grade_result(self, user, course_data, force_update):
try: try:
kwargs = { kwargs = {
...@@ -134,7 +119,7 @@ class CourseGradeFactory(object): ...@@ -134,7 +119,7 @@ class CourseGradeFactory(object):
if force_update: if force_update:
kwargs['force_update_subsections'] = True kwargs['force_update_subsections'] = True
method = CourseGradeFactory().update if force_update else CourseGradeFactory().create method = CourseGradeFactory().update if force_update else CourseGradeFactory().read
course_grade = method(**kwargs) course_grade = method(**kwargs)
return self.GradeResult(user, course_grade, None) return self.GradeResult(user, course_grade, None)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
...@@ -166,7 +151,9 @@ class CourseGradeFactory(object): ...@@ -166,7 +151,9 @@ class CourseGradeFactory(object):
raise PersistentCourseGrade.DoesNotExist raise PersistentCourseGrade.DoesNotExist
persistent_grade = PersistentCourseGrade.read(user.id, course_data.course_key) persistent_grade = PersistentCourseGrade.read(user.id, course_data.course_key)
course_grade = CourseGrade( log.debug(u'Grades: Read, %s, User: %s, %s', unicode(course_data), user.id, persistent_grade)
return CourseGrade(
user, user,
course_data, course_data,
persistent_grade.percent_grade, persistent_grade.percent_grade,
...@@ -174,12 +161,8 @@ class CourseGradeFactory(object): ...@@ -174,12 +161,8 @@ class CourseGradeFactory(object):
persistent_grade.passed_timestamp is not None, persistent_grade.passed_timestamp is not None,
) )
log.debug(u'Grades: Read, %s, User: %s, %s', unicode(course_data), user.id, persistent_grade)
return course_grade, persistent_grade.grading_policy_hash
@staticmethod @staticmethod
def _update(user, course_data, read_only, force_update_subsections=False): def _update(user, course_data, force_update_subsections=False):
""" """
Computes, saves, and returns a CourseGrade object for the Computes, saves, and returns a CourseGrade object for the
given user and course. given user and course.
...@@ -187,10 +170,9 @@ class CourseGradeFactory(object): ...@@ -187,10 +170,9 @@ class CourseGradeFactory(object):
COURSE_GRADE_NOW_PASSED if learner has passed course. COURSE_GRADE_NOW_PASSED if learner has passed course.
""" """
course_grade = CourseGrade(user, course_data, force_update_subsections=force_update_subsections) course_grade = CourseGrade(user, course_data, force_update_subsections=force_update_subsections)
course_grade.update() course_grade = course_grade.update()
should_persist = ( 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 should_persist_grades(course_data.course_key) and
course_grade.attempted course_grade.attempted
) )
......
...@@ -254,7 +254,8 @@ def force_recalculate_course_and_subsection_grades(sender, user, course_key, **k ...@@ -254,7 +254,8 @@ def force_recalculate_course_and_subsection_grades(sender, user, course_key, **k
Updates a saved course grade, forcing the subsection grades Updates a saved course grade, forcing the subsection grades
from which it is calculated to update along the way. from which it is calculated to update along the way.
""" """
if CourseGradeFactory().read(user, course_key=course_key): previous_course_grade = CourseGradeFactory().read(user, course_key=course_key)
if previous_course_grade and previous_course_grade.attempted:
CourseGradeFactory().update(user=user, course_key=course_key, force_update_subsections=True) CourseGradeFactory().update(user=user, course_key=course_key, force_update_subsections=True)
......
...@@ -79,7 +79,7 @@ class TestGradeIteration(SharedModuleStoreTestCase): ...@@ -79,7 +79,7 @@ class TestGradeIteration(SharedModuleStoreTestCase):
self.assertIsNone(course_grade.letter_grade) self.assertIsNone(course_grade.letter_grade)
self.assertEqual(course_grade.percent, 0.0) self.assertEqual(course_grade.percent, 0.0)
@patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.create') @patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read')
def test_grading_exception(self, mock_course_grade): def test_grading_exception(self, mock_course_grade):
"""Test that we correctly capture exception messages that bubble up from """Test that we correctly capture exception messages that bubble up from
grading. Note that we only see errors at this level if the grading grading. Note that we only see errors at this level if the grading
...@@ -287,7 +287,7 @@ class TestScoreForModule(SharedModuleStoreTestCase): ...@@ -287,7 +287,7 @@ class TestScoreForModule(SharedModuleStoreTestCase):
answer_problem(cls.course, cls.request, cls.l, score=1, max_value=3) answer_problem(cls.course, cls.request, cls.l, score=1, max_value=3)
answer_problem(cls.course, cls.request, cls.n, score=3, max_value=10) answer_problem(cls.course, cls.request, cls.n, score=3, max_value=10)
cls.course_grade = CourseGradeFactory().create(cls.request.user, cls.course) cls.course_grade = CourseGradeFactory().read(cls.request.user, cls.course)
def test_score_chapter(self): def test_score_chapter(self):
earned, possible = self.course_grade.score_for_module(self.a.location) earned, possible = self.course_grade.score_for_module(self.a.location)
......
...@@ -164,9 +164,9 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest ...@@ -164,9 +164,9 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self.assertEquals(mock_block_structure_create.call_count, 1) self.assertEquals(mock_block_structure_create.call_count, 1)
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 31, True), (ModuleStoreEnum.Type.mongo, 1, 27, True),
(ModuleStoreEnum.Type.mongo, 1, 27, False), (ModuleStoreEnum.Type.mongo, 1, 27, False),
(ModuleStoreEnum.Type.split, 3, 31, True), (ModuleStoreEnum.Type.split, 3, 27, True),
(ModuleStoreEnum.Type.split, 3, 27, False), (ModuleStoreEnum.Type.split, 3, 27, False),
) )
@ddt.unpack @ddt.unpack
...@@ -179,8 +179,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest ...@@ -179,8 +179,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self._apply_recalculate_subsection_grade() self._apply_recalculate_subsection_grade()
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 1, 31), (ModuleStoreEnum.Type.mongo, 1, 27),
(ModuleStoreEnum.Type.split, 3, 31), (ModuleStoreEnum.Type.split, 3, 27),
) )
@ddt.unpack @ddt.unpack
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls): def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
......
...@@ -12,16 +12,20 @@ from xmodule.graders import ProblemScore ...@@ -12,16 +12,20 @@ from xmodule.graders import ProblemScore
@contextmanager @contextmanager
def mock_passing_grade(grade_pass='Pass', percent=0.75, ): def mock_passing_grade(letter_grade='Pass', percent=0.75, ):
""" """
Mock the grading function to always return a passing grade. Mock the grading function to always return a passing grade.
""" """
with patch('lms.djangoapps.grades.course_grade.CourseGrade._compute_letter_grade') as mock_letter_grade: passing_grade_fields = dict(
with patch('lms.djangoapps.grades.course_grade.CourseGrade._compute_percent') as mock_percent_grade: letter_grade=letter_grade,
with patch('lms.djangoapps.grades.course_grade.CourseGrade.attempted') as mock_attempted: percent=percent,
mock_letter_grade.return_value = grade_pass passed=letter_grade is not None,
mock_percent_grade.return_value = percent attempted=True,
mock_attempted.return_value = True )
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade_read:
mock_grade_read.return_value = MagicMock(**passing_grade_fields)
with patch('lms.djangoapps.grades.course_grade.CourseGrade.update') as mock_grade_update:
mock_grade_update.return_value = MagicMock(**passing_grade_fields)
yield yield
......
...@@ -7,6 +7,7 @@ from nose.plugins.attrib import attr ...@@ -7,6 +7,7 @@ from nose.plugins.attrib import attr
from capa.tests.response_xml_factory import StringResponseXMLFactory from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
from lms.djangoapps.grades.tasks import compute_all_grades_for_course
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -73,6 +74,7 @@ class TestGradebook(SharedModuleStoreTestCase): ...@@ -73,6 +74,7 @@ class TestGradebook(SharedModuleStoreTestCase):
course_id=self.course.id, course_id=self.course.id,
module_state_key=item.location module_state_key=item.location
) )
compute_all_grades_for_course.apply_async(kwargs={'course_key': unicode(self.course.id)})
self.response = self.client.get(reverse( self.response = self.client.get(reverse(
'spoc_gradebook', 'spoc_gradebook',
......
...@@ -89,7 +89,7 @@ def get_grade_book_page(request, course, course_key): ...@@ -89,7 +89,7 @@ def get_grade_book_page(request, course, course_key):
'username': student.username, 'username': student.username,
'id': student.id, 'id': student.id,
'email': student.email, 'email': student.email,
'grade_summary': CourseGradeFactory().create(student, course).summary 'grade_summary': CourseGradeFactory().read(student, course).summary
} }
for student in enrolled_students for student in enrolled_students
] ]
......
...@@ -131,7 +131,7 @@ class TestRescoringTask(TestIntegrationTask): ...@@ -131,7 +131,7 @@ class TestRescoringTask(TestIntegrationTask):
# are in sync. # are in sync.
expected_subsection_grade = expected_score expected_subsection_grade = expected_score
course_grade = CourseGradeFactory().create(user, self.course) course_grade = CourseGradeFactory().read(user, self.course)
self.assertEquals( self.assertEquals(
course_grade.graded_subsections_by_format['Homework'][self.problem_section.location].graded_total.earned, course_grade.graded_subsections_by_format['Homework'][self.problem_section.location].graded_total.earned,
expected_subsection_grade, expected_subsection_grade,
......
...@@ -395,7 +395,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase): ...@@ -395,7 +395,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
RequestCache.clear_request_cache() RequestCache.clear_request_cache()
expected_query_count = 41 expected_query_count = 36
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'): with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
with check_mongo_calls(mongo_count): with check_mongo_calls(mongo_count):
with self.assertNumQueries(expected_query_count): with self.assertNumQueries(expected_query_count):
...@@ -1976,7 +1976,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): ...@@ -1976,7 +1976,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 3, 'failed': 3,
'skipped': 2 'skipped': 2
} }
with self.assertNumQueries(176): with self.assertNumQueries(106):
self.assertCertificatesGenerated(task_input, expected_results) self.assertCertificatesGenerated(task_input, expected_results)
expected_results = { expected_results = {
......
...@@ -110,7 +110,7 @@ def send_composite_outcome(user_id, course_id, assignment_id, version): ...@@ -110,7 +110,7 @@ def send_composite_outcome(user_id, course_id, assignment_id, version):
mapped_usage_key = assignment.usage_key.map_into_course(course_key) mapped_usage_key = assignment.usage_key.map_into_course(course_key)
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
course = modulestore().get_course(course_key, depth=0) course = modulestore().get_course(course_key, depth=0)
course_grade = CourseGradeFactory().create(user, course) course_grade = CourseGradeFactory().read(user, course)
earned, possible = course_grade.score_for_module(mapped_usage_key) earned, possible = course_grade.score_for_module(mapped_usage_key)
if possible == 0: if possible == 0:
weighted_score = 0 weighted_score = 0
......
...@@ -101,7 +101,7 @@ class SendCompositeOutcomeTest(BaseOutcomeTest): ...@@ -101,7 +101,7 @@ class SendCompositeOutcomeTest(BaseOutcomeTest):
) )
self.course_grade = MagicMock() self.course_grade = MagicMock()
self.course_grade_mock = self.setup_patch( self.course_grade_mock = self.setup_patch(
'lti_provider.tasks.CourseGradeFactory.create', self.course_grade 'lti_provider.tasks.CourseGradeFactory.read', self.course_grade
) )
self.module_store = MagicMock() self.module_store = MagicMock()
self.module_store.get_item = MagicMock(return_value=self.descriptor) self.module_store.get_item = MagicMock(return_value=self.descriptor)
......
...@@ -247,12 +247,8 @@ class OrderTest(ModuleStoreTestCase): ...@@ -247,12 +247,8 @@ class OrderTest(ModuleStoreTestCase):
self.assertEqual(cart.status, status) self.assertEqual(cart.status, status)
self.assertEqual(item.status, status) self.assertEqual(item.status, status)
@override_settings( @override_settings(LMS_SEGMENT_KEY="foobar")
LMS_SEGMENT_KEY="foobar", @patch.dict(settings.FEATURES, {'STORE_BILLING_INFO': True})
FEATURES={
'STORE_BILLING_INFO': True,
}
)
def test_purchase(self): def test_purchase(self):
# This test is for testing the subclassing functionality of OrderItem, but in # This test is for testing the subclassing functionality of OrderItem, but in
# order to do this, we end up testing the specific functionality of # order to do this, we end up testing the specific functionality of
...@@ -914,12 +910,8 @@ class CertificateItemTest(ModuleStoreTestCase): ...@@ -914,12 +910,8 @@ class CertificateItemTest(ModuleStoreTestCase):
cert_item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') cert_item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor')
self.assertEquals(cert_item.single_item_receipt_template, 'shoppingcart/receipt.html') self.assertEquals(cert_item.single_item_receipt_template, 'shoppingcart/receipt.html')
@override_settings( @override_settings(LMS_SEGMENT_KEY="foobar")
LMS_SEGMENT_KEY="foobar", @patch.dict(settings.FEATURES, {'STORE_BILLING_INFO': True})
FEATURES={
'STORE_BILLING_INFO': True,
}
)
@patch('student.models.CourseEnrollment.refund_cutoff_date') @patch('student.models.CourseEnrollment.refund_cutoff_date')
def test_refund_cert_callback_no_expiration(self, cutoff_date): def test_refund_cert_callback_no_expiration(self, cutoff_date):
# When there is no expiration date on a verified mode, the user can always get a refund # When there is no expiration date on a verified mode, the user can always get a refund
...@@ -956,12 +948,8 @@ class CertificateItemTest(ModuleStoreTestCase): ...@@ -956,12 +948,8 @@ class CertificateItemTest(ModuleStoreTestCase):
self.assertFalse(target_certs[0].refund_requested_time) self.assertFalse(target_certs[0].refund_requested_time)
self.assertEquals(target_certs[0].order.status, 'purchased') self.assertEquals(target_certs[0].order.status, 'purchased')
@override_settings( @override_settings(LMS_SEGMENT_KEY="foobar")
LMS_SEGMENT_KEY="foobar", @patch.dict(settings.FEATURES, {'STORE_BILLING_INFO': True})
FEATURES={
'STORE_BILLING_INFO': True,
}
)
@patch('student.models.CourseEnrollment.refund_cutoff_date') @patch('student.models.CourseEnrollment.refund_cutoff_date')
def test_refund_cert_callback_before_expiration(self, cutoff_date): def test_refund_cert_callback_before_expiration(self, cutoff_date):
# If the expiration date has not yet passed on a verified mode, the user can be refunded # If the expiration date has not yet passed on a verified mode, the user can be refunded
......
...@@ -89,13 +89,17 @@ BLOCK_STRUCTURES_SETTINGS = dict( ...@@ -89,13 +89,17 @@ BLOCK_STRUCTURES_SETTINGS = dict(
TASK_DEFAULT_RETRY_DELAY=0, TASK_DEFAULT_RETRY_DELAY=0,
) )
###################### Grade Downloads ###################### ###################### Grades ######################
GRADES_DOWNLOAD = { GRADES_DOWNLOAD = {
'STORAGE_TYPE': 'localfs', 'STORAGE_TYPE': 'localfs',
'BUCKET': 'edx-grades', 'BUCKET': 'edx-grades',
'ROOT_PATH': os.path.join(mkdtemp(), 'edx-s3', 'grades'), 'ROOT_PATH': os.path.join(mkdtemp(), 'edx-s3', 'grades'),
} }
FEATURES['PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'] = True
FEATURES['ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS'] = True
# Configure the LMS to use our stub XQueue implementation # Configure the LMS to use our stub XQueue implementation
XQUEUE_INTERFACE['url'] = 'http://localhost:8040' XQUEUE_INTERFACE['url'] = 'http://localhost:8040'
......
...@@ -307,6 +307,7 @@ FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True ...@@ -307,6 +307,7 @@ FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
########################### Grades ################################# ########################### Grades #################################
FEATURES['PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'] = True FEATURES['PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'] = True
FEATURES['ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS'] = True
###################### Payment ##############################3 ###################### Payment ##############################3
# Enable fake payment processing page # Enable fake payment processing page
......
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