Merge pull request #13329 from edx/sstudent/TNL-5382-part-1
creating django admin settings for subsection grades
This commit is contained in:
@@ -100,9 +100,6 @@ class CourseMetadata(object):
|
||||
if not XBlockStudioConfigurationFlag.is_enabled():
|
||||
filtered_list.append('allow_unsupported_xblocks')
|
||||
|
||||
if not settings.FEATURES.get('ENABLE_SUBSECTION_GRADES_SAVED'):
|
||||
filtered_list.append('enable_subsection_grades_saved')
|
||||
|
||||
return filtered_list
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -93,9 +93,6 @@ FEATURES['LICENSING'] = True
|
||||
FEATURES['ENABLE_MOBILE_REST_API'] = True # Enable video bumper in Studio
|
||||
FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings
|
||||
|
||||
# Enable persistent subsection grades, so that feature can be tested.
|
||||
FEATURES['ENABLE_SUBSECTION_GRADES_SAVED'] = True
|
||||
|
||||
# Enable partner support link in Studio footer
|
||||
PARTNER_SUPPORT_EMAIL = 'partner-support@example.com'
|
||||
|
||||
|
||||
@@ -212,12 +212,6 @@ FEATURES = {
|
||||
|
||||
# Show Language selector
|
||||
'SHOW_LANGUAGE_SELECTOR': False,
|
||||
|
||||
# Temporary feature flag for disabling saving of subsection grades.
|
||||
# There is also an advanced setting in the course module. The
|
||||
# feature flag and the advanced setting must both be true for
|
||||
# a course to use saved grades.
|
||||
'ENABLE_SUBSECTION_GRADES_SAVED': False,
|
||||
}
|
||||
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
@@ -322,9 +322,6 @@ SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
|
||||
# teams feature
|
||||
FEATURES['ENABLE_TEAMS'] = True
|
||||
|
||||
# Enable persistent subsection grades, so that feature can be tested.
|
||||
FEATURES['ENABLE_SUBSECTION_GRADES_SAVED'] = True
|
||||
|
||||
# Dummy secret key for dev/test
|
||||
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
|
||||
|
||||
@@ -801,17 +801,6 @@ class CourseFields(object):
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
enable_subsection_grades_saved = Boolean(
|
||||
display_name=_("Enable Subsection Grades Saved"),
|
||||
help=_(
|
||||
"Enter true or false. If this value is true, the robust "
|
||||
"grades feature of saving subsection grades is enabled "
|
||||
"for this course."
|
||||
),
|
||||
default=False,
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
learning_info = List(
|
||||
display_name=_("Course Learning Information"),
|
||||
help=_("Specify what student can learn from the course."),
|
||||
|
||||
@@ -228,5 +228,4 @@ class AdvancedSettingsPage(CoursePage):
|
||||
'create_zendesk_tickets',
|
||||
'ccx_connector',
|
||||
'enable_ccx',
|
||||
'enable_subsection_grades_saved',
|
||||
]
|
||||
|
||||
11
common/test/db_fixtures/grades.json
Normal file
11
common/test/db_fixtures/grades.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "grades.persistentgradesenabledflag",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"enabled_for_all_courses": true,
|
||||
"change_date": "2016-09-01"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -229,18 +229,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
|
||||
# # of sql queries to default,
|
||||
# # of mongo queries,
|
||||
# )
|
||||
('no_overrides', 1, True, False): (21, 6),
|
||||
('no_overrides', 2, True, False): (21, 6),
|
||||
('no_overrides', 3, True, False): (21, 6),
|
||||
('ccx', 1, True, False): (21, 6),
|
||||
('ccx', 2, True, False): (21, 6),
|
||||
('ccx', 3, True, False): (21, 6),
|
||||
('no_overrides', 1, False, False): (21, 6),
|
||||
('no_overrides', 2, False, False): (21, 6),
|
||||
('no_overrides', 3, False, False): (21, 6),
|
||||
('ccx', 1, False, False): (21, 6),
|
||||
('ccx', 2, False, False): (21, 6),
|
||||
('ccx', 3, False, False): (21, 6),
|
||||
('no_overrides', 1, True, False): (22, 6),
|
||||
('no_overrides', 2, True, False): (22, 6),
|
||||
('no_overrides', 3, True, False): (22, 6),
|
||||
('ccx', 1, True, False): (22, 6),
|
||||
('ccx', 2, True, False): (22, 6),
|
||||
('ccx', 3, True, False): (22, 6),
|
||||
('no_overrides', 1, False, False): (22, 6),
|
||||
('no_overrides', 2, False, False): (22, 6),
|
||||
('no_overrides', 3, False, False): (22, 6),
|
||||
('ccx', 1, False, False): (22, 6),
|
||||
('ccx', 2, False, False): (22, 6),
|
||||
('ccx', 3, False, False): (22, 6),
|
||||
}
|
||||
|
||||
|
||||
@@ -252,19 +252,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
|
||||
__test__ = True
|
||||
|
||||
TEST_DATA = {
|
||||
('no_overrides', 1, True, False): (21, 3),
|
||||
('no_overrides', 2, True, False): (21, 3),
|
||||
('no_overrides', 3, True, False): (21, 3),
|
||||
('ccx', 1, True, False): (21, 3),
|
||||
('ccx', 2, True, False): (21, 3),
|
||||
('ccx', 3, True, False): (21, 3),
|
||||
('ccx', 1, True, True): (22, 3),
|
||||
('ccx', 2, True, True): (22, 3),
|
||||
('ccx', 3, True, True): (22, 3),
|
||||
('no_overrides', 1, False, False): (21, 3),
|
||||
('no_overrides', 2, False, False): (21, 3),
|
||||
('no_overrides', 3, False, False): (21, 3),
|
||||
('ccx', 1, False, False): (21, 3),
|
||||
('ccx', 2, False, False): (21, 3),
|
||||
('ccx', 3, False, False): (21, 3),
|
||||
('no_overrides', 1, True, False): (22, 3),
|
||||
('no_overrides', 2, True, False): (22, 3),
|
||||
('no_overrides', 3, True, False): (22, 3),
|
||||
('ccx', 1, True, False): (22, 3),
|
||||
('ccx', 2, True, False): (22, 3),
|
||||
('ccx', 3, True, False): (22, 3),
|
||||
('ccx', 1, True, True): (23, 3),
|
||||
('ccx', 2, True, True): (23, 3),
|
||||
('ccx', 3, True, True): (23, 3),
|
||||
('no_overrides', 1, False, False): (22, 3),
|
||||
('no_overrides', 2, False, False): (22, 3),
|
||||
('no_overrides', 3, False, False): (22, 3),
|
||||
('ccx', 1, False, False): (22, 3),
|
||||
('ccx', 2, False, False): (22, 3),
|
||||
('ccx', 3, False, False): (22, 3),
|
||||
}
|
||||
|
||||
@@ -1346,7 +1346,7 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
self.assertContains(resp, u"Download Your Certificate")
|
||||
|
||||
@ddt.data(
|
||||
*itertools.product(((38, 4, True), (38, 4, False)), (True, False))
|
||||
*itertools.product(((39, 4, True), (39, 4, False)), (True, False))
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_query_counts(self, (sql_calls, mongo_calls, self_paced), self_paced_enabled):
|
||||
|
||||
27
lms/djangoapps/grades/admin.py
Normal file
27
lms/djangoapps/grades/admin.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Django admin page for grades models
|
||||
"""
|
||||
from django.contrib import admin
|
||||
|
||||
from config_models.admin import ConfigurationModelAdmin, KeyedConfigurationModelAdmin
|
||||
|
||||
from lms.djangoapps.grades.config.models import CoursePersistentGradesFlag, PersistentGradesEnabledFlag
|
||||
from lms.djangoapps.grades.config.forms import CoursePersistentGradesAdminForm
|
||||
|
||||
|
||||
class CoursePersistentGradesAdmin(KeyedConfigurationModelAdmin):
|
||||
"""
|
||||
Admin for enabling subsection grades on a course-by-course basis.
|
||||
Allows searching by course id.
|
||||
"""
|
||||
form = CoursePersistentGradesAdminForm
|
||||
search_fields = ['course_id']
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('course_id', 'enabled'),
|
||||
'description': 'Enter a valid course id. If it is invalid, an error message will display.'
|
||||
}),
|
||||
)
|
||||
|
||||
admin.site.register(CoursePersistentGradesFlag, CoursePersistentGradesAdmin)
|
||||
admin.site.register(PersistentGradesEnabledFlag, ConfigurationModelAdmin)
|
||||
0
lms/djangoapps/grades/config/__init__.py
Normal file
0
lms/djangoapps/grades/config/__init__.py
Normal file
37
lms/djangoapps/grades/config/forms.py
Normal file
37
lms/djangoapps/grades/config/forms.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Defines a form for providing validation of subsection grade templates.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
|
||||
from lms.djangoapps.grades.config.models import CoursePersistentGradesFlag
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CoursePersistentGradesAdminForm(forms.ModelForm):
|
||||
"""Input form for subsection grade enabling, allowing us to verify data."""
|
||||
|
||||
class Meta(object):
|
||||
model = CoursePersistentGradesFlag
|
||||
fields = '__all__'
|
||||
|
||||
def clean_course_id(self):
|
||||
"""Validate the course id"""
|
||||
cleaned_id = self.cleaned_data["course_id"]
|
||||
try:
|
||||
course_key = CourseLocator.from_string(cleaned_id)
|
||||
except InvalidKeyError:
|
||||
msg = u'Course id invalid. Entered course id was: "{0}."'.format(cleaned_id)
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
if not modulestore().has_course(course_key):
|
||||
msg = u'Course not found. Entered course id was: "{0}". '.format(course_key.to_deprecated_string())
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
return course_key
|
||||
70
lms/djangoapps/grades/config/models.py
Normal file
70
lms/djangoapps/grades/config/models.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Models for configuration of the feature flags
|
||||
controlling persistent grades.
|
||||
"""
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.db.models import BooleanField
|
||||
from xmodule_django.models import CourseKeyField
|
||||
|
||||
|
||||
class PersistentGradesEnabledFlag(ConfigurationModel):
|
||||
"""
|
||||
Enables persistent grades across the platform.
|
||||
When this feature flag is set to true, individual courses
|
||||
must also have persistent grades enabled for the
|
||||
feature to take effect.
|
||||
"""
|
||||
# this field overrides course-specific settings to enable the feature for all courses
|
||||
enabled_for_all_courses = BooleanField(default=False)
|
||||
|
||||
@classmethod
|
||||
def feature_enabled(cls, course_id=None):
|
||||
"""
|
||||
Looks at the currently active configuration model to determine whether
|
||||
the persistent grades feature is available.
|
||||
|
||||
If the flag is not enabled, the feature is not available.
|
||||
If the flag is enabled and the provided course_id is for an course
|
||||
with persistent grades enabled, the feature is available.
|
||||
If the flag is enabled and no course ID is given,
|
||||
we return True since the global setting is enabled.
|
||||
"""
|
||||
if not PersistentGradesEnabledFlag.is_enabled():
|
||||
return False
|
||||
elif not PersistentGradesEnabledFlag.current().enabled_for_all_courses and course_id:
|
||||
try:
|
||||
return CoursePersistentGradesFlag.objects.get(course_id=course_id).enabled
|
||||
except CoursePersistentGradesFlag.DoesNotExist:
|
||||
return False
|
||||
return True
|
||||
|
||||
class Meta(object):
|
||||
app_label = "grades"
|
||||
|
||||
def __unicode__(self):
|
||||
current_model = PersistentGradesEnabledFlag.current()
|
||||
return u"PersistentGradesEnabledFlag: enabled {}".format(
|
||||
current_model.is_enabled()
|
||||
)
|
||||
|
||||
|
||||
class CoursePersistentGradesFlag(ConfigurationModel):
|
||||
"""
|
||||
Enables persistent grades for a specific
|
||||
course. Only has an effect if the general
|
||||
flag above is set to True.
|
||||
"""
|
||||
KEY_FIELDS = ('course_id',)
|
||||
|
||||
class Meta(object):
|
||||
app_label = "grades"
|
||||
|
||||
# The course that these features are attached to.
|
||||
course_id = CourseKeyField(max_length=255, db_index=True, unique=True)
|
||||
|
||||
def __unicode__(self):
|
||||
not_en = "Not "
|
||||
if self.enabled:
|
||||
not_en = ""
|
||||
# pylint: disable=no-member
|
||||
return u"Course '{}': Persistent Grades {}Enabled".format(self.course_id.to_deprecated_string(), not_en)
|
||||
0
lms/djangoapps/grades/config/tests/__init__.py
Normal file
0
lms/djangoapps/grades/config/tests/__init__.py
Normal file
50
lms/djangoapps/grades/config/tests/test_models.py
Normal file
50
lms/djangoapps/grades/config/tests/test_models.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Tests for the models that control the
|
||||
persistent grading feature.
|
||||
"""
|
||||
import ddt
|
||||
|
||||
from django.test import TestCase
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
|
||||
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class PersistentGradesFeatureFlagTests(TestCase):
|
||||
"""
|
||||
Tests the behavior of the feature flags for persistent grading.
|
||||
These are set via Django admin settings.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(PersistentGradesFeatureFlagTests, self).setUp()
|
||||
self.course_id_1 = CourseLocator(org="edx", course="course", run="run")
|
||||
self.course_id_2 = CourseLocator(org="edx", course="course2", run="run")
|
||||
|
||||
@ddt.data(
|
||||
(True, True, True),
|
||||
(True, True, False),
|
||||
(True, False, True),
|
||||
(True, False, False),
|
||||
(False, True, True),
|
||||
(False, False, True),
|
||||
(False, True, False),
|
||||
(False, False, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_persistent_grades_feature_flags(self, global_flag, enabled_for_all_courses, enabled_for_course_1):
|
||||
with persistent_grades_feature_flags(
|
||||
global_flag=global_flag,
|
||||
enabled_for_all_courses=enabled_for_all_courses,
|
||||
course_id=self.course_id_1,
|
||||
enabled_for_course=enabled_for_course_1
|
||||
):
|
||||
self.assertEqual(PersistentGradesEnabledFlag.feature_enabled(), global_flag)
|
||||
self.assertEqual(
|
||||
PersistentGradesEnabledFlag.feature_enabled(self.course_id_1),
|
||||
global_flag and (enabled_for_all_courses or enabled_for_course_1)
|
||||
)
|
||||
self.assertEqual(
|
||||
PersistentGradesEnabledFlag.feature_enabled(self.course_id_2),
|
||||
global_flag and enabled_for_all_courses
|
||||
)
|
||||
24
lms/djangoapps/grades/config/tests/utils.py
Normal file
24
lms/djangoapps/grades/config/tests/utils.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Provides helper functions for tests that want
|
||||
to configure flags related to persistent grading.
|
||||
"""
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag, CoursePersistentGradesFlag
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
@contextmanager
|
||||
def persistent_grades_feature_flags(
|
||||
global_flag,
|
||||
enabled_for_all_courses=False,
|
||||
course_id=None,
|
||||
enabled_for_course=False
|
||||
):
|
||||
"""
|
||||
Most test cases will use a single call to this manager,
|
||||
as they need to set the global setting and the course-specific
|
||||
setting for a single course.
|
||||
"""
|
||||
PersistentGradesEnabledFlag.objects.create(enabled=global_flag, enabled_for_all_courses=enabled_for_all_courses)
|
||||
if course_id:
|
||||
CoursePersistentGradesFlag.objects.create(course_id=course_id, enabled=enabled_for_course)
|
||||
yield
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
import xmodule_django.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('grades', '0002_rename_last_edited_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CoursePersistentGradesFlag',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('course_id', xmodule_django.models.CourseKeyField(unique=True, max_length=255, db_index=True)),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PersistentGradesEnabledFlag',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('enabled_for_all_courses', models.BooleanField(default=False)),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -7,6 +7,7 @@ from django.conf import settings
|
||||
from lazy import lazy
|
||||
from logging import getLogger
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
|
||||
from openedx.core.djangoapps.signals.signals import GRADES_UPDATED
|
||||
from xmodule import block_metadata_utils
|
||||
|
||||
@@ -228,7 +229,7 @@ class CourseGradeFactory(object):
|
||||
"""
|
||||
Returns the saved grade for the given course and student.
|
||||
"""
|
||||
if settings.FEATURES.get('ENABLE_SUBSECTION_GRADES_SAVED') and course.enable_subsection_grades_saved:
|
||||
if PersistentGradesEnabledFlag.feature_enabled(course.id):
|
||||
# TODO LATER Retrieve the saved grade for the course, if it exists.
|
||||
_pretend_to_save_course_grades()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from course_blocks.api import get_course_blocks
|
||||
from courseware.model_data import ScoresClient
|
||||
from lms.djangoapps.grades.scores import get_score, possibly_scored
|
||||
from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
|
||||
from student.models import anonymous_id_for_user, User
|
||||
from submissions import api as submissions_api
|
||||
from xmodule import block_metadata_utils, graders
|
||||
@@ -180,7 +181,7 @@ class SubsectionGradeFactory(object):
|
||||
from courseware.courses import get_course_by_id # avoids circular import with courseware.py
|
||||
course = get_course_by_id(course_key, depth=0)
|
||||
# save ourselves the extra queries if the course does not use subsection grades
|
||||
if not course.enable_subsection_grades_saved:
|
||||
if not PersistentGradesEnabledFlag.feature_enabled(course.id):
|
||||
return
|
||||
|
||||
course_structure = get_course_blocks(self.student, usage_key)
|
||||
@@ -201,7 +202,7 @@ class SubsectionGradeFactory(object):
|
||||
"""
|
||||
Returns the saved grade for the student and subsection.
|
||||
"""
|
||||
if settings.FEATURES.get('ENABLE_SUBSECTION_GRADES_SAVED') and course.enable_subsection_grades_saved:
|
||||
if PersistentGradesEnabledFlag.feature_enabled(course.id):
|
||||
try:
|
||||
model = PersistentSubsectionGrade.read_grade(
|
||||
user_id=self.student.id,
|
||||
@@ -217,7 +218,7 @@ class SubsectionGradeFactory(object):
|
||||
"""
|
||||
Updates the saved grade for the student and subsection.
|
||||
"""
|
||||
if settings.FEATURES.get('ENABLE_SUBSECTION_GRADES_SAVED') and course.enable_subsection_grades_saved:
|
||||
if PersistentGradesEnabledFlag.feature_enabled(course.id):
|
||||
subsection_grade.save(self.student, subsection, course)
|
||||
|
||||
def _prefetch_scores(self, course_structure, course):
|
||||
|
||||
@@ -3,6 +3,7 @@ Grades related signals.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver, Signal
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
|
||||
from logging import getLogger
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
@@ -122,14 +123,15 @@ def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=u
|
||||
- course_id: Unicode string representing the course
|
||||
- usage_id: Unicode string indicating the courseware instance
|
||||
"""
|
||||
if not settings.FEATURES.get('ENABLE_SUBSECTION_GRADES_SAVED', False):
|
||||
return
|
||||
try:
|
||||
course_id = kwargs.get('course_id', None)
|
||||
usage_id = kwargs.get('usage_id', None)
|
||||
student = kwargs.get('user', None)
|
||||
|
||||
course_key = CourseLocator.from_string(course_id)
|
||||
if not PersistentGradesEnabledFlag.feature_enabled(course_key):
|
||||
return
|
||||
|
||||
usage_key = UsageKey.from_string(usage_id).replace(course_key=course_key)
|
||||
|
||||
from lms.djangoapps.grades.new.subsection_grade import SubsectionGradeFactory
|
||||
|
||||
@@ -12,7 +12,12 @@ from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
|
||||
|
||||
from lms.djangoapps.grades.models import BlockRecord, BlockRecordSet, PersistentSubsectionGrade, VisibleBlocks
|
||||
from lms.djangoapps.grades.models import (
|
||||
BlockRecord,
|
||||
BlockRecordSet,
|
||||
PersistentSubsectionGrade,
|
||||
VisibleBlocks
|
||||
)
|
||||
|
||||
|
||||
class GradesModelTestCase(TestCase):
|
||||
|
||||
@@ -3,12 +3,12 @@ Test saved subsection grade functionality.
|
||||
"""
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from mock import patch
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from courseware.tests.helpers import get_request_for_user
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -62,7 +62,6 @@ class GradeTestBase(SharedModuleStoreTestCase):
|
||||
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.enable_subsection_grades_saved = True
|
||||
CourseEnrollment.enroll(self.request.user, self.course.id)
|
||||
|
||||
|
||||
@@ -90,10 +89,14 @@ class TestCourseGradeFactory(GradeTestBase):
|
||||
# Grades are only saved if the feature flag and the advanced setting are
|
||||
# both set to True.
|
||||
grade_factory = CourseGradeFactory(self.request.user)
|
||||
with patch('lms.djangoapps.grades.new.course_grade._pretend_to_save_course_grades') as mock_save_grades:
|
||||
with patch.dict(settings.FEATURES, {'ENABLE_SUBSECTION_GRADES_SAVED': feature_flag}):
|
||||
with patch.object(self.course, 'enable_subsection_grades_saved', new=course_setting):
|
||||
grade_factory.create(self.course)
|
||||
with persistent_grades_feature_flags(
|
||||
global_flag=feature_flag,
|
||||
enabled_for_all_courses=False,
|
||||
course_id=self.course.id,
|
||||
enabled_for_course=course_setting
|
||||
):
|
||||
with patch('lms.djangoapps.grades.new.course_grade._pretend_to_save_course_grades') as mock_save_grades:
|
||||
grade_factory.create(self.course)
|
||||
self.assertEqual(mock_save_grades.called, feature_flag and course_setting)
|
||||
|
||||
|
||||
@@ -121,26 +124,40 @@ class SubsectionGradeFactoryTest(GradeTestBase):
|
||||
"""
|
||||
Tests to ensure that a persistent subsection grade is created, saved, then fetched on re-request.
|
||||
"""
|
||||
with patch(
|
||||
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._save_grade',
|
||||
wraps=self.subsection_grade_factory._save_grade # pylint: disable=protected-access
|
||||
) as mock_save_grades:
|
||||
with persistent_grades_feature_flags(
|
||||
global_flag=True,
|
||||
enabled_for_all_courses=False,
|
||||
course_id=self.course.id,
|
||||
enabled_for_course=True
|
||||
):
|
||||
with patch(
|
||||
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._get_saved_grade',
|
||||
wraps=self.subsection_grade_factory._get_saved_grade # pylint: disable=protected-access
|
||||
) as mock_get_saved_grade:
|
||||
with self.assertNumQueries(19):
|
||||
grade_a = self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course)
|
||||
self.assertTrue(mock_get_saved_grade.called)
|
||||
self.assertTrue(mock_save_grades.called)
|
||||
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._save_grade',
|
||||
wraps=self.subsection_grade_factory._save_grade # pylint: disable=protected-access
|
||||
) as mock_save_grades:
|
||||
with patch(
|
||||
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._get_saved_grade',
|
||||
wraps=self.subsection_grade_factory._get_saved_grade # pylint: disable=protected-access
|
||||
) as mock_get_saved_grade:
|
||||
with self.assertNumQueries(22):
|
||||
grade_a = self.subsection_grade_factory.create(
|
||||
self.sequence,
|
||||
self.course_structure,
|
||||
self.course
|
||||
)
|
||||
self.assertTrue(mock_get_saved_grade.called)
|
||||
self.assertTrue(mock_save_grades.called)
|
||||
|
||||
mock_get_saved_grade.reset_mock()
|
||||
mock_save_grades.reset_mock()
|
||||
mock_get_saved_grade.reset_mock()
|
||||
mock_save_grades.reset_mock()
|
||||
|
||||
with self.assertNumQueries(3):
|
||||
grade_b = self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course)
|
||||
self.assertTrue(mock_get_saved_grade.called)
|
||||
self.assertFalse(mock_save_grades.called)
|
||||
with self.assertNumQueries(4):
|
||||
grade_b = self.subsection_grade_factory.create(
|
||||
self.sequence,
|
||||
self.course_structure,
|
||||
self.course
|
||||
)
|
||||
self.assertTrue(mock_get_saved_grade.called)
|
||||
self.assertFalse(mock_save_grades.called)
|
||||
|
||||
self.assertEqual(grade_a.url_name, grade_b.url_name)
|
||||
self.assertEqual(grade_a.all_total, grade_b.all_total)
|
||||
@@ -158,9 +175,13 @@ class SubsectionGradeFactoryTest(GradeTestBase):
|
||||
with patch(
|
||||
'lms.djangoapps.grades.models.PersistentSubsectionGrade.read_grade'
|
||||
) as mock_read_saved_grade:
|
||||
with patch.dict(settings.FEATURES, {'ENABLE_SUBSECTION_GRADES_SAVED': feature_flag}):
|
||||
with patch.object(self.course, 'enable_subsection_grades_saved', new=course_setting):
|
||||
self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course)
|
||||
with persistent_grades_feature_flags(
|
||||
global_flag=feature_flag,
|
||||
enabled_for_all_courses=False,
|
||||
course_id=self.course.id,
|
||||
enabled_for_course=course_setting
|
||||
):
|
||||
self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course)
|
||||
self.assertEqual(mock_read_saved_grade.called, feature_flag and course_setting)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Tests for the score change signals defined in the courseware models module.
|
||||
import ddt
|
||||
from unittest import skip
|
||||
from django.test import TestCase
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag, CoursePersistentGradesFlag
|
||||
from mock import patch, MagicMock
|
||||
from student.models import anonymous_id_for_user
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -171,6 +172,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
super(ScoreChangedUpdatesSubsectionGradeTest, self).setUp()
|
||||
self.user = UserFactory()
|
||||
PersistentGradesEnabledFlag.objects.create(enabled=True)
|
||||
|
||||
def set_up_course(self, enable_subsection_grades=True):
|
||||
"""
|
||||
@@ -181,7 +183,8 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
|
||||
org='edx',
|
||||
name='course',
|
||||
run='run',
|
||||
metadata={'enable_subsection_grades_saved': enable_subsection_grades})
|
||||
)
|
||||
CoursePersistentGradesFlag.objects.create(course_id=self.course.id, enabled=enable_subsection_grades)
|
||||
|
||||
self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter")
|
||||
self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Open Sequential")
|
||||
@@ -203,7 +206,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
|
||||
def test_subsection_grade_updated_on_signal(self, default_store):
|
||||
with self.store.default_store(default_store):
|
||||
self.set_up_course()
|
||||
with check_mongo_calls(2) and self.assertNumQueries(15):
|
||||
with check_mongo_calls(2) and self.assertNumQueries(19):
|
||||
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@@ -212,14 +215,14 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
|
||||
self.set_up_course()
|
||||
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2')
|
||||
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(19):
|
||||
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_subsection_grades_not_enabled_on_course(self, default_store):
|
||||
with self.store.default_store(default_store):
|
||||
self.set_up_course(enable_subsection_grades=False)
|
||||
with check_mongo_calls(2) and self.assertNumQueries(0):
|
||||
with check_mongo_calls(2) and self.assertNumQueries(2):
|
||||
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
|
||||
|
||||
@skip("Pending completion of TNL-5089")
|
||||
@@ -231,18 +234,18 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_score_changed_sent_with_feature_flag(self, default_store, feature_flag):
|
||||
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_SUBSECTION_GRADES_SAVED': feature_flag}):
|
||||
with self.store.default_store(default_store):
|
||||
self.set_up_course()
|
||||
with check_mongo_calls(0) and self.assertNumQueries(19 if feature_flag else 1):
|
||||
SCORE_CHANGED.send(sender=None, **self.score_changed_kwargs)
|
||||
PersistentGradesEnabledFlag.objects.create(enabled=feature_flag)
|
||||
with self.store.default_store(default_store):
|
||||
self.set_up_course()
|
||||
with check_mongo_calls(0) and self.assertNumQueries(19 if feature_flag else 1):
|
||||
SCORE_CHANGED.send(sender=None, **self.score_changed_kwargs)
|
||||
|
||||
@ddt.data(
|
||||
('points_possible', 2, 15),
|
||||
('points_earned', 2, 15),
|
||||
('user', 0, 0),
|
||||
('points_possible', 2, 19),
|
||||
('points_earned', 2, 19),
|
||||
('user', 0, 3),
|
||||
('course_id', 0, 0),
|
||||
('usage_id', 0, 0),
|
||||
('usage_id', 0, 2),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_missing_kwargs(self, kwarg, expected_mongo_calls, expected_sql_calls):
|
||||
|
||||
@@ -1671,7 +1671,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
'skipped': 2
|
||||
}
|
||||
|
||||
with self.assertNumQueries(150):
|
||||
with self.assertNumQueries(151):
|
||||
self.assertCertificatesGenerated(task_input, expected_results)
|
||||
|
||||
@ddt.data(
|
||||
|
||||
@@ -124,9 +124,6 @@ FEATURES['ENABLE_TEAMS'] = True
|
||||
# Enable custom content licensing
|
||||
FEATURES['LICENSING'] = True
|
||||
|
||||
# Enable persistent subsection grades, so that feature can be tested.
|
||||
FEATURES['ENABLE_SUBSECTION_GRADES_SAVED'] = True
|
||||
|
||||
# Use the auto_auth workflow for creating users and logging them in
|
||||
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
|
||||
|
||||
|
||||
@@ -356,12 +356,6 @@ FEATURES = {
|
||||
# lives in the Extended table, saving the frontend from
|
||||
# making multiple queries.
|
||||
'ENABLE_READING_FROM_MULTIPLE_HISTORY_TABLES': True,
|
||||
|
||||
# Temporary feature flag for disabling saving of subsection grades.
|
||||
# There is also an advanced setting in the course module. The
|
||||
# feature flag and the advanced setting must both be true for
|
||||
# a course to use saved grades.
|
||||
'ENABLE_SUBSECTION_GRADES_SAVED': False,
|
||||
}
|
||||
|
||||
# Ignore static asset files on import which match this pattern
|
||||
|
||||
@@ -78,9 +78,6 @@ FEATURES['ENABLE_COMBINED_LOGIN_REGISTRATION'] = True
|
||||
# Enable the milestones app in tests to be consistent with it being enabled in production
|
||||
FEATURES['MILESTONES_APP'] = True
|
||||
|
||||
# Enable persistent subsection grades, so that feature can be tested.
|
||||
FEATURES['ENABLE_SUBSECTION_GRADES_SAVED'] = True
|
||||
|
||||
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
|
||||
WIKI_ENABLED = True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user