Commit fe081136 by Nimisha Asthagiri Committed by GitHub

Merge pull request #13440 from edx/tnl/progress-page-queries

Reduce sql queries with persisted grades
parents 6fd8452d 232d99d0
...@@ -36,7 +36,6 @@ from openedx.core.djangoapps.content.block_structure.api import get_course_in_ca ...@@ -36,7 +36,6 @@ from openedx.core.djangoapps.content.block_structure.api import get_course_in_ca
'django.conf.settings.FEATURES', 'django.conf.settings.FEATURES',
{ {
'ENABLE_XBLOCK_VIEW_ENDPOINT': True, 'ENABLE_XBLOCK_VIEW_ENDPOINT': True,
'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False # disable persistent grades until TNL-5458 (reduces queries)
} }
) )
@ddt.ddt @ddt.ddt
...@@ -230,18 +229,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): ...@@ -230,18 +229,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): (17, 6), ('no_overrides', 1, True, False): (20, 6),
('no_overrides', 2, True, False): (17, 6), ('no_overrides', 2, True, False): (20, 6),
('no_overrides', 3, True, False): (17, 6), ('no_overrides', 3, True, False): (20, 6),
('ccx', 1, True, False): (17, 6), ('ccx', 1, True, False): (20, 6),
('ccx', 2, True, False): (17, 6), ('ccx', 2, True, False): (20, 6),
('ccx', 3, True, False): (17, 6), ('ccx', 3, True, False): (20, 6),
('no_overrides', 1, False, False): (17, 6), ('no_overrides', 1, False, False): (20, 6),
('no_overrides', 2, False, False): (17, 6), ('no_overrides', 2, False, False): (20, 6),
('no_overrides', 3, False, False): (17, 6), ('no_overrides', 3, False, False): (20, 6),
('ccx', 1, False, False): (17, 6), ('ccx', 1, False, False): (20, 6),
('ccx', 2, False, False): (17, 6), ('ccx', 2, False, False): (20, 6),
('ccx', 3, False, False): (17, 6), ('ccx', 3, False, False): (20, 6),
} }
...@@ -253,19 +252,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): ...@@ -253,19 +252,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True __test__ = True
TEST_DATA = { TEST_DATA = {
('no_overrides', 1, True, False): (17, 3), ('no_overrides', 1, True, False): (20, 3),
('no_overrides', 2, True, False): (17, 3), ('no_overrides', 2, True, False): (20, 3),
('no_overrides', 3, True, False): (17, 3), ('no_overrides', 3, True, False): (20, 3),
('ccx', 1, True, False): (17, 3), ('ccx', 1, True, False): (20, 3),
('ccx', 2, True, False): (17, 3), ('ccx', 2, True, False): (20, 3),
('ccx', 3, True, False): (17, 3), ('ccx', 3, True, False): (20, 3),
('ccx', 1, True, True): (18, 3), ('ccx', 1, True, True): (21, 3),
('ccx', 2, True, True): (18, 3), ('ccx', 2, True, True): (21, 3),
('ccx', 3, True, True): (18, 3), ('ccx', 3, True, True): (21, 3),
('no_overrides', 1, False, False): (17, 3), ('no_overrides', 1, False, False): (20, 3),
('no_overrides', 2, False, False): (17, 3), ('no_overrides', 2, False, False): (20, 3),
('no_overrides', 3, False, False): (17, 3), ('no_overrides', 3, False, False): (20, 3),
('ccx', 1, False, False): (17, 3), ('ccx', 1, False, False): (20, 3),
('ccx', 2, False, False): (17, 3), ('ccx', 2, False, False): (20, 3),
('ccx', 3, False, False): (17, 3), ('ccx', 3, False, False): (20, 3),
} }
...@@ -1136,25 +1136,38 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1136,25 +1136,38 @@ class ProgressPageTests(ModuleStoreTestCase):
self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location) self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
self.vertical = ItemFactory.create(category='vertical', parent_location=self.section.location) self.vertical = ItemFactory.create(category='vertical', parent_location=self.section.location)
@ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>') def _get_progress_page(self, expected_status_code=200):
def test_progress_page_xss_prevent(self, malicious_code):
""" """
Test that XSS attack is prevented Gets the progress page for the user in the course.
"""
resp = self.client.get(
reverse('progress', args=[unicode(self.course.id)])
)
self.assertEqual(resp.status_code, expected_status_code)
return resp
def _get_student_progress_page(self, expected_status_code=200):
"""
Gets the progress page for the user in the course.
""" """
resp = self.client.get( resp = self.client.get(
reverse('student_progress', args=[unicode(self.course.id), self.user.id]) reverse('student_progress', args=[unicode(self.course.id), self.user.id])
) )
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, expected_status_code)
return resp
@ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>')
def test_progress_page_xss_prevent(self, malicious_code):
"""
Test that XSS attack is prevented
"""
resp = self._get_student_progress_page()
# Test that malicious code does not appear in html # Test that malicious code does not appear in html
self.assertNotIn(malicious_code, resp.content) self.assertNotIn(malicious_code, resp.content)
def test_pure_ungraded_xblock(self): def test_pure_ungraded_xblock(self):
ItemFactory.create(category='acid', parent_location=self.vertical.location) ItemFactory.create(category='acid', parent_location=self.vertical.location)
self._get_progress_page()
resp = self.client.get(
reverse('progress', args=[unicode(self.course.id)])
)
self.assertEqual(resp.status_code, 200)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_student_progress_with_valid_and_invalid_id(self, default_store): def test_student_progress_with_valid_and_invalid_id(self, default_store):
...@@ -1180,11 +1193,8 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1180,11 +1193,8 @@ class ProgressPageTests(ModuleStoreTestCase):
) )
self.assertEquals(resp.status_code, 404) self.assertEquals(resp.status_code, 404)
resp = self.client.get(
reverse('student_progress', args=[unicode(self.course.id), self.user.id])
)
# Assert that valid 'student_id' returns 200 status # Assert that valid 'student_id' returns 200 status
self.assertEqual(resp.status_code, 200) self._get_student_progress_page()
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_unenrolled_student_progress_for_credit_course(self, default_store): def test_unenrolled_student_progress_for_credit_course(self, default_store):
...@@ -1219,38 +1229,26 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1219,38 +1229,26 @@ class ProgressPageTests(ModuleStoreTestCase):
# Add a single credit requirement (final grade) # Add a single credit requirement (final grade)
set_credit_requirements(course.id, requirements) set_credit_requirements(course.id, requirements)
resp = self.client.get( self._get_student_progress_page()
reverse('student_progress', args=[unicode(course.id), not_enrolled_user.id])
)
self.assertEqual(resp.status_code, 200)
def test_non_ascii_grade_cutoffs(self): def test_non_ascii_grade_cutoffs(self):
resp = self.client.get( self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertEqual(resp.status_code, 200)
def test_generate_cert_config(self): def test_generate_cert_config(self):
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertNotContains(resp, 'Request Certificate') self.assertNotContains(resp, 'Request Certificate')
# Enable the feature, but do not enable it for this course # Enable the feature, but do not enable it for this course
CertificateGenerationConfiguration(enabled=True).save() CertificateGenerationConfiguration(enabled=True).save()
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertNotContains(resp, 'Request Certificate') self.assertNotContains(resp, 'Request Certificate')
# 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)
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertNotContains(resp, 'Request Certificate') self.assertNotContains(resp, 'Request Certificate')
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
...@@ -1295,9 +1293,7 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1295,9 +1293,7 @@ class ProgressPageTests(ModuleStoreTestCase):
self.course.save() self.course.save()
self.store.update_item(self.course, self.user.id) self.store.update_item(self.course, self.user.id)
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertContains(resp, u"View Certificate") self.assertContains(resp, u"View Certificate")
self.assertContains(resp, u"You can keep working for a higher grade") self.assertContains(resp, u"You can keep working for a higher grade")
...@@ -1308,9 +1304,7 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1308,9 +1304,7 @@ class ProgressPageTests(ModuleStoreTestCase):
certificates[0]['is_active'] = False certificates[0]['is_active'] = False
self.store.update_item(self.course, self.user.id) self.store.update_item(self.course, self.user.id)
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertNotContains(resp, u"View Your Certificate") self.assertNotContains(resp, u"View Your Certificate")
self.assertNotContains(resp, u"You can now view your certificate") self.assertNotContains(resp, u"You can now view your certificate")
self.assertContains(resp, "working on it...") self.assertContains(resp, "working on it...")
...@@ -1340,26 +1334,29 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1340,26 +1334,29 @@ class ProgressPageTests(ModuleStoreTestCase):
# 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)
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertContains(resp, u"Download Your Certificate") self.assertContains(resp, u"Download Your Certificate")
# disable persistent grades until TNL-5458 (reduces query counts)
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
@ddt.data( @ddt.data(
*itertools.product(((34, 4, True), (34, 4, False)), (True, False)) *itertools.product((True, False), (True, False))
) )
@ddt.unpack @ddt.unpack
def test_query_counts(self, (sql_calls, mongo_calls, self_paced), self_paced_enabled): def test_progress_queries_paced_courses(self, self_paced, self_paced_enabled):
"""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(sql_calls), check_mongo_calls(mongo_calls): with self.assertNumQueries(37), check_mongo_calls(4):
resp = self.client.get( self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
) def test_progress_queries(self):
self.assertEqual(resp.status_code, 200) self.setup_course()
with self.assertNumQueries(37), check_mongo_calls(4):
self._get_progress_page()
# subsequent accesses to the progress page require fewer queries.
for _ in range(2):
with self.assertNumQueries(20), check_mongo_calls(4):
self._get_progress_page()
@patch( @patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary', 'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
...@@ -1397,7 +1394,8 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1397,7 +1394,8 @@ class ProgressPageTests(ModuleStoreTestCase):
self.assertEqual( self.assertEqual(
cert_button_hidden, cert_button_hidden,
'Request Certificate' not in resp.content) 'Request Certificate' not in resp.content
)
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
@patch( @patch(
...@@ -1430,9 +1428,7 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1430,9 +1428,7 @@ class ProgressPageTests(ModuleStoreTestCase):
self.course.save() self.course.save()
self.store.update_item(self.course, self.user.id) self.store.update_item(self.course, self.user.id)
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertContains(resp, u"View Certificate") self.assertContains(resp, u"View Certificate")
self.assert_invalidate_certificate(generated_certificate) self.assert_invalidate_certificate(generated_certificate)
...@@ -1449,9 +1445,7 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1449,9 +1445,7 @@ class ProgressPageTests(ModuleStoreTestCase):
"http://www.example.com/certificate.pdf", "honor" "http://www.example.com/certificate.pdf", "honor"
) )
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertContains(resp, u'Download Your Certificate') self.assertContains(resp, u'Download Your Certificate')
self.assert_invalidate_certificate(generated_certificate) self.assert_invalidate_certificate(generated_certificate)
...@@ -1466,9 +1460,7 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1466,9 +1460,7 @@ class ProgressPageTests(ModuleStoreTestCase):
user = UserFactory.create() user = UserFactory.create()
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)
response = self.client.get( response = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertContains( self.assertContains(
response, response,
...@@ -1557,9 +1549,7 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1557,9 +1549,7 @@ class ProgressPageTests(ModuleStoreTestCase):
) )
# Invalidate user certificate # Invalidate user certificate
certificate.invalidate() certificate.invalidate()
resp = self.client.get( resp = self._get_progress_page()
reverse('progress', args=[unicode(self.course.id)])
)
self.assertNotContains(resp, u'Request Certificate') self.assertNotContains(resp, u'Request Certificate')
self.assertContains(resp, u'Your certificate has been invalidated') self.assertContains(resp, u'Your certificate has been invalidated')
......
...@@ -719,7 +719,7 @@ def _progress(request, course_key, student_id): ...@@ -719,7 +719,7 @@ def _progress(request, course_key, student_id):
# additional DB lookup (this kills the Progress page in particular). # additional DB lookup (this kills the Progress page in particular).
student = User.objects.prefetch_related("groups").get(id=student.id) student = User.objects.prefetch_related("groups").get(id=student.id)
course_grade = CourseGradeFactory(student).create(course) course_grade = CourseGradeFactory(student).create(course, read_only=False)
if not course_grade.has_access_to_course: if not course_grade.has_access_to_course:
# This means the student didn't have access to the course (which the instructor requested) # This means the student didn't have access to the course (which the instructor requested)
raise Http404 raise Http404
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from opaque_keys.edx.keys import CourseKey
import xmodule_django.models
class Migration(migrations.Migration):
dependencies = [
('grades', '0003_coursepersistentgradesflag_persistentgradesenabledflag'),
]
operations = [
migrations.AddField(
model_name='visibleblocks',
name='course_id',
field=xmodule_django.models.CourseKeyField(default=CourseKey.from_string('edX/BerylMonkeys/TNL-5458'), max_length=255, db_index=True),
preserve_default=False,
),
]
...@@ -40,10 +40,7 @@ class CourseGrade(object): ...@@ -40,10 +40,7 @@ class CourseGrade(object):
graded_total = subsection_grade.graded_total graded_total = subsection_grade.graded_total
if graded_total.possible > 0: if graded_total.possible > 0:
subsections_by_format[subsection_grade.format].append(graded_total) subsections_by_format[subsection_grade.format].append(graded_total)
log.info(u"Persistent Grades: Calculated subsections_by_format. course id: {0}, user: {1}".format( self._log_event(log.info, u"subsections_by_format")
self.course.location,
self.student.id
))
return subsections_by_format return subsections_by_format
@lazy @lazy
...@@ -55,10 +52,7 @@ class CourseGrade(object): ...@@ -55,10 +52,7 @@ class CourseGrade(object):
for chapter in self.chapter_grades: for chapter in self.chapter_grades:
for subsection_grade in chapter['sections']: for subsection_grade in chapter['sections']:
locations_to_weighted_scores.update(subsection_grade.locations_to_weighted_scores) locations_to_weighted_scores.update(subsection_grade.locations_to_weighted_scores)
log.info(u"Persistent Grades: Calculated locations_to_weighted_scores. course id: {0}, user: {1}".format( self._log_event(log.info, u"locations_to_weighted_scores")
self.course.id,
self.student.id
))
return locations_to_weighted_scores return locations_to_weighted_scores
@lazy @lazy
...@@ -72,10 +66,7 @@ class CourseGrade(object): ...@@ -72,10 +66,7 @@ class CourseGrade(object):
self.subsection_grade_totals_by_format, self.subsection_grade_totals_by_format,
generate_random_scores=settings.GENERATE_PROFILE_SCORES generate_random_scores=settings.GENERATE_PROFILE_SCORES
) )
log.info(u"Persistent Grades: Calculated grade_value. course id: {0}, user: {1}".format( self._log_event(log.info, u"grade_value")
self.course.location,
self.student.id
))
return grade_value return grade_value
@property @property
...@@ -123,31 +114,34 @@ class CourseGrade(object): ...@@ -123,31 +114,34 @@ class CourseGrade(object):
grade_summary['totaled_scores'] = self.subsection_grade_totals_by_format grade_summary['totaled_scores'] = self.subsection_grade_totals_by_format
grade_summary['raw_scores'] = list(self.locations_to_weighted_scores.itervalues()) grade_summary['raw_scores'] = list(self.locations_to_weighted_scores.itervalues())
self._log_event(log.warning, u"grade_summary, percent: {0}, grade: {1}".format(self.percent, self.letter_grade))
return grade_summary return grade_summary
def compute(self): def compute_and_update(self, read_only=False):
""" """
Computes the grade for the given student and course. Computes the grade for the given student and course.
If read_only is True, doesn't save any updates to the grades.
""" """
subsection_grade_factory = SubsectionGradeFactory(self.student) self._log_event(log.warning, u"compute_and_update, read_only: {}".format(read_only))
subsection_grade_factory = SubsectionGradeFactory(self.student, self.course, self.course_structure)
for chapter_key in self.course_structure.get_children(self.course.location): for chapter_key in self.course_structure.get_children(self.course.location):
chapter = self.course_structure[chapter_key] chapter = self.course_structure[chapter_key]
subsection_grades = [] chapter_subsection_grades = []
for subsection_key in self.course_structure.get_children(chapter_key): for subsection_key in self.course_structure.get_children(chapter_key):
subsection_grades.append( chapter_subsection_grades.append(
subsection_grade_factory.create( subsection_grade_factory.create(self.course_structure[subsection_key], read_only=True)
self.course_structure[subsection_key],
self.course_structure,
self.course,
)
) )
self.chapter_grades.append({ self.chapter_grades.append({
'display_name': block_metadata_utils.display_name_with_default_escaped(chapter), 'display_name': block_metadata_utils.display_name_with_default_escaped(chapter),
'url_name': block_metadata_utils.url_name_for_block(chapter), 'url_name': block_metadata_utils.url_name_for_block(chapter),
'sections': subsection_grades 'sections': chapter_subsection_grades
}) })
if not read_only:
subsection_grade_factory.bulk_create_unsaved()
self._signal_listeners_when_grade_computed() self._signal_listeners_when_grade_computed()
def score_for_module(self, location): def score_for_module(self, location):
...@@ -212,6 +206,16 @@ class CourseGrade(object): ...@@ -212,6 +206,16 @@ class CourseGrade(object):
receiver, response receiver, response
) )
def _log_event(self, log_func, log_statement):
"""
Logs the given statement, for this instance.
"""
log_func(u"Persistent Grades: CourseGrade.{0}, course: {1}, user: {2}".format(
log_statement,
self.course.id,
self.student.id
))
class CourseGradeFactory(object): class CourseGradeFactory(object):
""" """
...@@ -220,22 +224,26 @@ class CourseGradeFactory(object): ...@@ -220,22 +224,26 @@ class CourseGradeFactory(object):
def __init__(self, student): def __init__(self, student):
self.student = student self.student = student
def create(self, course): def create(self, course, read_only=False):
""" """
Returns the CourseGrade object for the given student and course. Returns the CourseGrade object for the given student and course.
If read_only is True, doesn't save any updates to the grades.
""" """
course_structure = get_course_blocks(self.student, course.location) course_structure = get_course_blocks(self.student, course.location)
return ( return (
self._get_saved_grade(course, course_structure) or self._get_saved_grade(course, course_structure) or
self._compute_and_update_grade(course, course_structure) self._compute_and_update_grade(course, course_structure, read_only)
) )
def _compute_and_update_grade(self, course, course_structure): def _compute_and_update_grade(self, course, course_structure, read_only):
""" """
Freshly computes and updates the grade for the student and course. Freshly computes and updates the grade for the student and course.
If read_only is True, doesn't save any updates to the grades.
""" """
course_grade = CourseGrade(self.student, course, course_structure) course_grade = CourseGrade(self.student, course, course_structure)
course_grade.compute() course_grade.compute_and_update(read_only)
return course_grade return course_grade
def _get_saved_grade(self, course, course_structure): # pylint: disable=unused-argument def _get_saved_grade(self, course, course_structure): # pylint: disable=unused-argument
......
""" """
Functionality for problem scores. Functionality for problem scores.
""" """
from logging import getLogger
from openedx.core.lib.cache_utils import memoized from openedx.core.lib.cache_utils import memoized
from xblock.core import XBlock from xblock.core import XBlock
from .transformer import GradesTransformer from .transformer import GradesTransformer
log = getLogger(__name__)
@memoized @memoized
def block_types_possibly_scored(): def block_types_possibly_scored():
""" """
...@@ -30,15 +35,17 @@ def possibly_scored(usage_key): ...@@ -30,15 +35,17 @@ def possibly_scored(usage_key):
return usage_key.block_type in block_types_possibly_scored() return usage_key.block_type in block_types_possibly_scored()
def weighted_score(raw_earned, raw_possible, weight): def weighted_score(raw_earned, raw_possible, weight=None):
"""Return a tuple that represents the weighted (correct, total) score.""" """
# If there is no weighting, or weighting can't be applied, return input. Return a tuple that represents the weighted (earned, possible) score.
If weight is None or raw_possible is 0, returns the original values.
"""
if weight is None or raw_possible == 0: if weight is None or raw_possible == 0:
return (raw_earned, raw_possible) return (raw_earned, raw_possible)
return float(raw_earned) * weight / raw_possible, float(weight) return float(raw_earned) * weight / raw_possible, float(weight)
def get_score(user, block, scores_client, submissions_scores_cache): def get_score(user, block, scores_client, submissions_scores_cache, weight, possible=None):
""" """
Return the score for a user on a problem, as a tuple (earned, possible). Return the score for a user on a problem, as a tuple (earned, possible).
e.g. (5,7) if you got 5 out of 7 points. e.g. (5,7) if you got 5 out of 7 points.
...@@ -49,8 +56,13 @@ def get_score(user, block, scores_client, submissions_scores_cache): ...@@ -49,8 +56,13 @@ def get_score(user, block, scores_client, submissions_scores_cache):
user: a Student object user: a Student object
block: a BlockStructure's BlockData object block: a BlockStructure's BlockData object
scores_client: an initialized ScoresClient scores_client: an initialized ScoresClient
submissions_scores_cache: A dict of location names to (earned, possible) point tuples. submissions_scores_cache: A dict of location names to (earned, possible)
If an entry is found in this cache, it takes precedence. point tuples. If an entry is found in this cache, it takes precedence.
weight: The weight of the problem to use in the calculation. A value of
None signifies that the weight should not be applied.
possible (optional): The possible maximum score of the problem to use in the
calculation. If None, uses the value found either in scores_client or
from the block.
""" """
submissions_scores_cache = submissions_scores_cache or {} submissions_scores_cache = submissions_scores_cache or {}
...@@ -74,16 +86,20 @@ def get_score(user, block, scores_client, submissions_scores_cache): ...@@ -74,16 +86,20 @@ def get_score(user, block, scores_client, submissions_scores_cache):
if score and score.total is not None: if score and score.total is not None:
# We have a valid score, just use it. # We have a valid score, just use it.
earned = score.correct if score.correct is not None else 0.0 earned = score.correct if score.correct is not None else 0.0
possible = score.total if possible is None:
possible = score.total
elif possible != score.total:
log.error(u"Persisted Grades: possible value {} != score.total value {}".format(possible, score.total))
else: else:
# This means we don't have a valid score entry and we don't have a # This means we don't have a valid score entry and we don't have a
# cached_max_score on hand. We know they've earned 0.0 points on this. # cached_max_score on hand. We know they've earned 0.0 points on this.
earned = 0.0 earned = 0.0
possible = block.transformer_data[GradesTransformer].max_score if possible is None:
possible = block.transformer_data[GradesTransformer].max_score
# Problem may be an error module (if something in the problem builder failed) # Problem may be an error module (if something in the problem builder failed)
# In which case possible might be None # In which case possible might be None
if possible is None: if possible is None:
return (None, None) return (None, None)
return weighted_score(earned, possible, block.weight) return weighted_score(earned, possible, weight)
...@@ -108,10 +108,13 @@ def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=u ...@@ -108,10 +108,13 @@ def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=u
'subsections', 'subsections',
set() set()
) )
subsection_grade_factory = SubsectionGradeFactory(student, course, collected_block_structure)
for subsection_usage_key in subsections_to_update: for subsection_usage_key in subsections_to_update:
transformed_subsection_structure = get_course_blocks( transformed_subsection_structure = get_course_blocks(
student, student,
subsection_usage_key, subsection_usage_key,
collected_block_structure=collected_block_structure, collected_block_structure=collected_block_structure,
) )
SubsectionGradeFactory(student).update(subsection_usage_key, transformed_subsection_structure, course) subsection_grade_factory.update(
transformed_subsection_structure[subsection_usage_key], transformed_subsection_structure
)
...@@ -395,7 +395,7 @@ class TestGetModuleScore(LoginEnrollmentTestCase, SharedModuleStoreTestCase): ...@@ -395,7 +395,7 @@ class TestGetModuleScore(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
# then stored in the request). # then stored in the request).
with self.assertNumQueries(1): with self.assertNumQueries(1):
score = get_module_score(self.request.user, self.course, self.seq1) score = get_module_score(self.request.user, self.course, self.seq1)
new_score = SubsectionGradeFactory(self.request.user).create(self.seq1, self.course_structure, self.course) new_score = SubsectionGradeFactory(self.request.user, self.course, self.course_structure).create(self.seq1)
self.assertEqual(score, 0) self.assertEqual(score, 0)
self.assertEqual(new_score.all_total.earned, 0) self.assertEqual(new_score.all_total.earned, 0)
...@@ -404,7 +404,7 @@ class TestGetModuleScore(LoginEnrollmentTestCase, SharedModuleStoreTestCase): ...@@ -404,7 +404,7 @@ class TestGetModuleScore(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
with self.assertNumQueries(1): with self.assertNumQueries(1):
score = get_module_score(self.request.user, self.course, self.seq1) score = get_module_score(self.request.user, self.course, self.seq1)
new_score = SubsectionGradeFactory(self.request.user).create(self.seq1, self.course_structure, self.course) new_score = SubsectionGradeFactory(self.request.user, self.course, self.course_structure).create(self.seq1)
self.assertEqual(score, 1.0) self.assertEqual(score, 1.0)
self.assertEqual(new_score.all_total.earned, 2.0) self.assertEqual(new_score.all_total.earned, 2.0)
# These differ because get_module_score normalizes the subsection score # These differ because get_module_score normalizes the subsection score
...@@ -416,7 +416,7 @@ class TestGetModuleScore(LoginEnrollmentTestCase, SharedModuleStoreTestCase): ...@@ -416,7 +416,7 @@ class TestGetModuleScore(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
with self.assertNumQueries(1): with self.assertNumQueries(1):
score = get_module_score(self.request.user, self.course, self.seq1) score = get_module_score(self.request.user, self.course, self.seq1)
new_score = SubsectionGradeFactory(self.request.user).create(self.seq1, self.course_structure, self.course) new_score = SubsectionGradeFactory(self.request.user, self.course, self.course_structure).create(self.seq1)
self.assertEqual(score, .5) self.assertEqual(score, .5)
self.assertEqual(new_score.all_total.earned, 1.0) self.assertEqual(new_score.all_total.earned, 1.0)
......
...@@ -6,7 +6,6 @@ from collections import OrderedDict ...@@ -6,7 +6,6 @@ from collections import OrderedDict
import ddt import ddt
from hashlib import sha1 from hashlib import sha1
import json import json
from mock import patch
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.test import TestCase from django.test import TestCase
...@@ -24,17 +23,25 @@ class BlockRecordListTestCase(TestCase): ...@@ -24,17 +23,25 @@ class BlockRecordListTestCase(TestCase):
""" """
Verify the behavior of BlockRecordList, particularly around edge cases Verify the behavior of BlockRecordList, particularly around edge cases
""" """
empty_json = '{"blocks":[],"course_key":null}' def setUp(self):
super(BlockRecordListTestCase, self).setUp()
self.course_key = CourseLocator(
org='some_org',
course='some_course',
run='some_run'
)
def test_empty_block_record_set(self): def test_empty_block_record_set(self):
brs = BlockRecordList(()) empty_json = '{0}"blocks":[],"course_key":"{1}"{2}'.format('{', unicode(self.course_key), '}')
brs = BlockRecordList((), self.course_key)
self.assertFalse(brs) self.assertFalse(brs)
self.assertEqual( self.assertEqual(
brs.to_json(), brs.json_value,
self.empty_json empty_json
) )
self.assertEqual( self.assertEqual(
BlockRecordList.from_json(self.empty_json), BlockRecordList.from_json(empty_json),
brs brs
) )
...@@ -112,7 +119,7 @@ class VisibleBlocksTest(GradesModelTestCase): ...@@ -112,7 +119,7 @@ class VisibleBlocksTest(GradesModelTestCase):
""" """
Creates and returns a BlockRecordList for the given blocks. Creates and returns a BlockRecordList for the given blocks.
""" """
return VisibleBlocks.objects.create_from_blockrecords(BlockRecordList.from_list(blocks)) return VisibleBlocks.objects.create_from_blockrecords(BlockRecordList.from_list(blocks, self.course_key))
def test_creation(self): def test_creation(self):
""" """
...@@ -159,7 +166,7 @@ class VisibleBlocksTest(GradesModelTestCase): ...@@ -159,7 +166,7 @@ class VisibleBlocksTest(GradesModelTestCase):
and accessing visible_blocks.blocks yields a copy of the initial array. and accessing visible_blocks.blocks yields a copy of the initial array.
Also, trying to set the blocks property should raise an exception. Also, trying to set the blocks property should raise an exception.
""" """
expected_blocks = (self.record_a, self.record_b) expected_blocks = BlockRecordList.from_list([self.record_a, self.record_b], self.course_key)
visible_blocks = self._create_block_record_list(expected_blocks) visible_blocks = self._create_block_record_list(expected_blocks)
self.assertEqual(expected_blocks, visible_blocks.blocks) self.assertEqual(expected_blocks, visible_blocks.blocks)
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
...@@ -178,6 +185,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): ...@@ -178,6 +185,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
block_type='subsection', block_type='subsection',
block_id='subsection_12345', block_id='subsection_12345',
) )
self.block_records = BlockRecordList([self.record_a, self.record_b], self.course_key)
self.params = { self.params = {
"user_id": 12345, "user_id": 12345,
"usage_key": self.usage_key, "usage_key": self.usage_key,
...@@ -187,21 +195,23 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): ...@@ -187,21 +195,23 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
"possible_all": 12.0, "possible_all": 12.0,
"earned_graded": 6.0, "earned_graded": 6.0,
"possible_graded": 8.0, "possible_graded": 8.0,
"visible_blocks": [self.record_a, self.record_b], "visible_blocks": self.block_records,
} }
def test_create(self): def test_create(self):
""" """
Tests model creation, and confirms error when trying to recreate model. Tests model creation, and confirms error when trying to recreate model.
""" """
created_grade = PersistentSubsectionGrade.objects.create(**self.params) created_grade = PersistentSubsectionGrade.create_grade(**self.params)
read_grade = PersistentSubsectionGrade.read_grade( with self.assertNumQueries(1):
user_id=self.params["user_id"], read_grade = PersistentSubsectionGrade.read_grade(
usage_key=self.params["usage_key"], user_id=self.params["user_id"],
) usage_key=self.params["usage_key"],
self.assertEqual(created_grade, read_grade) )
self.assertEqual(created_grade, read_grade)
self.assertEquals(read_grade.visible_blocks.blocks, self.block_records)
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
created_grade = PersistentSubsectionGrade.objects.create(**self.params) PersistentSubsectionGrade.create_grade(**self.params)
def test_create_bad_params(self): def test_create_bad_params(self):
""" """
...@@ -209,56 +219,19 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): ...@@ -209,56 +219,19 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
""" """
del self.params["earned_graded"] del self.params["earned_graded"]
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
PersistentSubsectionGrade.objects.create(**self.params) PersistentSubsectionGrade.create_grade(**self.params)
def test_course_version_is_optional(self): def test_course_version_is_optional(self):
del self.params["course_version"] del self.params["course_version"]
PersistentSubsectionGrade.objects.create(**self.params) PersistentSubsectionGrade.create_grade(**self.params)
def test_update_grade(self):
"""
Tests model update, and confirms error when updating a nonexistent model.
"""
with self.assertRaises(PersistentSubsectionGrade.DoesNotExist):
PersistentSubsectionGrade.update_grade(**self.params)
PersistentSubsectionGrade.objects.create(**self.params)
self.params['earned_all'] = 12.0
self.params['earned_graded'] = 8.0
with patch('lms.djangoapps.grades.models.log') as log_mock:
PersistentSubsectionGrade.update_grade(**self.params)
read_grade = PersistentSubsectionGrade.read_grade(
user_id=self.params["user_id"],
usage_key=self.params["usage_key"],
)
log_mock.info.assert_called_with(
u"Persistent Grades: Grade model updated: {0}".format(read_grade)
)
self.assertEqual(read_grade.earned_all, 12.0)
self.assertEqual(read_grade.earned_graded, 8.0)
@ddt.data(True, False) @ddt.data(True, False)
def test_save(self, already_created): def test_update_or_create_grade(self, already_created):
if already_created: created_grade = PersistentSubsectionGrade.create_grade(**self.params) if already_created else None
PersistentSubsectionGrade.objects.create(**self.params)
module_prefix = "lms.djangoapps.grades.models.PersistentSubsectionGrade."
with patch(
module_prefix + "objects.get_or_create",
wraps=PersistentSubsectionGrade.objects.get_or_create
) as mock_get_or_create:
with patch(module_prefix + "update") as mock_update:
PersistentSubsectionGrade.save_grade(**self.params)
self.assertTrue(mock_get_or_create.called)
self.assertEqual(mock_update.called, already_created)
def test_logging_for_save(self): self.params["earned_all"] = 7
with patch('lms.djangoapps.grades.models.log') as log_mock: updated_grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
PersistentSubsectionGrade.save_grade(**self.params) self.assertEquals(updated_grade.earned_all, 7)
read_grade = PersistentSubsectionGrade.read_grade( if already_created:
user_id=self.params["user_id"], self.assertEquals(created_grade.id, updated_grade.id)
usage_key=self.params["usage_key"], self.assertEquals(created_grade.earned_all, 6)
)
log_mock.info.assert_called_with(
u"Persistent Grades: Grade model saved: {0}".format(read_grade)
)
...@@ -61,8 +61,8 @@ class GradeTestBase(SharedModuleStoreTestCase): ...@@ -61,8 +61,8 @@ class GradeTestBase(SharedModuleStoreTestCase):
super(GradeTestBase, self).setUp() super(GradeTestBase, self).setUp()
self.request = get_request_for_user(UserFactory()) self.request = get_request_for_user(UserFactory())
self.client.login(username=self.request.user.username, password="test") self.client.login(username=self.request.user.username, password="test")
self.subsection_grade_factory = SubsectionGradeFactory(self.request.user)
self.course_structure = get_course_blocks(self.request.user, self.course.location) self.course_structure = get_course_blocks(self.request.user, self.course.location)
self.subsection_grade_factory = SubsectionGradeFactory(self.request.user, self.course, self.course_structure)
CourseEnrollment.enroll(self.request.user, self.course.id) CourseEnrollment.enroll(self.request.user, self.course.id)
...@@ -110,33 +110,25 @@ class SubsectionGradeFactoryTest(GradeTestBase): ...@@ -110,33 +110,25 @@ class SubsectionGradeFactoryTest(GradeTestBase):
Tests to ensure that a persistent subsection grade is created, saved, then fetched on re-request. Tests to ensure that a persistent subsection grade is created, saved, then fetched on re-request.
""" """
with patch( with patch(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._save_grade', 'lms.djangoapps.grades.new.subsection_grade.PersistentSubsectionGrade.create_grade',
wraps=self.subsection_grade_factory._save_grade # pylint: disable=protected-access wraps=PersistentSubsectionGrade.create_grade
) as mock_save_grades: ) as mock_create_grade:
with patch( with patch(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._get_saved_grade', 'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._get_saved_grade',
wraps=self.subsection_grade_factory._get_saved_grade # pylint: disable=protected-access wraps=self.subsection_grade_factory._get_saved_grade # pylint: disable=protected-access
) as mock_get_saved_grade: ) as mock_get_saved_grade:
with self.assertNumQueries(19): with self.assertNumQueries(12):
grade_a = self.subsection_grade_factory.create( grade_a = self.subsection_grade_factory.create(self.sequence)
self.sequence,
self.course_structure,
self.course
)
self.assertTrue(mock_get_saved_grade.called) self.assertTrue(mock_get_saved_grade.called)
self.assertTrue(mock_save_grades.called) self.assertTrue(mock_create_grade.called)
mock_get_saved_grade.reset_mock() mock_get_saved_grade.reset_mock()
mock_save_grades.reset_mock() mock_create_grade.reset_mock()
with self.assertNumQueries(3): with self.assertNumQueries(0):
grade_b = self.subsection_grade_factory.create( grade_b = self.subsection_grade_factory.create(self.sequence)
self.sequence,
self.course_structure,
self.course
)
self.assertTrue(mock_get_saved_grade.called) self.assertTrue(mock_get_saved_grade.called)
self.assertFalse(mock_save_grades.called) self.assertFalse(mock_create_grade.called)
self.assertEqual(grade_a.url_name, grade_b.url_name) self.assertEqual(grade_a.url_name, grade_b.url_name)
self.assertEqual(grade_a.all_total, grade_b.all_total) self.assertEqual(grade_a.all_total, grade_b.all_total)
...@@ -153,7 +145,7 @@ class SubsectionGradeFactoryTest(GradeTestBase): ...@@ -153,7 +145,7 @@ class SubsectionGradeFactoryTest(GradeTestBase):
# Grades are only saved if the feature flag and the advanced setting are # Grades are only saved if the feature flag and the advanced setting are
# both set to True. # both set to True.
with patch( with patch(
'lms.djangoapps.grades.models.PersistentSubsectionGrade.read_grade' 'lms.djangoapps.grades.models.PersistentSubsectionGrade.bulk_read_grades'
) as mock_read_saved_grade: ) as mock_read_saved_grade:
with persistent_grades_feature_flags( with persistent_grades_feature_flags(
global_flag=feature_flag, global_flag=feature_flag,
...@@ -161,7 +153,7 @@ class SubsectionGradeFactoryTest(GradeTestBase): ...@@ -161,7 +153,7 @@ class SubsectionGradeFactoryTest(GradeTestBase):
course_id=self.course.id, course_id=self.course.id,
enabled_for_course=course_setting enabled_for_course=course_setting
): ):
self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course) self.subsection_grade_factory.create(self.sequence)
self.assertEqual(mock_read_saved_grade.called, feature_flag and course_setting) self.assertEqual(mock_read_saved_grade.called, feature_flag and course_setting)
...@@ -174,10 +166,8 @@ class SubsectionGradeTest(GradeTestBase): ...@@ -174,10 +166,8 @@ class SubsectionGradeTest(GradeTestBase):
""" """
Assuming the underlying score reporting methods work, test that the score is calculated properly. Assuming the underlying score reporting methods work, test that the score is calculated properly.
""" """
grade = self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course)
with mock_get_score(1, 2): with mock_get_score(1, 2):
# The final 2 parameters are only passed through to our mocked-out get_score method grade = self.subsection_grade_factory.create(self.sequence)
grade.compute(self.request.user, self.course_structure, None, None)
self.assertEqual(grade.all_total.earned, 1) self.assertEqual(grade.all_total.earned, 1)
self.assertEqual(grade.all_total.possible, 2) self.assertEqual(grade.all_total.possible, 2)
...@@ -186,9 +176,8 @@ class SubsectionGradeTest(GradeTestBase): ...@@ -186,9 +176,8 @@ class SubsectionGradeTest(GradeTestBase):
Test that grades are persisted to the database properly, and that loading saved grades returns the same data. Test that grades are persisted to the database properly, and that loading saved grades returns the same data.
""" """
# Create a grade that *isn't* saved to the database # Create a grade that *isn't* saved to the database
self.subsection_grade_factory._prefetch_scores(self.course_structure, self.course) # pylint: disable=protected-access input_grade = SubsectionGrade(self.sequence, self.course)
input_grade = SubsectionGrade(self.sequence) input_grade.init_from_structure(
input_grade.compute(
self.request.user, self.request.user,
self.course_structure, self.course_structure,
self.subsection_grade_factory._scores_client, # pylint: disable=protected-access self.subsection_grade_factory._scores_client, # pylint: disable=protected-access
...@@ -197,16 +186,17 @@ class SubsectionGradeTest(GradeTestBase): ...@@ -197,16 +186,17 @@ class SubsectionGradeTest(GradeTestBase):
self.assertEqual(PersistentSubsectionGrade.objects.count(), 0) self.assertEqual(PersistentSubsectionGrade.objects.count(), 0)
# save to db, and verify object is in database # save to db, and verify object is in database
input_grade.save(self.request.user, self.sequence, self.course) input_grade.create_model(self.request.user)
self.assertEqual(PersistentSubsectionGrade.objects.count(), 1) self.assertEqual(PersistentSubsectionGrade.objects.count(), 1)
# load from db, and ensure output matches input # load from db, and ensure output matches input
loaded_grade = SubsectionGrade(self.sequence) loaded_grade = SubsectionGrade(self.sequence, self.course)
saved_model = PersistentSubsectionGrade.read_grade( saved_model = PersistentSubsectionGrade.read_grade(
user_id=self.request.user.id, user_id=self.request.user.id,
usage_key=self.sequence.location, usage_key=self.sequence.location,
) )
loaded_grade.load_from_data( loaded_grade.init_from_model(
self.request.user,
saved_model, saved_model,
self.course_structure, self.course_structure,
self.subsection_grade_factory._scores_client, # pylint: disable=protected-access self.subsection_grade_factory._scores_client, # pylint: disable=protected-access
......
...@@ -216,7 +216,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -216,7 +216,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
with check_mongo_calls(2) and self.assertNumQueries(15): with check_mongo_calls(2) and self.assertNumQueries(11):
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs) recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
def test_single_call_to_create_block_structure(self): def test_single_call_to_create_block_structure(self):
...@@ -236,7 +236,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -236,7 +236,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2')
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3')
with check_mongo_calls(2) and self.assertNumQueries(15): with check_mongo_calls(2) and self.assertNumQueries(11):
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs) recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
......
...@@ -1641,8 +1641,6 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): ...@@ -1641,8 +1641,6 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
super(TestCertificateGeneration, self).setUp() super(TestCertificateGeneration, self).setUp()
self.initialize_course() self.initialize_course()
# disable persistent grades until TNL-5458 (reduces query counts)
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
def test_certificate_generation_for_students(self): def test_certificate_generation_for_students(self):
""" """
Verify that certificates generated for all eligible students enrolled in a course. Verify that certificates generated for all eligible students enrolled in a course.
...@@ -1672,8 +1670,18 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): ...@@ -1672,8 +1670,18 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 3, 'failed': 3,
'skipped': 2 'skipped': 2
} }
with self.assertNumQueries(175):
self.assertCertificatesGenerated(task_input, expected_results)
with self.assertNumQueries(151): expected_results = {
'action_name': 'certificates generated',
'total': 10,
'attempted': 0,
'succeeded': 0,
'failed': 0,
'skipped': 10
}
with self.assertNumQueries(3):
self.assertCertificatesGenerated(task_input, expected_results) self.assertCertificatesGenerated(task_input, expected_results)
@ddt.data( @ddt.data(
......
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