pyupgrade on lms gating and grades apps (#26532)
This commit is contained in:
@@ -8,9 +8,9 @@ from collections import defaultdict
|
||||
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
|
||||
from common.djangoapps.util import milestones_helpers
|
||||
from lms.djangoapps.courseware.entrance_exams import get_entrance_exam_content
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from common.djangoapps.util import milestones_helpers
|
||||
from openedx.core.toggles import ENTRANCE_EXAMS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -82,5 +82,5 @@ def get_entrance_exam_score_ratio(course_grade, exam_chapter_key):
|
||||
entrance_exam_score_ratio = course_grade.chapter_percentage(exam_chapter_key)
|
||||
except KeyError:
|
||||
entrance_exam_score_ratio = 0.0, 0.0
|
||||
log.warning(u'Gating: Unexpectedly failed to find chapter_grade for %s.', exam_chapter_key)
|
||||
log.warning('Gating: Unexpectedly failed to find chapter_grade for %s.', exam_chapter_key)
|
||||
return entrance_exam_score_ratio
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""
|
||||
Signal handlers for the gating djangoapp
|
||||
"""
|
||||
|
||||
|
||||
import six
|
||||
from completion.models import BlockCompletion
|
||||
from django.db import models
|
||||
from django.dispatch import receiver
|
||||
@@ -37,10 +34,10 @@ def evaluate_subsection_completion_milestones(**kwargs):
|
||||
evaluation of any milestone which can be completed.
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
course_id = six.text_type(instance.context_key)
|
||||
course_id = str(instance.context_key)
|
||||
if not instance.context_key.is_course:
|
||||
return # Content in a library or some other thing that doesn't support milestones
|
||||
block_id = six.text_type(instance.block_key)
|
||||
block_id = str(instance.block_key)
|
||||
user_id = instance.user_id
|
||||
task_evaluate_subsection_completion_milestones.delay(course_id, block_id, user_id)
|
||||
|
||||
|
||||
@@ -5,14 +5,13 @@ This file contains celery tasks related to course content gating.
|
||||
|
||||
import logging
|
||||
|
||||
import six
|
||||
from celery import shared_task
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from edx_django_utils.monitoring import set_code_owner_attribute
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
|
||||
from lms.djangoapps.gating import api as gating_api
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.gating import api as gating_api
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -34,7 +33,7 @@ def task_evaluate_subsection_completion_milestones(course_id, block_id, user_id)
|
||||
course = store.get_course(course_key)
|
||||
if not course or not course.enable_subsection_gating:
|
||||
log.debug(
|
||||
u"Gating: ignoring evaluation of completion milestone because it disabled for course [%s]", course_id
|
||||
"Gating: ignoring evaluation of completion milestone because it disabled for course [%s]", course_id
|
||||
)
|
||||
else:
|
||||
try:
|
||||
@@ -44,12 +43,12 @@ def task_evaluate_subsection_completion_milestones(course_id, block_id, user_id)
|
||||
subsection_block = _get_subsection_of_block(completed_block_usage_key, course_structure)
|
||||
subsection = course_structure[subsection_block]
|
||||
log.debug(
|
||||
u"Gating: Evaluating completion milestone for subsection [%s] and user [%s]",
|
||||
six.text_type(subsection.location), user.id
|
||||
"Gating: Evaluating completion milestone for subsection [%s] and user [%s]",
|
||||
str(subsection.location), user.id
|
||||
)
|
||||
gating_api.evaluate_prerequisite(course, subsection, user)
|
||||
except KeyError:
|
||||
log.error(u"Gating: Given prerequisite subsection [%s] not found in course structure", block_id)
|
||||
log.error("Gating: Given prerequisite subsection [%s] not found in course structure", block_id)
|
||||
|
||||
|
||||
def _get_subsection_of_block(usage_key, block_structure):
|
||||
|
||||
@@ -3,10 +3,11 @@ Unit tests for gating.signals module
|
||||
"""
|
||||
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from ddt import data, ddt, unpack
|
||||
from milestones import api as milestones_api
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
from mock import Mock, patch
|
||||
|
||||
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from lms.djangoapps.gating.api import evaluate_prerequisite
|
||||
@@ -25,7 +26,7 @@ class GatingTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
"""
|
||||
Initial data setup
|
||||
"""
|
||||
super(GatingTestCase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
|
||||
# create course
|
||||
self.course = CourseFactory.create(
|
||||
@@ -65,7 +66,7 @@ class TestEvaluatePrerequisite(GatingTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestEvaluatePrerequisite, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.user_dict = {'id': self.user.id}
|
||||
self.prereq_milestone = None
|
||||
self.subsection_grade = Mock(location=self.seq1.location, percent_graded=0.5)
|
||||
|
||||
@@ -11,12 +11,12 @@ from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
from milestones import api as milestones_api
|
||||
from milestones.tests.utils import MilestonesTestCaseMixin
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.grades.api import CourseGradeFactory
|
||||
from lms.djangoapps.grades.tests.utils import answer_problem
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from openedx.core.lib.gating import api as gating_api
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -31,11 +31,11 @@ class TestGatedContent(MilestonesTestCaseMixin, SharedModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestGatedContent, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.set_up_course()
|
||||
|
||||
def setUp(self):
|
||||
super(TestGatedContent, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.setup_gating_milestone(50, 100)
|
||||
self.non_staff_user = UserFactory()
|
||||
self.staff_user = UserFactory(is_staff=True, is_superuser=True)
|
||||
|
||||
@@ -3,10 +3,10 @@ Unit tests for gating.signals module
|
||||
"""
|
||||
|
||||
|
||||
from mock import Mock, patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from lms.djangoapps.gating.signals import evaluate_subsection_gated_milestones
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.gating.signals import evaluate_subsection_gated_milestones
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -18,7 +18,7 @@ class TestHandleScoreChanged(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestHandleScoreChanged, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create(org='TestX', number='TS01', run='2016_Q1')
|
||||
self.user = UserFactory.create()
|
||||
self.subsection_grade = Mock()
|
||||
|
||||
@@ -11,11 +11,12 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from six import text_type
|
||||
|
||||
from common.djangoapps.track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
||||
# Public Grades Modules
|
||||
from lms.djangoapps.grades import constants, context, course_data, events
|
||||
# Grades APIs that should NOT belong within the Grades subsystem
|
||||
# TODO move Gradebook to be an external feature outside of core Grades
|
||||
from lms.djangoapps.grades.config.waffle import is_writable_gradebook_enabled, gradebook_can_see_bulk_management
|
||||
from lms.djangoapps.grades.config.waffle import gradebook_can_see_bulk_management, is_writable_gradebook_enabled
|
||||
# Public Grades Factories
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
from lms.djangoapps.grades.models_api import *
|
||||
@@ -27,7 +28,6 @@ from lms.djangoapps.grades.subsection_grade_factory import SubsectionGradeFactor
|
||||
from lms.djangoapps.grades.tasks import compute_all_grades_for_course as task_compute_all_grades_for_course
|
||||
from lms.djangoapps.grades.util_services import GradesUtilService
|
||||
from lms.djangoapps.utils import _get_key
|
||||
from common.djangoapps.track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
||||
|
||||
|
||||
def graded_subsections_for_course_id(course_id):
|
||||
@@ -80,8 +80,8 @@ def override_subsection_grade(
|
||||
signals.SUBSECTION_OVERRIDE_CHANGED.send(
|
||||
sender=None,
|
||||
user_id=user_id,
|
||||
course_id=text_type(course_key),
|
||||
usage_id=text_type(usage_key),
|
||||
course_id=str(course_key),
|
||||
usage_id=str(usage_key),
|
||||
only_if_higher=False,
|
||||
modified=override.modified,
|
||||
score_deleted=False,
|
||||
@@ -124,8 +124,8 @@ def undo_override_subsection_grade(user_id, course_key_or_id, usage_key_or_id, f
|
||||
signals.SUBSECTION_OVERRIDE_CHANGED.send(
|
||||
sender=None,
|
||||
user_id=user_id,
|
||||
course_id=text_type(course_key),
|
||||
usage_id=text_type(usage_key),
|
||||
course_id=str(course_key),
|
||||
usage_id=str(usage_key),
|
||||
only_if_higher=False,
|
||||
modified=datetime.now().replace(tzinfo=pytz.UTC), # Not used when score_deleted=True
|
||||
score_deleted=True,
|
||||
|
||||
@@ -17,21 +17,21 @@ class GradesConfig(AppConfig):
|
||||
"""
|
||||
Application Configuration for Grades.
|
||||
"""
|
||||
name = u'lms.djangoapps.grades'
|
||||
name = 'lms.djangoapps.grades'
|
||||
|
||||
plugin_app = {
|
||||
PluginURLs.CONFIG: {
|
||||
ProjectType.LMS: {
|
||||
PluginURLs.NAMESPACE: u'grades_api',
|
||||
PluginURLs.REGEX: u'^api/grades/',
|
||||
PluginURLs.RELATIVE_PATH: u'rest_api.urls',
|
||||
PluginURLs.NAMESPACE: 'grades_api',
|
||||
PluginURLs.REGEX: '^api/grades/',
|
||||
PluginURLs.RELATIVE_PATH: 'rest_api.urls',
|
||||
}
|
||||
},
|
||||
PluginSettings.CONFIG: {
|
||||
ProjectType.LMS: {
|
||||
SettingsType.PRODUCTION: {PluginSettings.RELATIVE_PATH: u'settings.production'},
|
||||
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: u'settings.common'},
|
||||
SettingsType.TEST: {PluginSettings.RELATIVE_PATH: u'settings.test'},
|
||||
SettingsType.PRODUCTION: {PluginSettings.RELATIVE_PATH: 'settings.production'},
|
||||
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: 'settings.common'},
|
||||
SettingsType.TEST: {PluginSettings.RELATIVE_PATH: 'settings.test'},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ log = logging.getLogger(__name__)
|
||||
class CoursePersistentGradesAdminForm(forms.ModelForm):
|
||||
"""Input form for subsection grade enabling, allowing us to verify data."""
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
model = CoursePersistentGradesFlag
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from django.conf import settings
|
||||
from django.db.models import BooleanField, IntegerField, TextField
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
from six import text_type
|
||||
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
@@ -64,12 +63,12 @@ class PersistentGradesEnabledFlag(ConfigurationModel):
|
||||
return effective.enabled if effective is not None else False
|
||||
return True
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
app_label = "grades"
|
||||
|
||||
def __str__(self):
|
||||
current_model = PersistentGradesEnabledFlag.current()
|
||||
return u"PersistentGradesEnabledFlag: enabled {}".format(
|
||||
return "PersistentGradesEnabledFlag: enabled {}".format(
|
||||
current_model.is_enabled()
|
||||
)
|
||||
|
||||
@@ -85,7 +84,7 @@ class CoursePersistentGradesFlag(ConfigurationModel):
|
||||
"""
|
||||
KEY_FIELDS = ('course_id',)
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
app_label = "grades"
|
||||
|
||||
# The course that these features are attached to.
|
||||
@@ -95,18 +94,18 @@ class CoursePersistentGradesFlag(ConfigurationModel):
|
||||
not_en = "Not "
|
||||
if self.enabled:
|
||||
not_en = ""
|
||||
return u"Course '{}': Persistent Grades {}Enabled".format(text_type(self.course_id), not_en)
|
||||
return "Course '{}': Persistent Grades {}Enabled".format(str(self.course_id), not_en)
|
||||
|
||||
|
||||
class ComputeGradesSetting(ConfigurationModel):
|
||||
"""
|
||||
.. no_pii:
|
||||
"""
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
app_label = "grades"
|
||||
|
||||
batch_size = IntegerField(default=100)
|
||||
course_ids = TextField(
|
||||
blank=False,
|
||||
help_text=u"Whitespace-separated list of course keys for which to compute grades."
|
||||
help_text="Whitespace-separated list of course keys for which to compute grades."
|
||||
)
|
||||
|
||||
@@ -5,11 +5,11 @@ persistent grading feature.
|
||||
|
||||
|
||||
import itertools
|
||||
from unittest.mock import patch
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from mock import patch
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
|
||||
@@ -25,7 +25,7 @@ class PersistentGradesFeatureFlagTests(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(PersistentGradesFeatureFlagTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.course_id_1 = CourseLocator(org="edx", course="course", run="run")
|
||||
self.course_id_2 = CourseLocator(org="edx", course="course2", run="run")
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ waffle switches for the Grades app.
|
||||
|
||||
|
||||
from edx_toggles.toggles import LegacyWaffleFlagNamespace, LegacyWaffleSwitch, LegacyWaffleSwitchNamespace
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
|
||||
# Namespace
|
||||
WAFFLE_NAMESPACE = u'grades'
|
||||
WAFFLE_NAMESPACE = 'grades'
|
||||
|
||||
# Switches
|
||||
|
||||
@@ -23,7 +24,7 @@ WAFFLE_NAMESPACE = u'grades'
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/14771
|
||||
# .. toggle_warnings: This requires the PersistentGradesEnabledFlag to be enabled.
|
||||
ASSUME_ZERO_GRADE_IF_ABSENT = u'assume_zero_grade_if_absent'
|
||||
ASSUME_ZERO_GRADE_IF_ABSENT = 'assume_zero_grade_if_absent'
|
||||
# .. toggle_name: grades.disable_regrade_on_policy_change
|
||||
# .. toggle_implementation: WaffleSwitch
|
||||
# .. toggle_default: False
|
||||
@@ -33,7 +34,7 @@ ASSUME_ZERO_GRADE_IF_ABSENT = u'assume_zero_grade_if_absent'
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_warnings: None
|
||||
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/15733
|
||||
DISABLE_REGRADE_ON_POLICY_CHANGE = u'disable_regrade_on_policy_change'
|
||||
DISABLE_REGRADE_ON_POLICY_CHANGE = 'disable_regrade_on_policy_change'
|
||||
|
||||
# Course Flags
|
||||
|
||||
@@ -47,7 +48,7 @@ DISABLE_REGRADE_ON_POLICY_CHANGE = u'disable_regrade_on_policy_change'
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_warnings: None
|
||||
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/20719
|
||||
REJECTED_EXAM_OVERRIDES_GRADE = u'rejected_exam_overrides_grade'
|
||||
REJECTED_EXAM_OVERRIDES_GRADE = 'rejected_exam_overrides_grade'
|
||||
# .. toggle_name: grades.rejected_exam_overrides_grade
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
# .. toggle_default: False
|
||||
@@ -58,7 +59,7 @@ REJECTED_EXAM_OVERRIDES_GRADE = u'rejected_exam_overrides_grade'
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_warnings: None
|
||||
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/19026
|
||||
ENFORCE_FREEZE_GRADE_AFTER_COURSE_END = u'enforce_freeze_grade_after_course_end'
|
||||
ENFORCE_FREEZE_GRADE_AFTER_COURSE_END = 'enforce_freeze_grade_after_course_end'
|
||||
|
||||
# .. toggle_name: grades.writable_gradebook
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
@@ -70,7 +71,7 @@ ENFORCE_FREEZE_GRADE_AFTER_COURSE_END = u'enforce_freeze_grade_after_course_end'
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/19054
|
||||
# .. toggle_warnings: Enabling this requires that the `WRITABLE_GRADEBOOK_URL` setting be properly defined.
|
||||
WRITABLE_GRADEBOOK = u'writable_gradebook'
|
||||
WRITABLE_GRADEBOOK = 'writable_gradebook'
|
||||
|
||||
# .. toggle_name: grades.bulk_management
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
@@ -82,14 +83,14 @@ WRITABLE_GRADEBOOK = u'writable_gradebook'
|
||||
# .. toggle_target_removal_date: None
|
||||
# .. toggle_warnings: None
|
||||
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/21389
|
||||
BULK_MANAGEMENT = u'bulk_management'
|
||||
BULK_MANAGEMENT = 'bulk_management'
|
||||
|
||||
|
||||
def waffle():
|
||||
"""
|
||||
Returns the namespaced, cached, audited Waffle class for Grades.
|
||||
"""
|
||||
return LegacyWaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Grades: ')
|
||||
return LegacyWaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='Grades: ')
|
||||
|
||||
|
||||
def waffle_switch(name):
|
||||
@@ -109,7 +110,7 @@ def waffle_flags():
|
||||
WARNING: do not replicate this pattern. Instead of declaring waffle flag names as strings, you should create
|
||||
LegacyWaffleFlag and CourseWaffleFlag objects as top-level constants.
|
||||
"""
|
||||
namespace = LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Grades: ')
|
||||
namespace = LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix='Grades: ')
|
||||
return {
|
||||
# By default, enable rejected exam grade overrides. Can be disabled on a course-by-course basis.
|
||||
# TODO: After removing this flag, add a migration to remove waffle flag in a follow-up deployment.
|
||||
|
||||
@@ -3,7 +3,7 @@ Constants and Enums used by Grading.
|
||||
"""
|
||||
|
||||
|
||||
class ScoreDatabaseTableEnum(object):
|
||||
class ScoreDatabaseTableEnum:
|
||||
"""
|
||||
The various database tables that store scores.
|
||||
"""
|
||||
@@ -12,7 +12,7 @@ class ScoreDatabaseTableEnum(object):
|
||||
overrides = 'overrides'
|
||||
|
||||
|
||||
class GradeOverrideFeatureEnum(object):
|
||||
proctoring = u'PROCTORING'
|
||||
gradebook = u'GRADEBOOK'
|
||||
class GradeOverrideFeatureEnum:
|
||||
proctoring = 'PROCTORING'
|
||||
gradebook = 'GRADEBOOK'
|
||||
grade_import = 'grade-import'
|
||||
|
||||
@@ -13,7 +13,7 @@ from .transformer import GradesTransformer
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseData(object):
|
||||
class CourseData:
|
||||
"""
|
||||
Utility access layer to intelligently get and cache the
|
||||
requested course data as long as at least one property is
|
||||
@@ -108,15 +108,15 @@ class CourseData(object):
|
||||
"""
|
||||
Return human-readable string representation.
|
||||
"""
|
||||
return u'Course: course_key: {}'.format(self.course_key)
|
||||
return f'Course: course_key: {self.course_key}'
|
||||
|
||||
def full_string(self): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
if self.effective_structure:
|
||||
return u'Course: course_key: {}, version: {}, edited_on: {}, grading_policy: {}'.format(
|
||||
return 'Course: course_key: {}, version: {}, edited_on: {}, grading_policy: {}'.format(
|
||||
self.course_key, self.version, self.edited_on, self.grading_policy_hash,
|
||||
)
|
||||
else:
|
||||
return u'Course: course_key: {}, empty course structure'.format(self.course_key)
|
||||
return f'Course: course_key: {self.course_key}, empty course structure'
|
||||
|
||||
@property
|
||||
def effective_structure(self):
|
||||
|
||||
@@ -6,7 +6,6 @@ CourseGrade Class
|
||||
from abc import abstractmethod
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
import six
|
||||
from ccx_keys.locator import CCXLocator
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
@@ -22,7 +21,7 @@ from .subsection_grade_factory import SubsectionGradeFactory
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CourseGradeBase(object):
|
||||
class CourseGradeBase:
|
||||
"""
|
||||
Base class for Course Grades.
|
||||
"""
|
||||
@@ -38,8 +37,8 @@ class CourseGradeBase(object):
|
||||
self.force_update_subsections = force_update_subsections
|
||||
|
||||
def __str__(self):
|
||||
return u'Course Grade: percent: {}, letter_grade: {}, passed: {}'.format(
|
||||
six.text_type(self.percent),
|
||||
return 'Course Grade: percent: {}, letter_grade: {}, passed: {}'.format(
|
||||
str(self.percent),
|
||||
self.letter_grade,
|
||||
self.passed,
|
||||
)
|
||||
@@ -70,7 +69,7 @@ class CourseGradeBase(object):
|
||||
a dict keyed by subsection format types.
|
||||
"""
|
||||
subsections_by_format = defaultdict(OrderedDict)
|
||||
for chapter in six.itervalues(self.chapter_grades):
|
||||
for chapter in self.chapter_grades.values():
|
||||
for subsection_grade in chapter['sections']:
|
||||
if subsection_grade.graded:
|
||||
graded_total = subsection_grade.graded_total
|
||||
@@ -99,7 +98,7 @@ class CourseGradeBase(object):
|
||||
keyed by subsection location.
|
||||
"""
|
||||
subsection_grades = defaultdict(OrderedDict)
|
||||
for chapter in six.itervalues(self.chapter_grades):
|
||||
for chapter in self.chapter_grades.values():
|
||||
for subsection_grade in chapter['sections']:
|
||||
subsection_grades[subsection_grade.location] = subsection_grade
|
||||
return subsection_grades
|
||||
@@ -110,7 +109,7 @@ class CourseGradeBase(object):
|
||||
Returns a dict of problem scores keyed by their locations.
|
||||
"""
|
||||
problem_scores = {}
|
||||
for chapter in six.itervalues(self.chapter_grades):
|
||||
for chapter in self.chapter_grades.values():
|
||||
for subsection_grade in chapter['sections']:
|
||||
problem_scores.update(subsection_grade.problem_scores)
|
||||
return problem_scores
|
||||
@@ -249,7 +248,7 @@ class CourseGrade(CourseGradeBase):
|
||||
Course Grade class when grades are updated or read from storage.
|
||||
"""
|
||||
def __init__(self, user, course_data, *args, **kwargs):
|
||||
super(CourseGrade, self).__init__(user, course_data, *args, **kwargs) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(user, course_data, *args, **kwargs)
|
||||
self._subsection_grade_factory = SubsectionGradeFactory(user, course_data=course_data)
|
||||
|
||||
def update(self):
|
||||
@@ -278,7 +277,7 @@ class CourseGrade(CourseGradeBase):
|
||||
if assume_zero_if_absent(self.course_data.course_key):
|
||||
return True
|
||||
|
||||
for chapter in six.itervalues(self.chapter_grades):
|
||||
for chapter in self.chapter_grades.values():
|
||||
for subsection_grade in chapter['sections']:
|
||||
if subsection_grade.all_total.first_attempted:
|
||||
return True
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
"""
|
||||
Course Grade Factory Class
|
||||
"""
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
|
||||
import six
|
||||
from six import text_type
|
||||
|
||||
from openedx.core.djangoapps.signals.signals import (
|
||||
COURSE_GRADE_CHANGED,
|
||||
COURSE_GRADE_NOW_FAILED,
|
||||
@@ -24,7 +19,7 @@ from .models_api import prefetch_grade_overrides_and_visible_blocks
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
class CourseGradeFactory(object):
|
||||
class CourseGradeFactory:
|
||||
"""
|
||||
Factory class to create Course Grade objects.
|
||||
"""
|
||||
@@ -108,7 +103,7 @@ class CourseGradeFactory(object):
|
||||
course_data = CourseData(
|
||||
user=None, course=course, collected_block_structure=collected_block_structure, course_key=course_key,
|
||||
)
|
||||
stats_tags = [u'action:{}'.format(course_data.course_key)] # lint-amnesty, pylint: disable=unused-variable
|
||||
stats_tags = [f'action:{course_data.course_key}'] # lint-amnesty, pylint: disable=unused-variable
|
||||
for user in users:
|
||||
yield self._iter_grade_result(user, course_data, force_update)
|
||||
|
||||
@@ -130,10 +125,10 @@ class CourseGradeFactory(object):
|
||||
# Keep marching on even if this student couldn't be graded for
|
||||
# some reason, but log it for future reference.
|
||||
log.exception(
|
||||
u'Cannot grade student %s in course %s because of exception: %s',
|
||||
'Cannot grade student %s in course %s because of exception: %s',
|
||||
user.id,
|
||||
course_data.course_key,
|
||||
text_type(exc)
|
||||
str(exc)
|
||||
)
|
||||
return self.GradeResult(user, None, exc)
|
||||
|
||||
@@ -142,7 +137,7 @@ class CourseGradeFactory(object):
|
||||
"""
|
||||
Returns a ZeroCourseGrade object for the given user and course.
|
||||
"""
|
||||
log.debug(u'Grades: CreateZero, %s, User: %s', six.text_type(course_data), user.id)
|
||||
log.debug('Grades: CreateZero, %s, User: %s', str(course_data), user.id)
|
||||
return ZeroCourseGrade(user, course_data)
|
||||
|
||||
@staticmethod
|
||||
@@ -155,14 +150,14 @@ class CourseGradeFactory(object):
|
||||
raise PersistentCourseGrade.DoesNotExist
|
||||
|
||||
persistent_grade = PersistentCourseGrade.read(user.id, course_data.course_key)
|
||||
log.debug(u'Grades: Read, %s, User: %s, %s', six.text_type(course_data), user.id, persistent_grade)
|
||||
log.debug('Grades: Read, %s, User: %s, %s', str(course_data), user.id, persistent_grade)
|
||||
|
||||
return CourseGrade(
|
||||
user,
|
||||
course_data,
|
||||
persistent_grade.percent_grade,
|
||||
persistent_grade.letter_grade,
|
||||
persistent_grade.letter_grade != u''
|
||||
persistent_grade.letter_grade != ''
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -221,7 +216,7 @@ class CourseGradeFactory(object):
|
||||
)
|
||||
|
||||
log.info(
|
||||
u'Grades: Update, %s, User: %s, %s, persisted: %s',
|
||||
'Grades: Update, %s, User: %s, %s, persisted: %s',
|
||||
course_data.full_string(), user.id, course_grade, should_persist,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""
|
||||
Emits course grade events.
|
||||
"""
|
||||
|
||||
|
||||
import six
|
||||
from crum import get_current_user
|
||||
from eventtracking import tracker
|
||||
|
||||
@@ -15,13 +12,13 @@ from common.djangoapps.track.event_transaction_utils import (
|
||||
set_event_transaction_type
|
||||
)
|
||||
|
||||
COURSE_GRADE_CALCULATED = u'edx.grades.course.grade_calculated'
|
||||
GRADES_OVERRIDE_EVENT_TYPE = u'edx.grades.problem.score_overridden'
|
||||
GRADES_RESCORE_EVENT_TYPE = u'edx.grades.problem.rescored'
|
||||
PROBLEM_SUBMITTED_EVENT_TYPE = u'edx.grades.problem.submitted'
|
||||
STATE_DELETED_EVENT_TYPE = u'edx.grades.problem.state_deleted'
|
||||
SUBSECTION_OVERRIDE_EVENT_TYPE = u'edx.grades.subsection.score_overridden'
|
||||
SUBSECTION_GRADE_CALCULATED = u'edx.grades.subsection.grade_calculated'
|
||||
COURSE_GRADE_CALCULATED = 'edx.grades.course.grade_calculated'
|
||||
GRADES_OVERRIDE_EVENT_TYPE = 'edx.grades.problem.score_overridden'
|
||||
GRADES_RESCORE_EVENT_TYPE = 'edx.grades.problem.rescored'
|
||||
PROBLEM_SUBMITTED_EVENT_TYPE = 'edx.grades.problem.submitted'
|
||||
STATE_DELETED_EVENT_TYPE = 'edx.grades.problem.state_deleted'
|
||||
SUBSECTION_OVERRIDE_EVENT_TYPE = 'edx.grades.subsection.score_overridden'
|
||||
SUBSECTION_GRADE_CALCULATED = 'edx.grades.subsection.grade_calculated'
|
||||
|
||||
|
||||
def grade_updated(**kwargs):
|
||||
@@ -41,13 +38,13 @@ def grade_updated(**kwargs):
|
||||
root_id = create_new_event_transaction_id()
|
||||
set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
|
||||
tracker.emit(
|
||||
six.text_type(PROBLEM_SUBMITTED_EVENT_TYPE),
|
||||
str(PROBLEM_SUBMITTED_EVENT_TYPE),
|
||||
{
|
||||
'user_id': six.text_type(kwargs['user_id']),
|
||||
'course_id': six.text_type(kwargs['course_id']),
|
||||
'problem_id': six.text_type(kwargs['usage_id']),
|
||||
'event_transaction_id': six.text_type(root_id),
|
||||
'event_transaction_type': six.text_type(PROBLEM_SUBMITTED_EVENT_TYPE),
|
||||
'user_id': str(kwargs['user_id']),
|
||||
'course_id': str(kwargs['course_id']),
|
||||
'problem_id': str(kwargs['usage_id']),
|
||||
'event_transaction_id': str(root_id),
|
||||
'event_transaction_type': str(PROBLEM_SUBMITTED_EVENT_TYPE),
|
||||
'weighted_earned': kwargs.get('weighted_earned'),
|
||||
'weighted_possible': kwargs.get('weighted_possible'),
|
||||
}
|
||||
@@ -57,31 +54,31 @@ def grade_updated(**kwargs):
|
||||
current_user = get_current_user()
|
||||
instructor_id = getattr(current_user, 'id', None)
|
||||
tracker.emit(
|
||||
six.text_type(root_type),
|
||||
str(root_type),
|
||||
{
|
||||
'course_id': six.text_type(kwargs['course_id']),
|
||||
'user_id': six.text_type(kwargs['user_id']),
|
||||
'problem_id': six.text_type(kwargs['usage_id']),
|
||||
'course_id': str(kwargs['course_id']),
|
||||
'user_id': str(kwargs['user_id']),
|
||||
'problem_id': str(kwargs['usage_id']),
|
||||
'new_weighted_earned': kwargs.get('weighted_earned'),
|
||||
'new_weighted_possible': kwargs.get('weighted_possible'),
|
||||
'only_if_higher': kwargs.get('only_if_higher'),
|
||||
'instructor_id': six.text_type(instructor_id),
|
||||
'event_transaction_id': six.text_type(get_event_transaction_id()),
|
||||
'event_transaction_type': six.text_type(root_type),
|
||||
'instructor_id': str(instructor_id),
|
||||
'event_transaction_id': str(get_event_transaction_id()),
|
||||
'event_transaction_type': str(root_type),
|
||||
}
|
||||
)
|
||||
|
||||
elif root_type in [SUBSECTION_OVERRIDE_EVENT_TYPE]:
|
||||
tracker.emit(
|
||||
six.text_type(root_type),
|
||||
str(root_type),
|
||||
{
|
||||
'course_id': six.text_type(kwargs['course_id']),
|
||||
'user_id': six.text_type(kwargs['user_id']),
|
||||
'problem_id': six.text_type(kwargs['usage_id']),
|
||||
'course_id': str(kwargs['course_id']),
|
||||
'user_id': str(kwargs['user_id']),
|
||||
'problem_id': str(kwargs['usage_id']),
|
||||
'only_if_higher': kwargs.get('only_if_higher'),
|
||||
'override_deleted': kwargs.get('score_deleted', False),
|
||||
'event_transaction_id': six.text_type(get_event_transaction_id()),
|
||||
'event_transaction_type': six.text_type(root_type),
|
||||
'event_transaction_id': str(get_event_transaction_id()),
|
||||
'event_transaction_type': str(root_type),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -98,19 +95,19 @@ def subsection_grade_calculated(subsection_grade):
|
||||
tracker.emit(
|
||||
event_name,
|
||||
{
|
||||
'user_id': six.text_type(subsection_grade.user_id),
|
||||
'course_id': six.text_type(subsection_grade.course_id),
|
||||
'block_id': six.text_type(subsection_grade.usage_key),
|
||||
'course_version': six.text_type(subsection_grade.course_version),
|
||||
'user_id': str(subsection_grade.user_id),
|
||||
'course_id': str(subsection_grade.course_id),
|
||||
'block_id': str(subsection_grade.usage_key),
|
||||
'course_version': str(subsection_grade.course_version),
|
||||
'weighted_total_earned': subsection_grade.earned_all,
|
||||
'weighted_total_possible': subsection_grade.possible_all,
|
||||
'weighted_graded_earned': subsection_grade.earned_graded,
|
||||
'weighted_graded_possible': subsection_grade.possible_graded,
|
||||
'first_attempted': six.text_type(subsection_grade.first_attempted),
|
||||
'subtree_edited_timestamp': six.text_type(subsection_grade.subtree_edited_timestamp),
|
||||
'event_transaction_id': six.text_type(get_event_transaction_id()),
|
||||
'event_transaction_type': six.text_type(get_event_transaction_type()),
|
||||
'visible_blocks_hash': six.text_type(subsection_grade.visible_blocks_id),
|
||||
'first_attempted': str(subsection_grade.first_attempted),
|
||||
'subtree_edited_timestamp': str(subsection_grade.subtree_edited_timestamp),
|
||||
'event_transaction_id': str(get_event_transaction_id()),
|
||||
'event_transaction_type': str(get_event_transaction_type()),
|
||||
'visible_blocks_hash': str(subsection_grade.visible_blocks_id),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -127,14 +124,14 @@ def course_grade_calculated(course_grade):
|
||||
tracker.emit(
|
||||
event_name,
|
||||
{
|
||||
'user_id': six.text_type(course_grade.user_id),
|
||||
'course_id': six.text_type(course_grade.course_id),
|
||||
'course_version': six.text_type(course_grade.course_version),
|
||||
'user_id': str(course_grade.user_id),
|
||||
'course_id': str(course_grade.course_id),
|
||||
'course_version': str(course_grade.course_version),
|
||||
'percent_grade': course_grade.percent_grade,
|
||||
'letter_grade': six.text_type(course_grade.letter_grade),
|
||||
'course_edited_timestamp': six.text_type(course_grade.course_edited_timestamp),
|
||||
'event_transaction_id': six.text_type(get_event_transaction_id()),
|
||||
'event_transaction_type': six.text_type(get_event_transaction_type()),
|
||||
'grading_policy_hash': six.text_type(course_grade.grading_policy_hash),
|
||||
'letter_grade': str(course_grade.letter_grade),
|
||||
'course_edited_timestamp': str(course_grade.course_edited_timestamp),
|
||||
'event_transaction_id': str(get_event_transaction_id()),
|
||||
'event_transaction_type': str(get_event_transaction_type()),
|
||||
'grading_policy_hash': str(course_grade.grading_policy_hash),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -8,12 +8,11 @@ import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from lms.djangoapps.grades import tasks
|
||||
from lms.djangoapps.grades.config.models import ComputeGradesSetting
|
||||
from openedx.core.lib.command_utils import get_mutually_exclusive_required_option, parse_course_keys
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from lms.djangoapps.grades import tasks
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -107,7 +106,7 @@ class Command(BaseCommand):
|
||||
for task_arg_tuple in tasks._course_task_args(course_key, **options): # lint-amnesty, pylint: disable=protected-access
|
||||
all_args.append(task_arg_tuple)
|
||||
|
||||
all_args.sort(key=lambda x: hashlib.md5('{!r}'.format(x).encode('utf-8')).digest())
|
||||
all_args.sort(key=lambda x: hashlib.md5(f'{x!r}'.encode('utf-8')).digest())
|
||||
|
||||
for args in all_args:
|
||||
yield {
|
||||
|
||||
@@ -7,18 +7,17 @@ in the specified time range.
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import six
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from pytz import utc
|
||||
from submissions.models import Submission
|
||||
|
||||
from common.djangoapps.student.models import user_by_anonymous_id
|
||||
from common.djangoapps.track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
from lms.djangoapps.courseware.models import StudentModule
|
||||
from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum
|
||||
from lms.djangoapps.grades.events import PROBLEM_SUBMITTED_EVENT_TYPE
|
||||
from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3
|
||||
from common.djangoapps.student.models import user_by_anonymous_id
|
||||
from common.djangoapps.track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,12 +66,12 @@ class Command(BaseCommand):
|
||||
continue
|
||||
task_args = {
|
||||
"user_id": record.student_id,
|
||||
"course_id": six.text_type(record.course_id),
|
||||
"usage_id": six.text_type(record.module_state_key),
|
||||
"course_id": str(record.course_id),
|
||||
"usage_id": str(record.module_state_key),
|
||||
"only_if_higher": False,
|
||||
"expected_modified_time": to_timestamp(record.modified),
|
||||
"score_deleted": False,
|
||||
"event_transaction_id": six.text_type(event_transaction_id),
|
||||
"event_transaction_id": str(event_transaction_id),
|
||||
"event_transaction_type": PROBLEM_SUBMITTED_EVENT_TYPE,
|
||||
"score_db_table": ScoreDatabaseTableEnum.courseware_student_module,
|
||||
}
|
||||
@@ -86,12 +85,12 @@ class Command(BaseCommand):
|
||||
task_args = {
|
||||
"user_id": user_by_anonymous_id(record.student_item.student_id).id,
|
||||
"anonymous_user_id": record.student_item.student_id,
|
||||
"course_id": six.text_type(record.student_item.course_id),
|
||||
"usage_id": six.text_type(record.student_item.item_id),
|
||||
"course_id": str(record.student_item.course_id),
|
||||
"usage_id": str(record.student_item.item_id),
|
||||
"only_if_higher": False,
|
||||
"expected_modified_time": to_timestamp(record.created_at),
|
||||
"score_deleted": False,
|
||||
"event_transaction_id": six.text_type(event_transaction_id),
|
||||
"event_transaction_id": str(event_transaction_id),
|
||||
"event_transaction_type": PROBLEM_SUBMITTED_EVENT_TYPE,
|
||||
"score_db_table": ScoreDatabaseTableEnum.submissions,
|
||||
}
|
||||
|
||||
@@ -3,18 +3,16 @@ Tests for compute_grades management command.
|
||||
"""
|
||||
|
||||
# pylint: disable=protected-access
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
import ddt
|
||||
import six
|
||||
from six.moves import range
|
||||
import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management import CommandError, call_command
|
||||
from mock import ANY, patch
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from lms.djangoapps.grades.config.models import ComputeGradesSetting
|
||||
from lms.djangoapps.grades.management.commands import compute_grades
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -29,12 +27,12 @@ class TestComputeGrades(SharedModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestComputeGrades, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.command = compute_grades.Command()
|
||||
|
||||
cls.courses = [CourseFactory.create() for _ in range(cls.num_courses)]
|
||||
cls.course_keys = [six.text_type(course.id) for course in cls.courses]
|
||||
cls.users = [get_user_model().objects.create(username='user{}'.format(idx)) for idx in range(cls.num_users)]
|
||||
cls.course_keys = [str(course.id) for course in cls.courses]
|
||||
cls.users = [get_user_model().objects.create(username=f'user{idx}') for idx in range(cls.num_users)]
|
||||
|
||||
for user in cls.users:
|
||||
for course in cls.courses:
|
||||
@@ -42,11 +40,11 @@ class TestComputeGrades(SharedModuleStoreTestCase):
|
||||
|
||||
def test_select_all_courses(self):
|
||||
courses = self.command._get_course_keys({'all_courses': True})
|
||||
assert set(six.text_type(course) for course in courses) == set(self.course_keys)
|
||||
assert {str(course) for course in courses} == set(self.course_keys)
|
||||
|
||||
def test_specify_courses(self):
|
||||
courses = self.command._get_course_keys({'courses': [self.course_keys[0], self.course_keys[1], 'd/n/e']})
|
||||
assert [six.text_type(course) for course in courses] == [self.course_keys[0], self.course_keys[1], 'd/n/e']
|
||||
assert [str(course) for course in courses] == [self.course_keys[0], self.course_keys[1], 'd/n/e']
|
||||
|
||||
def test_selecting_invalid_course(self):
|
||||
with pytest.raises(CommandError):
|
||||
@@ -55,7 +53,7 @@ class TestComputeGrades(SharedModuleStoreTestCase):
|
||||
def test_from_settings(self):
|
||||
ComputeGradesSetting.objects.create(course_ids=" ".join(self.course_keys))
|
||||
courses = self.command._get_course_keys({'from_settings': True})
|
||||
assert set(six.text_type(course) for course in courses) == set(self.course_keys)
|
||||
assert {str(course) for course in courses} == set(self.course_keys)
|
||||
# test that --from_settings always uses the latest setting
|
||||
ComputeGradesSetting.objects.create(course_ids='badcoursekey')
|
||||
with pytest.raises(CommandError):
|
||||
|
||||
@@ -4,16 +4,15 @@ Tests for recalculate_learner_grades management command.
|
||||
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
from unittest import mock
|
||||
|
||||
import mock
|
||||
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from lms.djangoapps.grades.management.commands import recalculate_learner_grades
|
||||
from lms.djangoapps.grades.tests.test_tasks import HasCourseWithProblemsMixin
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
DATE_FORMAT = u"%Y-%m-%d %H:%M"
|
||||
DATE_FORMAT = "%Y-%m-%d %H:%M"
|
||||
|
||||
|
||||
class TestRecalculateLearnerGrades(HasCourseWithProblemsMixin, ModuleStoreTestCase):
|
||||
@@ -22,7 +21,7 @@ class TestRecalculateLearnerGrades(HasCourseWithProblemsMixin, ModuleStoreTestCa
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestRecalculateLearnerGrades, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.command = recalculate_learner_grades.Command()
|
||||
|
||||
self.course1 = CourseFactory.create()
|
||||
@@ -50,7 +49,7 @@ class TestRecalculateLearnerGrades(HasCourseWithProblemsMixin, ModuleStoreTestCa
|
||||
with NamedTemporaryFile() as csv:
|
||||
csv.write(b"course_id,user_id\n")
|
||||
csv.writelines(
|
||||
"{},{}\n".format(course, user).encode()
|
||||
f"{course},{user}\n".encode()
|
||||
for user, course in self.user_course_pairs
|
||||
)
|
||||
csv.seek(0)
|
||||
|
||||
@@ -4,22 +4,21 @@ Tests for reset_grades management command.
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import ddt
|
||||
import six
|
||||
from django.conf import settings
|
||||
from mock import MagicMock, patch
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import utc
|
||||
|
||||
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum
|
||||
from lms.djangoapps.grades.management.commands import recalculate_subsection_grades
|
||||
from lms.djangoapps.grades.tests.test_tasks import HasCourseWithProblemsMixin
|
||||
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
DATE_FORMAT = u"%Y-%m-%d %H:%M"
|
||||
DATE_FORMAT = "%Y-%m-%d %H:%M"
|
||||
|
||||
|
||||
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
|
||||
@@ -30,7 +29,7 @@ class TestRecalculateSubsectionGrades(HasCourseWithProblemsMixin, ModuleStoreTes
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestRecalculateSubsectionGrades, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.command = recalculate_subsection_grades.Command()
|
||||
|
||||
@patch('lms.djangoapps.grades.management.commands.recalculate_subsection_grades.Submission')
|
||||
@@ -67,13 +66,13 @@ class TestRecalculateSubsectionGrades(HasCourseWithProblemsMixin, ModuleStoreTes
|
||||
self.command.handle(modified_start='2016-08-25 16:42', modified_end='2018-08-25 16:44')
|
||||
kwargs = {
|
||||
"user_id": "ID",
|
||||
"course_id": u'course-v1:x+y+z',
|
||||
"usage_id": u'abc',
|
||||
"course_id": 'course-v1:x+y+z',
|
||||
"usage_id": 'abc',
|
||||
"only_if_higher": False,
|
||||
"expected_modified_time": to_timestamp(utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT))),
|
||||
"score_deleted": False,
|
||||
"event_transaction_id": six.text_type(get_event_transaction_id()),
|
||||
"event_transaction_type": u'edx.grades.problem.submitted',
|
||||
"event_transaction_id": str(get_event_transaction_id()),
|
||||
"event_transaction_type": 'edx.grades.problem.submitted',
|
||||
"score_db_table": score_db_table,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
from django.db import migrations, models
|
||||
@@ -47,6 +44,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='persistentsubsectiongrade',
|
||||
unique_together=set([('course_id', 'user_id', 'usage_key')]),
|
||||
unique_together={('course_id', 'user_id', 'usage_key')},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
from django.db import migrations, models
|
||||
@@ -33,6 +30,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='persistentcoursegrade',
|
||||
unique_together=set([('course_id', 'user_id')]),
|
||||
unique_together={('course_id', 'user_id')},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -18,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='persistentcoursegrade',
|
||||
index_together=set([('passed_timestamp', 'course_id')]),
|
||||
index_together={('passed_timestamp', 'course_id')},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -13,10 +10,10 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.AlterIndexTogether(
|
||||
name='persistentcoursegrade',
|
||||
index_together=set([('passed_timestamp', 'course_id'), ('modified', 'course_id')]),
|
||||
index_together={('passed_timestamp', 'course_id'), ('modified', 'course_id')},
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='persistentsubsectiongrade',
|
||||
index_together=set([('modified', 'course_id', 'usage_key')]),
|
||||
index_together={('modified', 'course_id', 'usage_key')},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -13,6 +10,6 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.AlterIndexTogether(
|
||||
name='persistentsubsectiongrade',
|
||||
index_together=set([('modified', 'course_id', 'usage_key'), ('first_attempted', 'course_id', 'user_id')]),
|
||||
index_together={('modified', 'course_id', 'usage_key'), ('first_attempted', 'course_id', 'user_id')},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
@@ -21,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('batch_size', models.IntegerField(default=100)),
|
||||
('course_ids', models.TextField(help_text=u'Whitespace-separated list of course keys for which to compute grades.')),
|
||||
('course_ids', models.TextField(help_text='Whitespace-separated list of course keys for which to compute grades.')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.16 on 2018-11-27 20:53
|
||||
|
||||
|
||||
@@ -20,8 +19,8 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('override_id', models.IntegerField(db_index=True)),
|
||||
('feature', models.CharField(choices=[(u'PROCTORING', u'proctoring'), (u'GRADEBOOK', u'gradebook')], default=u'PROCTORING', max_length=32)),
|
||||
('action', models.CharField(choices=[(u'CREATEORUPDATE', u'create_or_update'), (u'DELETE', u'delete')], default=u'CREATEORUPDATE', max_length=32)),
|
||||
('feature', models.CharField(choices=[('PROCTORING', 'proctoring'), ('GRADEBOOK', 'gradebook')], default='PROCTORING', max_length=32)),
|
||||
('action', models.CharField(choices=[('CREATEORUPDATE', 'create_or_update'), ('DELETE', 'delete')], default='CREATEORUPDATE', max_length=32)),
|
||||
('comments', models.CharField(blank=True, max_length=300, null=True)),
|
||||
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-06-05 13:59
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.21 on 2019-07-03 14:46
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.27 on 2020-01-07 16:52
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from lms.djangoapps.grades.config.waffle import (
|
||||
|
||||
@@ -15,7 +15,6 @@ from base64 import b64encode
|
||||
from collections import defaultdict, namedtuple
|
||||
from hashlib import sha1
|
||||
|
||||
import six
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user, unused-import
|
||||
from django.db import models
|
||||
@@ -26,7 +25,6 @@ from model_utils.models import TimeStampedModel
|
||||
from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from simple_history.models import HistoricalRecords
|
||||
from six.moves import map
|
||||
|
||||
from lms.djangoapps.courseware.fields import UnsignedBigIntAutoField
|
||||
from lms.djangoapps.grades import constants, events # lint-amnesty, pylint: disable=unused-import
|
||||
@@ -42,7 +40,7 @@ BLOCK_RECORD_LIST_VERSION = 1
|
||||
BlockRecord = namedtuple('BlockRecord', ['locator', 'weight', 'raw_possible', 'graded'])
|
||||
|
||||
|
||||
class BlockRecordList(object):
|
||||
class BlockRecordList:
|
||||
"""
|
||||
An immutable ordered list of BlockRecord objects.
|
||||
"""
|
||||
@@ -89,11 +87,11 @@ class BlockRecordList(object):
|
||||
"""
|
||||
list_of_block_dicts = [block._asdict() for block in self.blocks]
|
||||
for block_dict in list_of_block_dicts:
|
||||
block_dict['locator'] = six.text_type(block_dict['locator']) # BlockUsageLocator is not json-serializable
|
||||
block_dict['locator'] = str(block_dict['locator']) # BlockUsageLocator is not json-serializable
|
||||
data = {
|
||||
u'blocks': list_of_block_dicts,
|
||||
u'course_key': six.text_type(self.course_key),
|
||||
u'version': self.version,
|
||||
'blocks': list_of_block_dicts,
|
||||
'course_key': str(self.course_key),
|
||||
'version': self.version,
|
||||
}
|
||||
return json.dumps(
|
||||
data,
|
||||
@@ -144,16 +142,16 @@ class VisibleBlocks(models.Model):
|
||||
hashed = models.CharField(max_length=100, unique=True)
|
||||
course_id = CourseKeyField(blank=False, max_length=255, db_index=True)
|
||||
|
||||
_CACHE_NAMESPACE = u"grades.models.VisibleBlocks"
|
||||
_CACHE_NAMESPACE = "grades.models.VisibleBlocks"
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
app_label = "grades"
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
String representation of this model.
|
||||
"""
|
||||
return u"VisibleBlocks object - hash:{}, raw json:'{}'".format(self.hashed, self.blocks_json)
|
||||
return f"VisibleBlocks object - hash:{self.hashed}, raw json:'{self.blocks_json}'"
|
||||
|
||||
@property
|
||||
def blocks(self):
|
||||
@@ -200,7 +198,7 @@ class VisibleBlocks(models.Model):
|
||||
else:
|
||||
model, _ = cls.objects.get_or_create(
|
||||
hashed=blocks.hash_value,
|
||||
defaults={u'blocks_json': blocks.json_value, u'course_id': blocks.course_key},
|
||||
defaults={'blocks_json': blocks.json_value, 'course_id': blocks.course_key},
|
||||
)
|
||||
return model
|
||||
|
||||
@@ -261,7 +259,7 @@ class VisibleBlocks(models.Model):
|
||||
|
||||
@classmethod
|
||||
def _cache_key(cls, user_id, course_key):
|
||||
return u"visible_blocks_cache.{}.{}".format(course_key, user_id)
|
||||
return f"visible_blocks_cache.{course_key}.{user_id}"
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -272,7 +270,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
|
||||
.. no_pii:
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
app_label = "grades"
|
||||
unique_together = [
|
||||
# * Specific grades can be pulled using all three columns,
|
||||
@@ -304,8 +302,8 @@ class PersistentSubsectionGrade(TimeStampedModel):
|
||||
usage_key = UsageKeyField(blank=False, max_length=255)
|
||||
|
||||
# Information relating to the state of content when grade was calculated
|
||||
subtree_edited_timestamp = models.DateTimeField(u'Last content edit timestamp', blank=True, null=True)
|
||||
course_version = models.CharField(u'Guid of latest course version', blank=True, max_length=255)
|
||||
subtree_edited_timestamp = models.DateTimeField('Last content edit timestamp', blank=True, null=True)
|
||||
course_version = models.CharField('Guid of latest course version', blank=True, max_length=255)
|
||||
|
||||
# earned/possible refers to the number of points achieved and available to achieve.
|
||||
# graded refers to the subset of all problems that are marked as being graded.
|
||||
@@ -323,7 +321,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
|
||||
visible_blocks = models.ForeignKey(VisibleBlocks, db_column='visible_blocks_hash', to_field='hashed',
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
_CACHE_NAMESPACE = u'grades.models.PersistentSubsectionGrade'
|
||||
_CACHE_NAMESPACE = 'grades.models.PersistentSubsectionGrade'
|
||||
|
||||
@property
|
||||
def full_usage_key(self):
|
||||
@@ -341,7 +339,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
|
||||
Returns a string representation of this model.
|
||||
"""
|
||||
return (
|
||||
u"{} user: {}, course version: {}, subsection: {} ({}). {}/{} graded, {}/{} all, first_attempted: {}"
|
||||
"{} user: {}, course version: {}, subsection: {} ({}). {}/{} graded, {}/{} all, first_attempted: {}"
|
||||
).format(
|
||||
type(self).__name__,
|
||||
self.user_id,
|
||||
@@ -441,8 +439,8 @@ class PersistentSubsectionGrade(TimeStampedModel):
|
||||
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(usage_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Created/updated grade ***{}*** for user ***{}*** in course ***{}***'
|
||||
u'for subsection ***{}*** with default params ***{}***'
|
||||
log.info('Created/updated grade ***{}*** for user ***{}*** in course ***{}***'
|
||||
'for subsection ***{}*** with default params ***{}***'
|
||||
.format(grade, user_id, usage_key.course_key, usage_key, params))
|
||||
|
||||
grade.override = PersistentSubsectionGradeOverride.get_override(user_id, usage_key)
|
||||
@@ -504,7 +502,7 @@ class PersistentSubsectionGrade(TimeStampedModel):
|
||||
|
||||
@classmethod
|
||||
def _cache_key(cls, course_id):
|
||||
return u"subsection_grades_cache.{}".format(course_id)
|
||||
return f"subsection_grades_cache.{course_id}"
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
@@ -515,7 +513,7 @@ class PersistentCourseGrade(TimeStampedModel):
|
||||
.. no_pii:
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
app_label = "grades"
|
||||
# Indices:
|
||||
# (course_id, user_id) for individual grades
|
||||
@@ -538,30 +536,30 @@ class PersistentCourseGrade(TimeStampedModel):
|
||||
course_id = CourseKeyField(blank=False, max_length=255)
|
||||
|
||||
# Information relating to the state of content when grade was calculated
|
||||
course_edited_timestamp = models.DateTimeField(u'Last content edit timestamp', blank=True, null=True)
|
||||
course_version = models.CharField(u'Course content version identifier', blank=True, max_length=255)
|
||||
grading_policy_hash = models.CharField(u'Hash of grading policy', blank=False, max_length=255)
|
||||
course_edited_timestamp = models.DateTimeField('Last content edit timestamp', blank=True, null=True)
|
||||
course_version = models.CharField('Course content version identifier', blank=True, max_length=255)
|
||||
grading_policy_hash = models.CharField('Hash of grading policy', blank=False, max_length=255)
|
||||
|
||||
# Information about the course grade itself
|
||||
percent_grade = models.FloatField(blank=False)
|
||||
letter_grade = models.CharField(u'Letter grade for course', blank=False, max_length=255)
|
||||
letter_grade = models.CharField('Letter grade for course', blank=False, max_length=255)
|
||||
|
||||
# Information related to course completion
|
||||
passed_timestamp = models.DateTimeField(u'Date learner earned a passing grade', blank=True, null=True)
|
||||
passed_timestamp = models.DateTimeField('Date learner earned a passing grade', blank=True, null=True)
|
||||
|
||||
_CACHE_NAMESPACE = u"grades.models.PersistentCourseGrade"
|
||||
_CACHE_NAMESPACE = "grades.models.PersistentCourseGrade"
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Returns a string representation of this model.
|
||||
"""
|
||||
return u', '.join([
|
||||
u"{} user: {}".format(type(self).__name__, self.user_id),
|
||||
u"course version: {}".format(self.course_version),
|
||||
u"grading policy: {}".format(self.grading_policy_hash),
|
||||
u"percent grade: {}%".format(self.percent_grade),
|
||||
u"letter grade: {}".format(self.letter_grade),
|
||||
u"passed timestamp: {}".format(self.passed_timestamp),
|
||||
return ', '.join([
|
||||
"{} user: {}".format(type(self).__name__, self.user_id),
|
||||
f"course version: {self.course_version}",
|
||||
f"grading policy: {self.grading_policy_hash}",
|
||||
f"percent grade: {self.percent_grade}%",
|
||||
f"letter grade: {self.letter_grade}",
|
||||
f"passed timestamp: {self.passed_timestamp}",
|
||||
])
|
||||
|
||||
@classmethod
|
||||
@@ -637,7 +635,7 @@ class PersistentCourseGrade(TimeStampedModel):
|
||||
|
||||
@classmethod
|
||||
def _cache_key(cls, course_id):
|
||||
return u"grades_cache.{}".format(course_id)
|
||||
return f"grades_cache.{course_id}"
|
||||
|
||||
@staticmethod
|
||||
def _emit_grade_calculated_event(grade):
|
||||
@@ -651,7 +649,7 @@ class PersistentSubsectionGradeOverride(models.Model):
|
||||
|
||||
.. no_pii:
|
||||
"""
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
app_label = "grades"
|
||||
|
||||
grade = models.OneToOneField(PersistentSubsectionGrade, related_name='override', on_delete=models.CASCADE)
|
||||
@@ -671,7 +669,7 @@ class PersistentSubsectionGradeOverride(models.Model):
|
||||
# store the reason for the override
|
||||
override_reason = models.CharField(max_length=300, blank=True, null=True)
|
||||
|
||||
_CACHE_NAMESPACE = u"grades.models.PersistentSubsectionGradeOverride"
|
||||
_CACHE_NAMESPACE = "grades.models.PersistentSubsectionGradeOverride"
|
||||
|
||||
# This is necessary because CMS does not install the grades app, but it
|
||||
# imports this models code. Simple History will attempt to connect to the installed
|
||||
@@ -681,12 +679,12 @@ class PersistentSubsectionGradeOverride(models.Model):
|
||||
_history_user = None
|
||||
|
||||
def __str__(self):
|
||||
return u', '.join([
|
||||
u"{}".format(type(self).__name__),
|
||||
u"earned_all_override: {}".format(self.earned_all_override),
|
||||
u"possible_all_override: {}".format(self.possible_all_override),
|
||||
u"earned_graded_override: {}".format(self.earned_graded_override),
|
||||
u"possible_graded_override: {}".format(self.possible_graded_override),
|
||||
return ', '.join([
|
||||
"{}".format(type(self).__name__),
|
||||
f"earned_all_override: {self.earned_all_override}",
|
||||
f"possible_all_override: {self.possible_all_override}",
|
||||
f"earned_graded_override: {self.earned_graded_override}",
|
||||
f"possible_graded_override: {self.possible_graded_override}",
|
||||
])
|
||||
|
||||
def get_history(self):
|
||||
@@ -731,12 +729,12 @@ class PersistentSubsectionGradeOverride(models.Model):
|
||||
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(subsection_grade_model.course_id) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Creating override for user ***{}*** for PersistentSubsectionGrade'
|
||||
u'***{}*** with override data ***{}*** and derived grade_defaults ***{}***.'
|
||||
log.info('Creating override for user ***{}*** for PersistentSubsectionGrade'
|
||||
'***{}*** with override data ***{}*** and derived grade_defaults ***{}***.'
|
||||
.format(requesting_user, subsection_grade_model, override_data, grade_defaults))
|
||||
try:
|
||||
override = PersistentSubsectionGradeOverride.objects.get(grade=subsection_grade_model)
|
||||
for key, value in six.iteritems(grade_defaults):
|
||||
for key, value in grade_defaults.items():
|
||||
setattr(override, key, value)
|
||||
except PersistentSubsectionGradeOverride.DoesNotExist:
|
||||
override = PersistentSubsectionGradeOverride(grade=subsection_grade_model, **grade_defaults)
|
||||
|
||||
@@ -26,7 +26,7 @@ class GradingPolicySerializer(serializers.Serializer):
|
||||
# When the grader dictionary was missing keys, DRF v2 would default to None;
|
||||
# DRF v3 unhelpfully raises an exception.
|
||||
return dict(
|
||||
super(GradingPolicySerializer, self).to_representation( # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().to_representation(
|
||||
defaultdict(lambda: None, instance)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,10 +8,9 @@ from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
|
||||
import six
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Case, Exists, F, OuterRef, When, Q
|
||||
from django.db.models import Case, Exists, F, OuterRef, Q, When
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -22,21 +21,32 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from six import text_type
|
||||
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.roles import BulkRoleCache
|
||||
from common.djangoapps.track.event_transaction_utils import (
|
||||
create_new_event_transaction_id,
|
||||
get_event_transaction_id,
|
||||
get_event_transaction_type,
|
||||
set_event_transaction_type
|
||||
)
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.courseware.courses import get_course_by_id
|
||||
from lms.djangoapps.grades.api import CourseGradeFactory, clear_prefetched_course_and_subsection_grades
|
||||
from lms.djangoapps.grades.api import constants as grades_constants
|
||||
from lms.djangoapps.grades.api import context as grades_context
|
||||
from lms.djangoapps.grades.api import events as grades_events
|
||||
from lms.djangoapps.grades.api import is_writable_gradebook_enabled, prefetch_course_and_subsection_grades
|
||||
from lms.djangoapps.grades.api import gradebook_can_see_bulk_management as can_see_bulk_management
|
||||
from lms.djangoapps.grades.api import is_writable_gradebook_enabled, prefetch_course_and_subsection_grades
|
||||
from lms.djangoapps.grades.course_data import CourseData
|
||||
from lms.djangoapps.grades.grade_utils import are_grades_frozen
|
||||
# TODO these imports break abstraction of the core Grades layer. This code needs
|
||||
# to be refactored so Gradebook views only access public Grades APIs.
|
||||
from lms.djangoapps.grades.models import (
|
||||
PersistentSubsectionGrade,
|
||||
PersistentSubsectionGradeOverride,
|
||||
PersistentCourseGrade,
|
||||
PersistentSubsectionGrade,
|
||||
PersistentSubsectionGradeOverride
|
||||
)
|
||||
from lms.djangoapps.grades.rest_api.serializers import (
|
||||
StudentGradebookEntrySerializer,
|
||||
@@ -46,7 +56,6 @@ from lms.djangoapps.grades.rest_api.v1.utils import USER_MODEL, CourseEnrollment
|
||||
from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade
|
||||
from lms.djangoapps.grades.subsection_grade_factory import SubsectionGradeFactory
|
||||
from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.program_enrollments.api import get_external_key_by_user_and_course
|
||||
from openedx.core.djangoapps.course_groups import cohorts
|
||||
from openedx.core.djangoapps.util.forms import to_bool
|
||||
@@ -58,16 +67,6 @@ from openedx.core.lib.api.view_utils import (
|
||||
view_auth_classes
|
||||
)
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.roles import BulkRoleCache
|
||||
from common.djangoapps.track.event_transaction_utils import (
|
||||
create_new_event_transaction_id,
|
||||
get_event_transaction_id,
|
||||
get_event_transaction_type,
|
||||
set_event_transaction_type
|
||||
)
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.util.misc import get_default_short_labeler
|
||||
|
||||
@@ -326,7 +325,7 @@ class CourseGradingView(BaseCourseView):
|
||||
'assignment_type': subsection.format,
|
||||
'graded': subsection.graded,
|
||||
'short_label': short_label,
|
||||
'module_id': text_type(subsection.location),
|
||||
'module_id': str(subsection.location),
|
||||
'display_name': subsection.display_name,
|
||||
})
|
||||
return subsections
|
||||
@@ -471,7 +470,7 @@ class GradebookView(GradeViewMixin, PaginatedAPIView):
|
||||
'attempted': attempted,
|
||||
'category': subsection_grade.format,
|
||||
'label': short_label,
|
||||
'module_id': text_type(subsection_grade.location),
|
||||
'module_id': str(subsection_grade.location),
|
||||
'percent': subsection_grade.percent_graded,
|
||||
'score_earned': score_earned,
|
||||
'score_possible': score_possible,
|
||||
@@ -496,7 +495,7 @@ class GradebookView(GradeViewMixin, PaginatedAPIView):
|
||||
user_entry['section_breakdown'] = breakdown
|
||||
user_entry['progress_page_url'] = reverse(
|
||||
'student_progress',
|
||||
kwargs=dict(course_id=text_type(course.id), student_id=user.id)
|
||||
kwargs=dict(course_id=str(course.id), student_id=user.id)
|
||||
)
|
||||
user_entry['user_id'] = user.id
|
||||
user_entry['full_name'] = user.profile.name
|
||||
@@ -813,7 +812,7 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
user_id=requested_user_id,
|
||||
usage_id=requested_usage_id,
|
||||
success=False,
|
||||
reason=text_type(exc)
|
||||
reason=str(exc)
|
||||
))
|
||||
continue
|
||||
|
||||
@@ -829,8 +828,8 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
subsection_grade_model = self._create_subsection_grade(user, course, subsection)
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'PersistentSubsectionGrade ***{}*** created for'
|
||||
u' subsection ***{}*** in course ***{}*** for user ***{}***.'
|
||||
log.info('PersistentSubsectionGrade ***{}*** created for'
|
||||
' subsection ***{}*** in course ***{}*** for user ***{}***.'
|
||||
.format(subsection_grade_model, subsection.location, course, user.id))
|
||||
else:
|
||||
self._log_update_result(request.user, requested_user_id, requested_usage_id, success=False)
|
||||
@@ -838,7 +837,7 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
user_id=requested_user_id,
|
||||
usage_id=requested_usage_id,
|
||||
success=False,
|
||||
reason=u'usage_key {} does not exist in this course.'.format(usage_key)
|
||||
reason=f'usage_key {usage_key} does not exist in this course.'
|
||||
))
|
||||
continue
|
||||
|
||||
@@ -850,13 +849,13 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
)
|
||||
result.append(GradebookUpdateResponseItem(
|
||||
user_id=user.id,
|
||||
usage_id=text_type(usage_key),
|
||||
usage_id=str(usage_key),
|
||||
success=True,
|
||||
reason=None
|
||||
))
|
||||
|
||||
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
if all((item.success for item in result)):
|
||||
if all(item.success for item in result):
|
||||
status_code = status.HTTP_202_ACCEPTED
|
||||
|
||||
return Response(
|
||||
@@ -890,13 +889,13 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
kwargs=dict(
|
||||
user_id=subsection_grade_model.user_id,
|
||||
anonymous_user_id=None,
|
||||
course_id=text_type(subsection_grade_model.course_id),
|
||||
usage_id=text_type(subsection_grade_model.usage_key),
|
||||
course_id=str(subsection_grade_model.course_id),
|
||||
usage_id=str(subsection_grade_model.usage_key),
|
||||
only_if_higher=False,
|
||||
expected_modified_time=to_timestamp(override.modified),
|
||||
score_deleted=False,
|
||||
event_transaction_id=six.text_type(get_event_transaction_id()),
|
||||
event_transaction_type=six.text_type(get_event_transaction_type()),
|
||||
event_transaction_id=str(get_event_transaction_id()),
|
||||
event_transaction_type=str(get_event_transaction_type()),
|
||||
score_db_table=grades_constants.ScoreDatabaseTableEnum.overrides,
|
||||
force_update_subsections=True,
|
||||
)
|
||||
@@ -915,7 +914,7 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView):
|
||||
):
|
||||
|
||||
log.info(
|
||||
u'Grades: Bulk_Update, UpdatedByUser: %s, User: %s, Usage: %s, Grade: %s, GradeOverride: %s, Success: %s',
|
||||
'Grades: Bulk_Update, UpdatedByUser: %s, User: %s, Usage: %s, Grade: %s, GradeOverride: %s, Success: %s',
|
||||
request_user.id,
|
||||
user_id,
|
||||
usage_id,
|
||||
|
||||
@@ -6,12 +6,11 @@ Mixins classes being used by all test classes within this folder
|
||||
from datetime import datetime
|
||||
|
||||
from pytz import UTC
|
||||
from six.moves import range
|
||||
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory
|
||||
from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory, ProgramCourseEnrollmentFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory
|
||||
from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
@@ -59,7 +58,7 @@ class GradeViewTestMixin(SharedModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GradeViewTestMixin, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.date = datetime(2013, 1, 22, tzinfo=UTC)
|
||||
cls.course = cls._create_test_course_with_default_grading_policy(
|
||||
display_name='test course', run="Testing_course"
|
||||
@@ -89,7 +88,7 @@ class GradeViewTestMixin(SharedModuleStoreTestCase):
|
||||
|
||||
program_enrollment = ProgramEnrollmentFactory(
|
||||
user=user,
|
||||
external_user_key='program_user_key_{}'.format(index),
|
||||
external_user_key=f'program_user_key_{index}',
|
||||
)
|
||||
|
||||
ProgramCourseEnrollmentFactory(
|
||||
@@ -99,7 +98,7 @@ class GradeViewTestMixin(SharedModuleStoreTestCase):
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(GradeViewTestMixin, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.password = 'test'
|
||||
self.global_staff = GlobalStaffFactory.create()
|
||||
self.student = UserFactory(password=self.password, username='student', email='student@example.com')
|
||||
@@ -143,19 +142,19 @@ class GradeViewTestMixin(SharedModuleStoreTestCase):
|
||||
category='sequential',
|
||||
parent_location=chapter.location,
|
||||
due=datetime(2017, 12, 18, 11, 30, 00),
|
||||
display_name=u'Sequential {} {}'.format(grading_type, num),
|
||||
display_name=f'Sequential {grading_type} {num}',
|
||||
format=grading_type,
|
||||
graded=True,
|
||||
)
|
||||
vertical = ItemFactory.create(
|
||||
category='vertical',
|
||||
parent_location=section.location,
|
||||
display_name=u'Vertical {} {}'.format(grading_type, num),
|
||||
display_name=f'Vertical {grading_type} {num}',
|
||||
)
|
||||
ItemFactory.create(
|
||||
category='problem',
|
||||
parent_location=vertical.location,
|
||||
display_name=u'Problem {} {}'.format(grading_type, num),
|
||||
display_name=f'Problem {grading_type} {num}',
|
||||
)
|
||||
|
||||
return course
|
||||
|
||||
@@ -6,12 +6,13 @@ Tests for the course grading API view
|
||||
import json
|
||||
from collections import OrderedDict, namedtuple
|
||||
from datetime import datetime
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from freezegun import freeze_time
|
||||
from mock import MagicMock, patch
|
||||
from opaque_keys.edx.locator import BlockUsageLocator
|
||||
from pytz import UTC
|
||||
from rest_framework import status
|
||||
@@ -19,7 +20,7 @@ from rest_framework.test import APITestCase
|
||||
from six import text_type
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate
|
||||
from lms.djangoapps.courseware.tests.factories import InstructorFactory, StaffFactory
|
||||
from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK, waffle_flags
|
||||
@@ -38,7 +39,6 @@ from lms.djangoapps.grades.rest_api.v1.views import CourseEnrollmentPagination
|
||||
from lms.djangoapps.grades.subsection_grade import ReadSubsectionGrade
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
@@ -53,7 +53,7 @@ class CourseGradingViewTest(SharedModuleStoreTestCase, APITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseGradingViewTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
|
||||
cls.course = CourseFactory.create(display_name='test course', run="Testing_course")
|
||||
cls.course_key = cls.course.id
|
||||
@@ -177,28 +177,28 @@ class CourseGradingViewTest(SharedModuleStoreTestCase, APITestCase):
|
||||
'assignment_type': None,
|
||||
'display_name': self.subsection1.display_name,
|
||||
'graded': False,
|
||||
'module_id': text_type(self.subsection1.location),
|
||||
'module_id': str(self.subsection1.location),
|
||||
'short_label': None
|
||||
},
|
||||
{
|
||||
'assignment_type': None,
|
||||
'display_name': self.subsection2.display_name,
|
||||
'graded': False,
|
||||
'module_id': text_type(self.subsection2.location),
|
||||
'module_id': str(self.subsection2.location),
|
||||
'short_label': None
|
||||
},
|
||||
{
|
||||
'assignment_type': 'Homework',
|
||||
'display_name': self.homework.display_name,
|
||||
'graded': True,
|
||||
'module_id': text_type(self.homework.location),
|
||||
'module_id': str(self.homework.location),
|
||||
'short_label': 'HW 01',
|
||||
},
|
||||
{
|
||||
'assignment_type': 'Midterm Exam',
|
||||
'display_name': self.midterm.display_name,
|
||||
'graded': True,
|
||||
'module_id': text_type(self.midterm.location),
|
||||
'module_id': str(self.midterm.location),
|
||||
'short_label': 'Midterm 01',
|
||||
},
|
||||
],
|
||||
@@ -243,7 +243,7 @@ class GradebookViewTestBase(GradeViewTestMixin, APITestCase):
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GradebookViewTestBase, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.namespaced_url = 'grades_api:v1:course_gradebook'
|
||||
cls.waffle_flag = waffle_flags()[WRITABLE_GRADEBOOK]
|
||||
|
||||
@@ -362,7 +362,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GradebookViewTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.mock_subsection_grades = {
|
||||
cls.subsections[cls.chapter_1.location][0].location: cls.mock_subsection_grade(
|
||||
cls.subsections[cls.chapter_1.location][0],
|
||||
@@ -398,11 +398,11 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
"""
|
||||
Helper function to create the course gradebook API read url.
|
||||
"""
|
||||
base_url = super(GradebookViewTest, self).get_url(course_key) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
base_url = super().get_url(course_key)
|
||||
if username:
|
||||
return "{0}?username={1}".format(base_url, username)
|
||||
return f"{base_url}?username={username}"
|
||||
if user_contains:
|
||||
return "{0}?user_contains={1}".format(base_url, user_contains)
|
||||
return f"{base_url}?user_contains={user_contains}"
|
||||
return base_url
|
||||
|
||||
@staticmethod
|
||||
@@ -433,7 +433,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
('attempted', True),
|
||||
('category', 'Homework'),
|
||||
('label', 'HW 01'),
|
||||
('module_id', text_type(self.subsections[self.chapter_1.location][0].location)),
|
||||
('module_id', str(self.subsections[self.chapter_1.location][0].location)),
|
||||
('percent', 0.5),
|
||||
('score_earned', 1.0),
|
||||
('score_possible', 2.0),
|
||||
@@ -443,7 +443,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
('attempted', True),
|
||||
('category', 'Lab'),
|
||||
('label', 'Lab 01'),
|
||||
('module_id', text_type(self.subsections[self.chapter_1.location][1].location)),
|
||||
('module_id', str(self.subsections[self.chapter_1.location][1].location)),
|
||||
('percent', 0.5),
|
||||
('score_earned', 1.0),
|
||||
('score_possible', 2.0),
|
||||
@@ -453,7 +453,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
('attempted', True),
|
||||
('category', 'Homework'),
|
||||
('label', 'HW 02'),
|
||||
('module_id', text_type(self.subsections[self.chapter_2.location][0].location)),
|
||||
('module_id', str(self.subsections[self.chapter_2.location][0].location)),
|
||||
('percent', 0.5),
|
||||
('score_earned', 1.0),
|
||||
('score_possible', 2.0),
|
||||
@@ -463,7 +463,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
('attempted', True),
|
||||
('category', 'Lab'),
|
||||
('label', 'Lab 02'),
|
||||
('module_id', text_type(self.subsections[self.chapter_2.location][1].location)),
|
||||
('module_id', str(self.subsections[self.chapter_2.location][1].location)),
|
||||
('percent', 0.5),
|
||||
('score_earned', 1.0),
|
||||
('score_possible', 2.0),
|
||||
@@ -916,7 +916,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
getattr(self, login_method)()
|
||||
# both of our test users are in the audit track, so this is functionally equivalent
|
||||
# to just `?cohort_id=cohort.id`.
|
||||
query = '?cohort_id={}&enrollment_mode={}'.format(cohort.id, CourseMode.AUDIT)
|
||||
query = f'?cohort_id={cohort.id}&enrollment_mode={CourseMode.AUDIT}'
|
||||
resp = self.client.get(
|
||||
self.get_url(course_key=self.course.id) + query
|
||||
)
|
||||
@@ -952,7 +952,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
with override_waffle_flag(self.waffle_flag, active=True):
|
||||
getattr(self, login_method)()
|
||||
resp = self.client.get(
|
||||
self.get_url(course_key=self.course.id) + '?cohort_id={}'.format(empty_cohort.id)
|
||||
self.get_url(course_key=self.course.id) + f'?cohort_id={empty_cohort.id}'
|
||||
)
|
||||
self._assert_empty_response(resp)
|
||||
|
||||
@@ -981,7 +981,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
with override_waffle_flag(self.waffle_flag, active=True):
|
||||
getattr(self, login_method)()
|
||||
resp = self.client.get(
|
||||
self.get_url(course_key=self.course.id) + '?enrollment_mode={}'.format(CourseMode.AUDIT)
|
||||
self.get_url(course_key=self.course.id) + f'?enrollment_mode={CourseMode.AUDIT}'
|
||||
)
|
||||
|
||||
self._assert_data_all_users(resp)
|
||||
@@ -1006,7 +1006,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
with override_waffle_flag(self.waffle_flag, active=True):
|
||||
getattr(self, login_method)()
|
||||
resp = self.client.get(
|
||||
self.get_url(course_key=self.course.id) + '?enrollment_mode={}'.format(CourseMode.VERIFIED)
|
||||
self.get_url(course_key=self.course.id) + f'?enrollment_mode={CourseMode.VERIFIED}'
|
||||
)
|
||||
self._assert_empty_response(resp)
|
||||
|
||||
@@ -1028,7 +1028,7 @@ class GradebookViewTest(GradebookViewTestBase):
|
||||
self.login_staff()
|
||||
query = ''
|
||||
if page_size:
|
||||
query = '?page_size={}'.format(page_size)
|
||||
query = f'?page_size={page_size}'
|
||||
resp = self.client.get(
|
||||
self.get_url(course_key=self.course.id) + query
|
||||
)
|
||||
@@ -1289,7 +1289,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GradebookBulkUpdateViewTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.namespaced_url = 'grades_api:v1:course_gradebook_bulk_update'
|
||||
|
||||
def test_feature_not_enabled(self):
|
||||
@@ -1334,7 +1334,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
post_data = [
|
||||
{
|
||||
'user_id': self.student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][0].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][0].location),
|
||||
'grade': {}, # doesn't matter what we put here.
|
||||
}
|
||||
]
|
||||
@@ -1358,7 +1358,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
post_data = [
|
||||
{
|
||||
'user_id': unenrolled_student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][0].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][0].location),
|
||||
'grade': {}, # doesn't matter what we put here.
|
||||
}
|
||||
]
|
||||
@@ -1372,7 +1372,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
expected_data = [
|
||||
{
|
||||
'user_id': unenrolled_student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][0].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][0].location),
|
||||
'success': False,
|
||||
'reason': 'CourseEnrollment matching query does not exist.',
|
||||
},
|
||||
@@ -1391,7 +1391,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
post_data = [
|
||||
{
|
||||
'user_id': -123,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][0].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][0].location),
|
||||
'grade': {}, # doesn't matter what we put here.
|
||||
}
|
||||
]
|
||||
@@ -1405,7 +1405,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
expected_data = [
|
||||
{
|
||||
'user_id': -123,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][0].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][0].location),
|
||||
'success': False,
|
||||
'reason': 'User matching query does not exist.',
|
||||
},
|
||||
@@ -1478,7 +1478,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
'user_id': self.student.id,
|
||||
'usage_id': usage_id,
|
||||
'success': False,
|
||||
'reason': 'usage_key {} does not exist in this course.'.format(usage_id),
|
||||
'reason': f'usage_key {usage_id} does not exist in this course.',
|
||||
},
|
||||
]
|
||||
assert status.HTTP_422_UNPROCESSABLE_ENTITY == resp.status_code
|
||||
@@ -1495,7 +1495,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
post_data = [
|
||||
{
|
||||
'user_id': self.student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][0].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][0].location),
|
||||
'grade': {
|
||||
'earned_all_override': 3,
|
||||
'possible_all_override': 3,
|
||||
@@ -1505,7 +1505,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
},
|
||||
{
|
||||
'user_id': self.student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][1].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][1].location),
|
||||
'grade': {
|
||||
'earned_all_override': 1,
|
||||
'possible_all_override': 4,
|
||||
@@ -1524,13 +1524,13 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
expected_data = [
|
||||
{
|
||||
'user_id': self.student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][0].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][0].location),
|
||||
'success': True,
|
||||
'reason': None,
|
||||
},
|
||||
{
|
||||
'user_id': self.student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][1].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][1].location),
|
||||
'success': True,
|
||||
'reason': None,
|
||||
},
|
||||
@@ -1541,7 +1541,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
second_post_data = [
|
||||
{
|
||||
'user_id': self.student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][1].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][1].location),
|
||||
'grade': {
|
||||
'earned_all_override': 3,
|
||||
'possible_all_override': 4,
|
||||
@@ -1601,7 +1601,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
post_data = [
|
||||
{
|
||||
'user_id': self.student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][0].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][0].location),
|
||||
'grade': {
|
||||
'earned_all_override': 0,
|
||||
'possible_all_override': 3,
|
||||
@@ -1611,7 +1611,7 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase):
|
||||
},
|
||||
{
|
||||
'user_id': self.student.id,
|
||||
'usage_id': text_type(self.subsections[self.chapter_1.location][1].location),
|
||||
'usage_id': str(self.subsections[self.chapter_1.location][1].location),
|
||||
'grade': {
|
||||
'earned_all_override': 0,
|
||||
'possible_all_override': 4,
|
||||
@@ -1635,7 +1635,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
""" Test for the audit api call """
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(SubsectionGradeViewTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.namespaced_url = 'grades_api:v1:course_grade_overrides'
|
||||
cls.locator_a = BlockUsageLocator(
|
||||
course_key=cls.course_key,
|
||||
@@ -1677,7 +1677,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
'subsection_id': subsection_id or self.subsection_id,
|
||||
}
|
||||
)
|
||||
return "{0}?user_id={1}".format(base_url, user_id or self.user_id)
|
||||
return "{}?user_id={}".format(base_url, user_id or self.user_id)
|
||||
|
||||
@patch('lms.djangoapps.grades.subsection_grade_factory.SubsectionGradeFactory.create')
|
||||
@ddt.data(
|
||||
@@ -1722,8 +1722,8 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
]),
|
||||
'user_id': user_no_grade.id,
|
||||
'override': None,
|
||||
'course_id': text_type(self.usage_key.course_key),
|
||||
'subsection_id': text_type(self.usage_key),
|
||||
'course_id': str(self.usage_key.course_key),
|
||||
'subsection_id': str(self.usage_key),
|
||||
'history': []
|
||||
}
|
||||
assert expected_data == resp.data
|
||||
@@ -1750,8 +1750,8 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
]),
|
||||
'user_id': self.user_id,
|
||||
'override': None,
|
||||
'course_id': text_type(self.course_key),
|
||||
'subsection_id': text_type(self.usage_key),
|
||||
'course_id': str(self.course_key),
|
||||
'subsection_id': str(self.usage_key),
|
||||
'history': []
|
||||
}
|
||||
|
||||
@@ -1793,8 +1793,8 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
('earned_graded_override', 0.0),
|
||||
('possible_graded_override', 8.0)
|
||||
]),
|
||||
'course_id': text_type(self.course_key),
|
||||
'subsection_id': text_type(self.usage_key),
|
||||
'course_id': str(self.course_key),
|
||||
'subsection_id': str(self.usage_key),
|
||||
'history': [OrderedDict([
|
||||
('created', '2019-01-01T00:00:00Z'),
|
||||
('grade_id', 1),
|
||||
@@ -1804,7 +1804,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
('override_reason', None),
|
||||
('system', None),
|
||||
('history_date', '2019-01-01T00:00:00Z'),
|
||||
('history_type', u'+'),
|
||||
('history_type', '+'),
|
||||
('history_user', None),
|
||||
('history_user_id', None),
|
||||
('id', 1),
|
||||
@@ -1851,8 +1851,8 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
('earned_graded_override', 0.0),
|
||||
('possible_graded_override', 8.0)
|
||||
]),
|
||||
'course_id': text_type(self.course_key),
|
||||
'subsection_id': text_type(self.usage_key),
|
||||
'course_id': str(self.course_key),
|
||||
'subsection_id': str(self.usage_key),
|
||||
'history': [OrderedDict([
|
||||
('created', '2019-01-01T00:00:00Z'),
|
||||
('grade_id', 1),
|
||||
@@ -1862,7 +1862,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
('override_reason', None),
|
||||
('system', None),
|
||||
('history_date', '2019-01-01T00:00:00Z'),
|
||||
('history_type', u'+'),
|
||||
('history_type', '+'),
|
||||
('history_user', self.global_staff.username),
|
||||
('history_user_id', self.global_staff.id),
|
||||
('id', 1),
|
||||
@@ -1948,8 +1948,8 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
]),
|
||||
'user_id': other_user.id,
|
||||
'override': None,
|
||||
'course_id': text_type(self.usage_key.course_key),
|
||||
'subsection_id': text_type(self.usage_key),
|
||||
'course_id': str(self.usage_key.course_key),
|
||||
'subsection_id': str(self.usage_key),
|
||||
'history': []
|
||||
}
|
||||
|
||||
@@ -1991,8 +1991,8 @@ class SubsectionGradeViewTest(GradebookViewTestBase):
|
||||
]),
|
||||
'user_id': self.user_id,
|
||||
'override': None,
|
||||
'course_id': text_type(self.usage_key.course_key),
|
||||
'subsection_id': text_type(unreleased_subsection.location),
|
||||
'course_id': str(self.usage_key.course_key),
|
||||
'subsection_id': str(unreleased_subsection.location),
|
||||
'history': []
|
||||
}
|
||||
assert expected_data == resp.data
|
||||
|
||||
@@ -6,28 +6,27 @@ Tests for the views
|
||||
from datetime import datetime
|
||||
|
||||
import ddt
|
||||
import six
|
||||
from django.urls import reverse
|
||||
from pytz import UTC
|
||||
|
||||
from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFactory, AccessTokenFactory
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, StaffFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, StaffFactory
|
||||
from openedx.core.djangoapps.oauth_dispatch.tests.factories import AccessTokenFactory, ApplicationFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class GradingPolicyTestMixin(object):
|
||||
class GradingPolicyTestMixin:
|
||||
"""
|
||||
Mixin class for Grading Policy tests
|
||||
"""
|
||||
view_name = None
|
||||
|
||||
def setUp(self):
|
||||
super(GradingPolicyTestMixin, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.create_user_and_access_token()
|
||||
|
||||
def create_user_and_access_token(self):
|
||||
@@ -39,7 +38,7 @@ class GradingPolicyTestMixin(object):
|
||||
def create_course_data(cls): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
cls.invalid_course_id = 'foo/bar/baz'
|
||||
cls.course = CourseFactory.create(display_name='An Introduction to API Testing', raw_grader=cls.raw_grader)
|
||||
cls.course_id = six.text_type(cls.course.id)
|
||||
cls.course_id = str(cls.course.id)
|
||||
with cls.store.bulk_operations(cls.course.id, emit_signals=False):
|
||||
cls.sequential = ItemFactory.create(
|
||||
category="sequential",
|
||||
@@ -153,7 +152,7 @@ class GradingPolicyTestMixin(object):
|
||||
org="MTD",
|
||||
default_store=modulestore_type,
|
||||
)
|
||||
self.assert_get_for_course(course_id=six.text_type(course.id))
|
||||
self.assert_get_for_course(course_id=str(course.id))
|
||||
|
||||
|
||||
class CourseGradingPolicyTests(GradingPolicyTestMixin, SharedModuleStoreTestCase):
|
||||
@@ -181,14 +180,14 @@ class CourseGradingPolicyTests(GradingPolicyTestMixin, SharedModuleStoreTestCase
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseGradingPolicyTests, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.create_course_data()
|
||||
|
||||
def test_get(self):
|
||||
"""
|
||||
The view should return grading policy for a course.
|
||||
"""
|
||||
response = super(CourseGradingPolicyTests, self).test_get() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
response = super().test_get()
|
||||
|
||||
expected = [
|
||||
{
|
||||
@@ -233,14 +232,14 @@ class CourseGradingPolicyMissingFieldsTests(GradingPolicyTestMixin, SharedModule
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseGradingPolicyMissingFieldsTests, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.create_course_data()
|
||||
|
||||
def test_get(self):
|
||||
"""
|
||||
The view should return grading policy for a course.
|
||||
"""
|
||||
response = super(CourseGradingPolicyMissingFieldsTests, self).test_get() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
response = super().test_get()
|
||||
|
||||
expected = [
|
||||
{
|
||||
|
||||
@@ -4,18 +4,18 @@ Tests for v1 views
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import ddt
|
||||
from django.urls import reverse
|
||||
from mock import MagicMock, patch
|
||||
from opaque_keys import InvalidKeyError
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.grades.rest_api.v1.tests.mixins import GradeViewTestMixin
|
||||
from lms.djangoapps.grades.rest_api.v1.views import CourseGradesView
|
||||
from openedx.core.djangoapps.user_authn.tests.utils import AuthAndScopesTestMixin
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -29,7 +29,7 @@ class SingleUserGradesTests(GradeViewTestMixin, AuthAndScopesTestMixin, APITestC
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(SingleUserGradesTests, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.namespaced_url = 'grades_api:v1:course_grades'
|
||||
|
||||
def get_url(self, username):
|
||||
@@ -40,7 +40,7 @@ class SingleUserGradesTests(GradeViewTestMixin, AuthAndScopesTestMixin, APITestC
|
||||
'course_id': self.course_key,
|
||||
}
|
||||
)
|
||||
return "{0}?username={1}".format(base_url, username)
|
||||
return f"{base_url}?username={username}"
|
||||
|
||||
def assert_success_response_for_student(self, response):
|
||||
""" This method is required by AuthAndScopesTestMixin. """
|
||||
@@ -120,7 +120,7 @@ class SingleUserGradesTests(GradeViewTestMixin, AuthAndScopesTestMixin, APITestC
|
||||
'course_id': 'course-v1:MITx+8.MechCX+2014_T1',
|
||||
}
|
||||
)
|
||||
url = "{0}?username={1}".format(base_url, self.student.username)
|
||||
url = f"{base_url}?username={self.student.username}"
|
||||
resp = self.client.get(url)
|
||||
assert resp.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert 'error_code' in resp.data
|
||||
@@ -166,7 +166,7 @@ class CourseGradesViewTest(GradeViewTestMixin, APITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseGradesViewTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.namespaced_url = 'grades_api:v1:course_grades'
|
||||
|
||||
def get_url(self, course_key=None):
|
||||
|
||||
@@ -15,27 +15,27 @@ urlpatterns = [
|
||||
name='course_grades'
|
||||
),
|
||||
url(
|
||||
r'^courses/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN),
|
||||
fr'^courses/{settings.COURSE_ID_PATTERN}/$',
|
||||
views.CourseGradesView.as_view(),
|
||||
name='course_grades'
|
||||
),
|
||||
url(
|
||||
r'^policy/courses/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN),
|
||||
fr'^policy/courses/{settings.COURSE_ID_PATTERN}/$',
|
||||
views.CourseGradingPolicy.as_view(),
|
||||
name='course_grading_policy'
|
||||
),
|
||||
url(
|
||||
r'^gradebook/{course_id}/$'.format(course_id=settings.COURSE_ID_PATTERN),
|
||||
fr'^gradebook/{settings.COURSE_ID_PATTERN}/$',
|
||||
gradebook_views.GradebookView.as_view(),
|
||||
name='course_gradebook'
|
||||
),
|
||||
url(
|
||||
r'^gradebook/{course_id}/bulk-update$'.format(course_id=settings.COURSE_ID_PATTERN),
|
||||
fr'^gradebook/{settings.COURSE_ID_PATTERN}/bulk-update$',
|
||||
gradebook_views.GradebookBulkUpdateView.as_view(),
|
||||
name='course_gradebook_bulk_update'
|
||||
),
|
||||
url(
|
||||
r'^gradebook/{course_id}/grading-info$'.format(course_id=settings.COURSE_ID_PATTERN),
|
||||
fr'^gradebook/{settings.COURSE_ID_PATTERN}/grading-info$',
|
||||
gradebook_views.CourseGradingView.as_view(),
|
||||
name='course_gradebook_grading_info'
|
||||
),
|
||||
|
||||
@@ -12,10 +12,10 @@ from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework.pagination import CursorPagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.util.query import use_read_replica_if_available
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
|
||||
|
||||
USER_MODEL = get_user_model()
|
||||
|
||||
@@ -45,7 +45,7 @@ class CourseEnrollmentPagination(CursorPagination):
|
||||
Return a response given serialized page data, optional status_code (defaults to 200),
|
||||
and kwargs. Each key-value pair of kwargs is added to the response data.
|
||||
"""
|
||||
resp = super(CourseEnrollmentPagination, self).get_paginated_response(data) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
resp = super().get_paginated_response(data)
|
||||
|
||||
for (key, value) in kwargs.items():
|
||||
resp.data[key] = value
|
||||
@@ -181,6 +181,6 @@ class GradeViewMixin(DeveloperErrorViewMixin):
|
||||
"""
|
||||
Ensures that the user is authenticated (e.g. not an AnonymousUser).
|
||||
"""
|
||||
super(GradeViewMixin, self).perform_authentication(request) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().perform_authentication(request)
|
||||
if request.user.is_anonymous:
|
||||
raise AuthenticationFailed
|
||||
|
||||
@@ -5,7 +5,6 @@ Functionality for problem scores.
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
import six
|
||||
from numpy import around
|
||||
from xblock.core import XBlock
|
||||
|
||||
@@ -105,7 +104,7 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
|
||||
weight = _get_weight_from_block(persisted_block, block)
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Weight for block: ***{}*** is {}'
|
||||
log.info('Weight for block: ***{}*** is {}'
|
||||
.format(str(block.location), weight))
|
||||
|
||||
# Priority order for retrieving the scores:
|
||||
@@ -118,8 +117,8 @@ def get_score(submissions_scores, csm_scores, persisted_block, block):
|
||||
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Calculated raw-earned: {}, raw_possible: {}, weighted_earned: '
|
||||
u'{}, weighted_possible: {}, first_attempted: {} for block: ***{}***.'
|
||||
log.info('Calculated raw-earned: {}, raw_possible: {}, weighted_earned: '
|
||||
'{}, weighted_possible: {}, first_attempted: {} for block: ***{}***.'
|
||||
.format(raw_earned, raw_possible, weighted_earned,
|
||||
weighted_possible, first_attempted, str(block.location)))
|
||||
|
||||
@@ -174,7 +173,7 @@ def _get_score_from_submissions(submissions_scores, block):
|
||||
Returns the score values from the submissions API if found.
|
||||
"""
|
||||
if submissions_scores:
|
||||
submission_value = submissions_scores.get(six.text_type(block.location))
|
||||
submission_value = submissions_scores.get(str(block.location))
|
||||
if submission_value:
|
||||
first_attempted = submission_value['created_at']
|
||||
weighted_earned = submission_value['points_earned']
|
||||
@@ -222,7 +221,7 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
|
||||
"""
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Using _get_score_from_persisted_or_latest_block to calculate score for block: ***{}***.'.format(
|
||||
log.info('Using _get_score_from_persisted_or_latest_block to calculate score for block: ***{}***.'.format(
|
||||
str(block.location)
|
||||
))
|
||||
raw_earned = 0.0
|
||||
@@ -234,8 +233,8 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight):
|
||||
raw_possible = block.transformer_data[GradesTransformer].max_score
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Using latest block content to calculate score for block: ***{}***.')
|
||||
log.info(u'weight for block: ***{}*** is {}.'.format(str(block.location), raw_possible))
|
||||
log.info('Using latest block content to calculate score for block: ***{}***.')
|
||||
log.info('weight for block: ***{}*** is {}.'.format(str(block.location), raw_possible))
|
||||
|
||||
# TODO TNL-5982 remove defensive code for scorables without max_score
|
||||
if raw_possible is None:
|
||||
|
||||
@@ -6,7 +6,7 @@ Grade service
|
||||
from . import api
|
||||
|
||||
|
||||
class GradesService(object):
|
||||
class GradesService:
|
||||
"""
|
||||
Course grade service
|
||||
|
||||
|
||||
@@ -6,29 +6,28 @@ Grades related signals.
|
||||
from contextlib import contextmanager
|
||||
from logging import getLogger
|
||||
|
||||
import six
|
||||
from django.dispatch import receiver
|
||||
from opaque_keys.edx.keys import LearningContextKey
|
||||
from submissions.models import score_reset, score_set
|
||||
from xblock.scorable import ScorableXBlockMixin, Score
|
||||
|
||||
from lms.djangoapps.courseware.model_data import get_score, set_score
|
||||
from openedx.core.djangoapps.course_groups.signals.signals import COHORT_MEMBERSHIP_UPDATED
|
||||
from openedx.core.lib.grade_utils import is_score_higher_or_equal
|
||||
from common.djangoapps.student.models import user_by_anonymous_id
|
||||
from common.djangoapps.student.signals import ENROLLMENT_TRACK_UPDATED
|
||||
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
from lms.djangoapps.courseware.model_data import get_score, set_score
|
||||
from lms.djangoapps.grades.tasks import (
|
||||
RECALCULATE_GRADE_DELAY_SECONDS,
|
||||
recalculate_course_and_subsection_grades_for_user,
|
||||
recalculate_subsection_grade_v3
|
||||
)
|
||||
from openedx.core.djangoapps.course_groups.signals.signals import COHORT_MEMBERSHIP_UPDATED
|
||||
from openedx.core.lib.grade_utils import is_score_higher_or_equal
|
||||
|
||||
from .. import events
|
||||
from ..constants import ScoreDatabaseTableEnum
|
||||
from ..course_grade_factory import CourseGradeFactory
|
||||
from ..scores import weighted_score
|
||||
from lms.djangoapps.grades.tasks import ( # lint-amnesty, pylint: disable=wrong-import-order
|
||||
RECALCULATE_GRADE_DELAY_SECONDS,
|
||||
recalculate_course_and_subsection_grades_for_user,
|
||||
recalculate_subsection_grade_v3
|
||||
)
|
||||
from .signals import (
|
||||
PROBLEM_RAW_SCORE_CHANGED,
|
||||
PROBLEM_WEIGHTED_SCORE_CHANGED,
|
||||
@@ -151,8 +150,8 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
|
||||
if not is_score_higher_or_equal(prev_raw_earned, prev_raw_possible, raw_earned, raw_possible):
|
||||
update_score = False
|
||||
log.warning(
|
||||
u"Grades: Rescore is not higher than previous: "
|
||||
u"user: {}, block: {}, previous: {}/{}, new: {}/{} ".format(
|
||||
"Grades: Rescore is not higher than previous: "
|
||||
"user: {}, block: {}, previous: {}/{}, new: {}/{} ".format(
|
||||
user, block.location, prev_raw_earned, prev_raw_possible, raw_earned, raw_possible,
|
||||
)
|
||||
)
|
||||
@@ -172,8 +171,8 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
|
||||
raw_possible=raw_possible,
|
||||
weight=getattr(block, 'weight', None),
|
||||
user_id=user.id,
|
||||
course_id=six.text_type(block.location.course_key),
|
||||
usage_id=six.text_type(block.location),
|
||||
course_id=str(block.location.course_key),
|
||||
usage_id=str(block.location),
|
||||
only_if_higher=only_if_higher,
|
||||
modified=score_modified_time,
|
||||
score_db_table=ScoreDatabaseTableEnum.courseware_student_module,
|
||||
@@ -233,8 +232,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
|
||||
only_if_higher=kwargs.get('only_if_higher'),
|
||||
expected_modified_time=to_timestamp(kwargs['modified']),
|
||||
score_deleted=kwargs.get('score_deleted', False),
|
||||
event_transaction_id=six.text_type(get_event_transaction_id()),
|
||||
event_transaction_type=six.text_type(get_event_transaction_type()),
|
||||
event_transaction_id=str(get_event_transaction_id()),
|
||||
event_transaction_type=str(get_event_transaction_type()),
|
||||
score_db_table=kwargs['score_db_table'],
|
||||
force_update_subsections=kwargs.get('force_update_subsections', False),
|
||||
),
|
||||
@@ -262,6 +261,6 @@ def recalculate_course_and_subsection_grades(sender, user, course_key, countdown
|
||||
countdown=countdown,
|
||||
kwargs=dict(
|
||||
user_id=user.id,
|
||||
course_key=six.text_type(course_key)
|
||||
course_key=str(course_key)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ from abc import ABCMeta
|
||||
from collections import OrderedDict
|
||||
from logging import getLogger
|
||||
|
||||
import six
|
||||
from django.utils.html import escape
|
||||
from lazy import lazy
|
||||
|
||||
@@ -19,7 +18,7 @@ from xmodule.graders import AggregatedScore, ShowCorrectness
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
class SubsectionGradeBase(six.with_metaclass(ABCMeta, object)):
|
||||
class SubsectionGradeBase(metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract base class for Subsection Grades.
|
||||
"""
|
||||
@@ -79,7 +78,7 @@ class ZeroSubsectionGrade(SubsectionGradeBase):
|
||||
"""
|
||||
|
||||
def __init__(self, subsection, course_data):
|
||||
super(ZeroSubsectionGrade, self).__init__(subsection) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(subsection)
|
||||
self.course_data = course_data
|
||||
|
||||
@property
|
||||
@@ -139,14 +138,14 @@ class ZeroSubsectionGrade(SubsectionGradeBase):
|
||||
return locations
|
||||
|
||||
|
||||
class NonZeroSubsectionGrade(six.with_metaclass(ABCMeta, SubsectionGradeBase)):
|
||||
class NonZeroSubsectionGrade(SubsectionGradeBase, metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract base class for Subsection Grades with
|
||||
possibly NonZero values.
|
||||
"""
|
||||
|
||||
def __init__(self, subsection, all_total, graded_total, override=None):
|
||||
super(NonZeroSubsectionGrade, self).__init__(subsection) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(subsection)
|
||||
self.all_total = all_total
|
||||
self.graded_total = graded_total
|
||||
self.override = override
|
||||
@@ -169,7 +168,7 @@ class NonZeroSubsectionGrade(six.with_metaclass(ABCMeta, SubsectionGradeBase)):
|
||||
):
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Computing block score for block: ***{}*** in course: ***{}***.'.format(
|
||||
log.info('Computing block score for block: ***{}*** in course: ***{}***.'.format(
|
||||
str(block_key),
|
||||
str(block_key.course_key),
|
||||
))
|
||||
@@ -178,8 +177,8 @@ class NonZeroSubsectionGrade(six.with_metaclass(ABCMeta, SubsectionGradeBase)):
|
||||
except KeyError:
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'User\'s access to block: ***{}*** in course: ***{}*** has changed. '
|
||||
u'No block score calculated.'.format(str(block_key), str(block_key.course_key)))
|
||||
log.info('User\'s access to block: ***{}*** in course: ***{}*** has changed. '
|
||||
'No block score calculated.'.format(str(block_key), str(block_key.course_key)))
|
||||
# It's possible that the user's access to that
|
||||
# block has changed since the subsection grade
|
||||
# was last persisted.
|
||||
@@ -187,7 +186,7 @@ class NonZeroSubsectionGrade(six.with_metaclass(ABCMeta, SubsectionGradeBase)):
|
||||
if getattr(block, 'has_score', False):
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Block: ***{}*** in course: ***{}*** HAS has_score attribute. Continuing.'
|
||||
log.info('Block: ***{}*** in course: ***{}*** HAS has_score attribute. Continuing.'
|
||||
.format(str(block_key), str(block_key.course_key)))
|
||||
return get_score(
|
||||
submissions_scores,
|
||||
@@ -197,8 +196,8 @@ class NonZeroSubsectionGrade(six.with_metaclass(ABCMeta, SubsectionGradeBase)):
|
||||
)
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Block: ***{}*** in course: ***{}*** DOES NOT HAVE has_score attribute. '
|
||||
u'No block score calculated.'
|
||||
log.info('Block: ***{}*** in course: ***{}*** DOES NOT HAVE has_score attribute. '
|
||||
'No block score calculated.'
|
||||
.format(str(block_key), str(block_key.course_key)))
|
||||
|
||||
@staticmethod
|
||||
@@ -210,16 +209,16 @@ class NonZeroSubsectionGrade(six.with_metaclass(ABCMeta, SubsectionGradeBase)):
|
||||
used instead.
|
||||
"""
|
||||
score_type = 'graded' if is_graded else 'all'
|
||||
earned_value = getattr(grade_model, 'earned_{}'.format(score_type))
|
||||
possible_value = getattr(grade_model, 'possible_{}'.format(score_type))
|
||||
earned_value = getattr(grade_model, f'earned_{score_type}')
|
||||
possible_value = getattr(grade_model, f'possible_{score_type}')
|
||||
if hasattr(grade_model, 'override'):
|
||||
score_type = 'graded_override' if is_graded else 'all_override'
|
||||
|
||||
earned_override = getattr(grade_model.override, 'earned_{}'.format(score_type))
|
||||
earned_override = getattr(grade_model.override, f'earned_{score_type}')
|
||||
if earned_override is not None:
|
||||
earned_value = earned_override
|
||||
|
||||
possible_override = getattr(grade_model.override, 'possible_{}'.format(score_type))
|
||||
possible_override = getattr(grade_model.override, f'possible_{score_type}')
|
||||
if possible_override is not None:
|
||||
possible_value = possible_override
|
||||
|
||||
@@ -244,7 +243,7 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
|
||||
self.model = model
|
||||
self.factory = factory
|
||||
|
||||
super(ReadSubsectionGrade, self).__init__(subsection, all_total, graded_total, override) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(subsection, all_total, graded_total, override)
|
||||
|
||||
@lazy
|
||||
def problem_scores(self):
|
||||
@@ -283,8 +282,8 @@ class CreateSubsectionGrade(NonZeroSubsectionGrade):
|
||||
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Calculated problem score ***{}*** for block ***{!s}***'
|
||||
u' in subsection ***{}***.'
|
||||
log.info('Calculated problem score ***{}*** for block ***{!s}***'
|
||||
' in subsection ***{}***.'
|
||||
.format(problem_score, block_key, subsection.location))
|
||||
if problem_score:
|
||||
self.problem_scores[block_key] = problem_score
|
||||
@@ -293,11 +292,11 @@ class CreateSubsectionGrade(NonZeroSubsectionGrade):
|
||||
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(subsection.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Calculated aggregate all_total ***{}***'
|
||||
u' and grade_total ***{}*** for subsection ***{}***'
|
||||
log.info('Calculated aggregate all_total ***{}***'
|
||||
' and grade_total ***{}*** for subsection ***{}***'
|
||||
.format(all_total, graded_total, subsection.location))
|
||||
|
||||
super(CreateSubsectionGrade, self).__init__(subsection, all_total, graded_total) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__(subsection, all_total, graded_total)
|
||||
|
||||
def update_or_create_model(self, student, score_deleted=False, force_update_subsections=False):
|
||||
"""
|
||||
@@ -306,8 +305,8 @@ class CreateSubsectionGrade(NonZeroSubsectionGrade):
|
||||
if self._should_persist_per_attempted(score_deleted, force_update_subsections):
|
||||
# TODO: Remove as part of EDUCATOR-4602.
|
||||
if str(self.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019':
|
||||
log.info(u'Updating PersistentSubsectionGrade for student ***{}*** in'
|
||||
u' subsection ***{}*** with params ***{}***.'
|
||||
log.info('Updating PersistentSubsectionGrade for student ***{}*** in'
|
||||
' subsection ***{}*** with params ***{}***.'
|
||||
.format(student.id, self.location, self._persisted_model_params(student)))
|
||||
model = PersistentSubsectionGrade.update_or_create_grade(**self._persisted_model_params(student))
|
||||
|
||||
@@ -379,5 +378,5 @@ class CreateSubsectionGrade(NonZeroSubsectionGrade):
|
||||
return [
|
||||
BlockRecord(location, score.weight, score.raw_possible, score.graded)
|
||||
for location, score in
|
||||
six.iteritems(self.problem_scores)
|
||||
self.problem_scores.items()
|
||||
]
|
||||
|
||||
@@ -10,13 +10,13 @@ from django.conf import settings
|
||||
from lazy import lazy
|
||||
from submissions import api as submissions_api
|
||||
|
||||
from openedx.core.djangoapps.signals.signals import COURSE_ASSESSMENT_GRADE_CHANGED
|
||||
from common.djangoapps.student.models import anonymous_id_for_user
|
||||
from lms.djangoapps.courseware.model_data import ScoresClient
|
||||
from lms.djangoapps.grades.config import assume_zero_if_absent, should_persist_grades
|
||||
from lms.djangoapps.grades.models import PersistentSubsectionGrade
|
||||
from lms.djangoapps.grades.scores import possibly_scored
|
||||
from openedx.core.djangoapps.signals.signals import COURSE_ASSESSMENT_GRADE_CHANGED
|
||||
from openedx.core.lib.grade_utils import is_score_higher_or_equal
|
||||
from common.djangoapps.student.models import anonymous_id_for_user
|
||||
|
||||
from .course_data import CourseData
|
||||
from .subsection_grade import CreateSubsectionGrade, ReadSubsectionGrade, ZeroSubsectionGrade
|
||||
@@ -24,7 +24,7 @@ from .subsection_grade import CreateSubsectionGrade, ReadSubsectionGrade, ZeroSu
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
class SubsectionGradeFactory(object):
|
||||
class SubsectionGradeFactory:
|
||||
"""
|
||||
Factory for Subsection Grades.
|
||||
"""
|
||||
@@ -44,7 +44,7 @@ class SubsectionGradeFactory(object):
|
||||
grade currently exists, even if the assume_zero_if_absent flag is enabled for the course.
|
||||
"""
|
||||
self._log_event(
|
||||
log.debug, u"create, read_only: {0}, subsection: {1}".format(read_only, subsection.location), subsection,
|
||||
log.debug, f"create, read_only: {read_only}, subsection: {subsection.location}", subsection,
|
||||
)
|
||||
|
||||
subsection_grade = self._get_bulk_cached_grade(subsection)
|
||||
@@ -76,7 +76,7 @@ class SubsectionGradeFactory(object):
|
||||
"""
|
||||
Updates the SubsectionGrade object for the student and subsection.
|
||||
"""
|
||||
self._log_event(log.debug, u"update, subsection: {}".format(subsection.location), subsection)
|
||||
self._log_event(log.debug, f"update, subsection: {subsection.location}", subsection)
|
||||
|
||||
calculated_grade = CreateSubsectionGrade(
|
||||
subsection, self.course_data.structure, self._submissions_scores, self._csm_scores,
|
||||
@@ -173,7 +173,7 @@ class SubsectionGradeFactory(object):
|
||||
"""
|
||||
Logs the given statement, for this instance.
|
||||
"""
|
||||
log_func(u"Grades: SGF.{}, course: {}, version: {}, edit: {}, user: {}".format(
|
||||
log_func("Grades: SGF.{}, course: {}, version: {}, edit: {}, user: {}".format(
|
||||
log_statement,
|
||||
self.course_data.course_key,
|
||||
getattr(subsection, 'course_version', None),
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"""
|
||||
This module contains tasks for asynchronous execution of grade updates.
|
||||
"""
|
||||
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
import six
|
||||
from celery import shared_task
|
||||
from celery_utils.persist_on_failure import LoggedPersistOnFailureTask
|
||||
from django.conf import settings
|
||||
@@ -21,13 +18,14 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from submissions import api as sub_api
|
||||
|
||||
from lms.djangoapps.courseware.model_data import get_score
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.grades.config.models import ComputeGradesSetting
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview # lint-amnesty, pylint: disable=unused-import
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.track.event_transaction_utils import set_event_transaction_id, set_event_transaction_type
|
||||
from common.djangoapps.util.date_utils import from_timestamp
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.courseware.model_data import get_score
|
||||
from lms.djangoapps.grades.config.models import ComputeGradesSetting
|
||||
from openedx.core.djangoapps.content.course_overviews.models import \
|
||||
CourseOverview # lint-amnesty, pylint: disable=unused-import
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from .config.waffle import DISABLE_REGRADE_ON_POLICY_CHANGE, waffle
|
||||
@@ -65,7 +63,7 @@ def compute_all_grades_for_course(**kwargs):
|
||||
else:
|
||||
course_key = CourseKey.from_string(kwargs.pop('course_key'))
|
||||
if are_grades_frozen(course_key):
|
||||
log.info(u"Attempted compute_all_grades_for_course for course '%s', but grades are frozen.", course_key)
|
||||
log.info("Attempted compute_all_grades_for_course for course '%s', but grades are frozen.", course_key)
|
||||
return
|
||||
for course_key_string, offset, batch_size in _course_task_args(course_key=course_key, **kwargs):
|
||||
kwargs.update({
|
||||
@@ -122,7 +120,7 @@ def compute_grades_for_course(course_key, offset, batch_size, **kwargs): # pyli
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key)
|
||||
if are_grades_frozen(course_key):
|
||||
log.info(u"Attempted compute_grades_for_course for course '%s', but grades are frozen.", course_key)
|
||||
log.info("Attempted compute_grades_for_course for course '%s', but grades are frozen.", course_key)
|
||||
return
|
||||
|
||||
enrollments = CourseEnrollment.objects.filter(course_id=course_key).order_by('created')
|
||||
@@ -152,14 +150,14 @@ def recalculate_course_and_subsection_grades_for_user(self, **kwargs): # pylint
|
||||
course_key_str = kwargs.get('course_key')
|
||||
|
||||
if not (user_id or course_key_str):
|
||||
message = u'recalculate_course_and_subsection_grades_for_user missing "user" or "course_key" kwargs from {}'
|
||||
message = 'recalculate_course_and_subsection_grades_for_user missing "user" or "course_key" kwargs from {}'
|
||||
raise Exception(message.format(kwargs))
|
||||
|
||||
user = User.objects.get(id=user_id)
|
||||
course_key = CourseKey.from_string(course_key_str)
|
||||
if are_grades_frozen(course_key):
|
||||
log.info(
|
||||
u"Attempted recalculate_course_and_subsection_grades_for_user for course '%s', but grades are frozen.",
|
||||
"Attempted recalculate_course_and_subsection_grades_for_user for course '%s', but grades are frozen.",
|
||||
course_key,
|
||||
)
|
||||
return
|
||||
@@ -215,13 +213,13 @@ def _recalculate_subsection_grade(self, **kwargs):
|
||||
try:
|
||||
course_key = CourseLocator.from_string(kwargs['course_id'])
|
||||
if are_grades_frozen(course_key):
|
||||
log.info(u"Attempted _recalculate_subsection_grade for course '%s', but grades are frozen.", course_key)
|
||||
log.info("Attempted _recalculate_subsection_grade for course '%s', but grades are frozen.", course_key)
|
||||
return
|
||||
|
||||
scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key)
|
||||
|
||||
set_custom_attributes_for_course_key(course_key)
|
||||
set_custom_attribute('usage_id', six.text_type(scored_block_usage_key))
|
||||
set_custom_attribute('usage_id', str(scored_block_usage_key))
|
||||
|
||||
# The request cache is not maintained on celery workers,
|
||||
# where this code runs. So we take the values from the
|
||||
@@ -250,7 +248,7 @@ def _recalculate_subsection_grade(self, **kwargs):
|
||||
)
|
||||
except Exception as exc:
|
||||
if not isinstance(exc, KNOWN_RETRY_ERRORS):
|
||||
log.info(u"tnl-6244 grades unexpected failure: {}. task id: {}. kwargs={}".format(
|
||||
log.info("tnl-6244 grades unexpected failure: {}. task id: {}. kwargs={}".format(
|
||||
repr(exc),
|
||||
self.request.id,
|
||||
kwargs,
|
||||
@@ -271,8 +269,8 @@ def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs):
|
||||
score = sub_api.get_score(
|
||||
{
|
||||
"student_id": kwargs['anonymous_user_id'],
|
||||
"course_id": six.text_type(scored_block_usage_key.course_key),
|
||||
"item_id": six.text_type(scored_block_usage_key),
|
||||
"course_id": str(scored_block_usage_key.course_key),
|
||||
"item_id": str(scored_block_usage_key),
|
||||
"item_type": scored_block_usage_key.block_type,
|
||||
}
|
||||
)
|
||||
@@ -296,8 +294,8 @@ def _has_db_updated_with_new_score(self, scored_block_usage_key, **kwargs):
|
||||
|
||||
if not db_is_updated:
|
||||
log.info(
|
||||
u"Grades: tasks._has_database_updated_with_new_score is False. Task ID: {}. Kwargs: {}. Found "
|
||||
u"modified time: {}".format(
|
||||
"Grades: tasks._has_database_updated_with_new_score is False. Task ID: {}. Kwargs: {}. Found "
|
||||
"modified time: {}".format(
|
||||
self.request.id,
|
||||
kwargs,
|
||||
found_modified_time,
|
||||
@@ -353,12 +351,12 @@ def _course_task_args(course_key, **kwargs):
|
||||
from_settings = kwargs.pop('from_settings', True)
|
||||
enrollment_count = CourseEnrollment.objects.filter(course_id=course_key).count()
|
||||
if enrollment_count == 0:
|
||||
log.warning(u"No enrollments found for {}".format(course_key))
|
||||
log.warning(f"No enrollments found for {course_key}")
|
||||
|
||||
if from_settings is False:
|
||||
batch_size = kwargs.pop('batch_size', 100)
|
||||
else:
|
||||
batch_size = ComputeGradesSetting.current().batch_size
|
||||
|
||||
for offset in six.moves.range(0, enrollment_count, batch_size):
|
||||
yield (six.text_type(course_key), offset, batch_size)
|
||||
for offset in range(0, enrollment_count, batch_size):
|
||||
yield (str(course_key), offset, batch_size)
|
||||
|
||||
@@ -6,10 +6,10 @@ Base file for Grades tests
|
||||
from crum import set_current_request
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
@@ -23,7 +23,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GradeTestBase, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory.create()
|
||||
with cls.store.bulk_operations(cls.course.id):
|
||||
cls.chapter = ItemFactory.create(
|
||||
@@ -78,7 +78,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
|
||||
cls.store.update_item(cls.chapter_2, UserFactory().id)
|
||||
|
||||
def setUp(self):
|
||||
super(GradeTestBase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.addCleanup(set_current_request, None)
|
||||
self.request = get_mock_request(UserFactory())
|
||||
self.client.login(username=self.request.user.username, password="test")
|
||||
|
||||
@@ -6,11 +6,11 @@ Test grading with access changes.
|
||||
from crum import set_current_request
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -27,7 +27,7 @@ class GradesAccessIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreT
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(GradesAccessIntegrationTest, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.store = modulestore()
|
||||
cls.course = CourseFactory.create()
|
||||
cls.chapter = ItemFactory.create(
|
||||
@@ -69,13 +69,13 @@ class GradesAccessIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreT
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(GradesAccessIntegrationTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.addCleanup(set_current_request, None)
|
||||
self.request = get_mock_request(UserFactory())
|
||||
self.student = self.request.user
|
||||
self.client.login(username=self.student.username, password="test")
|
||||
CourseEnrollment.enroll(self.student, self.course.id)
|
||||
self.instructor = UserFactory.create(is_staff=True, username=u'test_instructor', password=u'test')
|
||||
self.instructor = UserFactory.create(is_staff=True, username='test_instructor', password='test')
|
||||
self.refresh_course()
|
||||
|
||||
def test_subsection_access_changed(self):
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
"""
|
||||
Test grading events across apps.
|
||||
"""
|
||||
from unittest.mock import call as mock_call
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
import six
|
||||
from crum import set_current_request
|
||||
from mock import call as mock_call
|
||||
from mock import patch
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
||||
from lms.djangoapps.instructor.enrollment import reset_student_attempts
|
||||
from lms.djangoapps.instructor_task.api import submit_rescore_problem_for_student
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -68,13 +66,13 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
|
||||
def setUp(self):
|
||||
self.reset_course()
|
||||
super(GradesEventIntegrationTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.addCleanup(set_current_request, None)
|
||||
self.request = get_mock_request(UserFactory())
|
||||
self.student = self.request.user
|
||||
self.client.login(username=self.student.username, password="test")
|
||||
CourseEnrollment.enroll(self.student, self.course.id)
|
||||
self.instructor = UserFactory.create(is_staff=True, username=u'test_instructor', password=u'test')
|
||||
self.instructor = UserFactory.create(is_staff=True, username='test_instructor', password='test')
|
||||
self.refresh_course()
|
||||
|
||||
@patch('lms.djangoapps.grades.events.tracker')
|
||||
@@ -88,11 +86,11 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
mock_call(
|
||||
events.PROBLEM_SUBMITTED_EVENT_TYPE,
|
||||
{
|
||||
'user_id': six.text_type(self.student.id),
|
||||
'user_id': str(self.student.id),
|
||||
'event_transaction_id': event_transaction_id,
|
||||
'event_transaction_type': events.PROBLEM_SUBMITTED_EVENT_TYPE,
|
||||
'course_id': six.text_type(self.course.id),
|
||||
'problem_id': six.text_type(self.problem.location),
|
||||
'course_id': str(self.course.id),
|
||||
'problem_id': str(self.problem.location),
|
||||
'weighted_earned': 2.0,
|
||||
'weighted_possible': 2.0,
|
||||
},
|
||||
@@ -100,15 +98,15 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
mock_call(
|
||||
events.COURSE_GRADE_CALCULATED,
|
||||
{
|
||||
'course_version': six.text_type(course.course_version),
|
||||
'course_version': str(course.course_version),
|
||||
'percent_grade': 0.02,
|
||||
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
|
||||
'user_id': six.text_type(self.student.id),
|
||||
'letter_grade': u'',
|
||||
'grading_policy_hash': 'ChVp0lHGQGCevD0t4njna/C44zQ=',
|
||||
'user_id': str(self.student.id),
|
||||
'letter_grade': '',
|
||||
'event_transaction_id': event_transaction_id,
|
||||
'event_transaction_type': events.PROBLEM_SUBMITTED_EVENT_TYPE,
|
||||
'course_id': six.text_type(self.course.id),
|
||||
'course_edited_timestamp': six.text_type(course.subtree_edited_on),
|
||||
'course_id': str(self.course.id),
|
||||
'course_edited_timestamp': str(course.subtree_edited_on),
|
||||
}
|
||||
),
|
||||
],
|
||||
@@ -129,10 +127,10 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
enrollment_tracker.emit.assert_called_with(
|
||||
events.STATE_DELETED_EVENT_TYPE,
|
||||
{
|
||||
'user_id': six.text_type(self.student.id),
|
||||
'course_id': six.text_type(self.course.id),
|
||||
'problem_id': six.text_type(self.problem.location),
|
||||
'instructor_id': six.text_type(self.instructor.id),
|
||||
'user_id': str(self.student.id),
|
||||
'course_id': str(self.course.id),
|
||||
'problem_id': str(self.problem.location),
|
||||
'instructor_id': str(self.instructor.id),
|
||||
'event_transaction_id': event_transaction_id,
|
||||
'event_transaction_type': events.STATE_DELETED_EVENT_TYPE,
|
||||
}
|
||||
@@ -142,14 +140,14 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
events.COURSE_GRADE_CALCULATED,
|
||||
{
|
||||
'percent_grade': 0.0,
|
||||
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
|
||||
'user_id': six.text_type(self.student.id),
|
||||
'letter_grade': u'',
|
||||
'grading_policy_hash': 'ChVp0lHGQGCevD0t4njna/C44zQ=',
|
||||
'user_id': str(self.student.id),
|
||||
'letter_grade': '',
|
||||
'event_transaction_id': event_transaction_id,
|
||||
'event_transaction_type': events.STATE_DELETED_EVENT_TYPE,
|
||||
'course_id': six.text_type(self.course.id),
|
||||
'course_edited_timestamp': six.text_type(course.subtree_edited_on),
|
||||
'course_version': six.text_type(course.course_version),
|
||||
'course_id': str(self.course.id),
|
||||
'course_edited_timestamp': str(course.subtree_edited_on),
|
||||
'course_version': str(course.course_version),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -176,7 +174,7 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
|
||||
# make sure the tracker's context is updated with course info
|
||||
for args in events_tracker.get_tracker().context.call_args_list:
|
||||
assert args[0][1] == {'course_id': six.text_type(self.course.id), 'org_id': six.text_type(self.course.org)}
|
||||
assert args[0][1] == {'course_id': str(self.course.id), 'org_id': str(self.course.org)}
|
||||
|
||||
event_transaction_id = events_tracker.emit.mock_calls[0][1][1]['event_transaction_id']
|
||||
events_tracker.emit.assert_has_calls(
|
||||
@@ -184,13 +182,13 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
mock_call(
|
||||
events.GRADES_RESCORE_EVENT_TYPE,
|
||||
{
|
||||
'course_id': six.text_type(self.course.id),
|
||||
'user_id': six.text_type(self.student.id),
|
||||
'problem_id': six.text_type(self.problem.location),
|
||||
'course_id': str(self.course.id),
|
||||
'user_id': str(self.student.id),
|
||||
'problem_id': str(self.problem.location),
|
||||
'new_weighted_earned': 2,
|
||||
'new_weighted_possible': 2,
|
||||
'only_if_higher': False,
|
||||
'instructor_id': six.text_type(self.instructor.id),
|
||||
'instructor_id': str(self.instructor.id),
|
||||
'event_transaction_id': event_transaction_id,
|
||||
'event_transaction_type': events.GRADES_RESCORE_EVENT_TYPE,
|
||||
},
|
||||
@@ -198,15 +196,15 @@ class GradesEventIntegrationTest(ProblemSubmissionTestMixin, SharedModuleStoreTe
|
||||
mock_call(
|
||||
events.COURSE_GRADE_CALCULATED,
|
||||
{
|
||||
'course_version': six.text_type(course.course_version),
|
||||
'course_version': str(course.course_version),
|
||||
'percent_grade': 0.02,
|
||||
'grading_policy_hash': u'ChVp0lHGQGCevD0t4njna/C44zQ=',
|
||||
'user_id': six.text_type(self.student.id),
|
||||
'letter_grade': u'',
|
||||
'grading_policy_hash': 'ChVp0lHGQGCevD0t4njna/C44zQ=',
|
||||
'user_id': str(self.student.id),
|
||||
'letter_grade': '',
|
||||
'event_transaction_id': event_transaction_id,
|
||||
'event_transaction_type': events.GRADES_RESCORE_EVENT_TYPE,
|
||||
'course_id': six.text_type(self.course.id),
|
||||
'course_edited_timestamp': six.text_type(course.subtree_edited_on),
|
||||
'course_id': str(self.course.id),
|
||||
'course_edited_timestamp': str(course.subtree_edited_on),
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -5,14 +5,13 @@ import itertools
|
||||
import ddt
|
||||
import pytz
|
||||
from crum import set_current_request
|
||||
from six.moves import range
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from xmodule.graders import ProblemScore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
@@ -35,14 +34,14 @@ class TestMultipleProblemTypesSubsectionScores(SharedModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMultipleProblemTypesSubsectionScores, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.load_scoreable_course()
|
||||
chapter1 = cls.course.get_children()[0]
|
||||
cls.seq1 = chapter1.get_children()[0]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
password = u'test'
|
||||
password = 'test'
|
||||
self.student = UserFactory.create(is_staff=False, username=u'test_student', password=password)
|
||||
self.client.login(username=self.student.username, password=password)
|
||||
self.addCleanup(set_current_request, None)
|
||||
@@ -104,13 +103,13 @@ class TestVariedMetadata(ProblemSubmissionTestMixin, ModuleStoreTestCase):
|
||||
persisted score.
|
||||
"""
|
||||
default_problem_metadata = {
|
||||
u'graded': True,
|
||||
u'weight': 2.5,
|
||||
u'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc),
|
||||
'graded': True,
|
||||
'weight': 2.5,
|
||||
'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc),
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestVariedMetadata, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create()
|
||||
with self.store.bulk_operations(self.course.id):
|
||||
self.chapter = ItemFactory.create(
|
||||
@@ -129,7 +128,7 @@ class TestVariedMetadata(ProblemSubmissionTestMixin, ModuleStoreTestCase):
|
||||
category='vertical',
|
||||
display_name='Test Vertical 1'
|
||||
)
|
||||
self.problem_xml = u'''
|
||||
self.problem_xml = '''
|
||||
<problem url_name="capa-optionresponse">
|
||||
<optionresponse>
|
||||
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
|
||||
@@ -171,7 +170,7 @@ class TestVariedMetadata(ProblemSubmissionTestMixin, ModuleStoreTestCase):
|
||||
two) is submitted.
|
||||
"""
|
||||
|
||||
self.submit_question_answer(u'problem', {u'2_1': u'Correct'})
|
||||
self.submit_question_answer('problem', {'2_1': 'Correct'})
|
||||
course_structure = get_course_blocks(self.request.user, self.course.location)
|
||||
subsection_factory = SubsectionGradeFactory(
|
||||
self.request.user,
|
||||
@@ -182,10 +181,10 @@ class TestVariedMetadata(ProblemSubmissionTestMixin, ModuleStoreTestCase):
|
||||
|
||||
@ddt.data(
|
||||
({}, 1.25, 2.5),
|
||||
({u'weight': 27}, 13.5, 27),
|
||||
({u'weight': 1.0}, 0.5, 1.0),
|
||||
({u'weight': 0.0}, 0.0, 0.0),
|
||||
({u'weight': None}, 1.0, 2.0),
|
||||
({'weight': 27}, 13.5, 27),
|
||||
({'weight': 1.0}, 0.5, 1.0),
|
||||
({'weight': 0.0}, 0.0, 0.0),
|
||||
({'weight': None}, 1.0, 2.0),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_weight_metadata_alterations(self, alterations, expected_earned, expected_possible):
|
||||
@@ -195,8 +194,8 @@ class TestVariedMetadata(ProblemSubmissionTestMixin, ModuleStoreTestCase):
|
||||
assert score.all_total.possible == expected_possible
|
||||
|
||||
@ddt.data(
|
||||
({u'graded': True}, 1.25, 2.5),
|
||||
({u'graded': False}, 0.0, 0.0),
|
||||
({'graded': True}, 1.25, 2.5),
|
||||
({'graded': False}, 0.0, 0.0),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_graded_metadata_alterations(self, alterations, expected_earned, expected_possible):
|
||||
@@ -214,7 +213,7 @@ class TestWeightedProblems(SharedModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestWeightedProblems, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory.create()
|
||||
with cls.store.bulk_operations(cls.course.id):
|
||||
cls.chapter = ItemFactory.create(parent=cls.course, category="chapter", display_name="chapter")
|
||||
@@ -227,13 +226,13 @@ class TestWeightedProblems(SharedModuleStoreTestCase):
|
||||
ItemFactory.create(
|
||||
parent=cls.vertical,
|
||||
category="problem",
|
||||
display_name="problem_{}".format(i),
|
||||
display_name=f"problem_{i}",
|
||||
data=problem_xml,
|
||||
)
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(TestWeightedProblems, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.user = UserFactory()
|
||||
self.addCleanup(set_current_request, None)
|
||||
self.request = get_mock_request(self.user)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
""" Tests calling the grades api directly """
|
||||
|
||||
|
||||
import ddt
|
||||
from mock import patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import ddt
|
||||
|
||||
from lms.djangoapps.grades import api
|
||||
from lms.djangoapps.grades.models import (
|
||||
PersistentSubsectionGrade,
|
||||
PersistentSubsectionGradeOverride,
|
||||
)
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.grades import api
|
||||
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
@@ -22,7 +20,7 @@ class OverrideSubsectionGradeTests(ModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super(OverrideSubsectionGradeTests, cls).setUpTestData()
|
||||
super().setUpTestData()
|
||||
cls.user = UserFactory()
|
||||
cls.overriding_user = UserFactory()
|
||||
cls.signal_patcher = patch('lms.djangoapps.grades.signals.signals.SUBSECTION_OVERRIDE_CHANGED.send')
|
||||
@@ -35,13 +33,13 @@ class OverrideSubsectionGradeTests(ModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(OverrideSubsectionGradeTests, cls).tearDownClass()
|
||||
super().tearDownClass()
|
||||
cls.signal_patcher.stop()
|
||||
cls.id_patcher.stop()
|
||||
cls.type_patcher.stop()
|
||||
|
||||
def setUp(self):
|
||||
super(OverrideSubsectionGradeTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course', run='Spring2019')
|
||||
self.subsection = ItemFactory.create(parent=self.course, category="subsection", display_name="Subsection")
|
||||
self.grade = PersistentSubsectionGrade.update_or_create_grade(
|
||||
@@ -57,7 +55,7 @@ class OverrideSubsectionGradeTests(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
super(OverrideSubsectionGradeTests, self).tearDown() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().tearDown()
|
||||
PersistentSubsectionGradeOverride.objects.all().delete() # clear out all previous overrides
|
||||
|
||||
@ddt.data(0.0, None, 3.0)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"""
|
||||
Tests for CourseData utility class.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import six
|
||||
from mock import patch
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from openedx.core.djangoapps.content.block_structure.api import get_course_in_cache
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -22,7 +21,7 @@ class CourseDataTest(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(CourseDataTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
with self.store.default_store(ModuleStoreEnum.Type.split):
|
||||
self.course = CourseFactory.create()
|
||||
# need to re-retrieve the course since the version on the original course isn't accurate.
|
||||
@@ -77,7 +76,7 @@ class CourseDataTest(ModuleStoreTestCase):
|
||||
assert course_data.course.id == self.course.id
|
||||
assert course_data.version == self.course.course_version
|
||||
assert course_data.edited_on == expected_edited_on
|
||||
assert u'Course: course_key' in six.text_type(course_data)
|
||||
assert u'Course: course_key' in str(course_data)
|
||||
assert u'Course: course_key' in course_data.full_string()
|
||||
|
||||
def test_no_data(self):
|
||||
@@ -93,8 +92,8 @@ class CourseDataTest(ModuleStoreTestCase):
|
||||
course_data = CourseData(
|
||||
self.user, structure=empty_structure, collected_block_structure=self.collected_structure,
|
||||
)
|
||||
assert u'Course: course_key: {}, version:'.format(self.course.id) in course_data.full_string()
|
||||
assert 'Course: course_key: {}, version:'.format(self.course.id) in course_data.full_string()
|
||||
|
||||
# full_string returns minimal value when structures aren't readily available.
|
||||
course_data = CourseData(self.user, course_key=self.course.id)
|
||||
assert u'empty course structure' in course_data.full_string()
|
||||
assert 'empty course structure' in course_data.full_string()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
from unittest.mock import patch
|
||||
|
||||
import ddt
|
||||
import six
|
||||
from crum import set_current_request
|
||||
from django.conf import settings
|
||||
from mock import patch
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangolib.testing.utils import get_mock_request
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
@@ -38,7 +38,7 @@ class ZeroGradeTest(GradeTestBase):
|
||||
chapter_grades = ZeroCourseGrade(self.request.user, course_data).chapter_grades
|
||||
for chapter in chapter_grades:
|
||||
for section in chapter_grades[chapter]['sections']:
|
||||
for score in six.itervalues(section.problem_scores):
|
||||
for score in section.problem_scores.values():
|
||||
assert score.earned == 0
|
||||
assert score.first_attempted is None
|
||||
assert section.all_total.earned == 0
|
||||
@@ -76,7 +76,7 @@ class TestScoreForModule(SharedModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestScoreForModule, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory.create()
|
||||
with cls.store.bulk_operations(cls.course.id):
|
||||
cls.a = ItemFactory.create(parent=cls.course, category="chapter", display_name="a")
|
||||
@@ -107,7 +107,7 @@ class TestScoreForModule(SharedModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(TestScoreForModule, cls).tearDownClass()
|
||||
super().tearDownClass()
|
||||
set_current_request(None)
|
||||
|
||||
def test_score_chapter(self):
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"""
|
||||
Tests for the CourseGradeFactory class.
|
||||
"""
|
||||
|
||||
|
||||
import itertools
|
||||
from unittest.mock import patch
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from mock import patch
|
||||
from six import text_type
|
||||
from edx_toggles.toggles.testutils import override_waffle_switch
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
|
||||
from openedx.core.djangoapps.content.block_structure.factory import BlockStructureFactory
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -87,7 +85,7 @@ class TestCourseGradeFactory(GradeTestBase):
|
||||
_assert_section_order(course_grade)
|
||||
|
||||
def _assert_grade_values(course_grade, expected_pass, expected_percent):
|
||||
assert course_grade.letter_grade == (u'Pass' if expected_pass else None)
|
||||
assert course_grade.letter_grade == ('Pass' if expected_pass else None)
|
||||
assert course_grade.percent == expected_percent
|
||||
|
||||
def _assert_section_order(course_grade):
|
||||
@@ -217,27 +215,27 @@ class TestCourseGradeFactory(GradeTestBase):
|
||||
'section_breakdown': [
|
||||
{
|
||||
'category': 'Homework',
|
||||
'detail': u'Homework 1 - Test Sequential X - 50% (1/2)',
|
||||
'label': u'HW 01',
|
||||
'detail': 'Homework 1 - Test Sequential X - 50% (1/2)',
|
||||
'label': 'HW 01',
|
||||
'percent': 0.5
|
||||
},
|
||||
{
|
||||
'category': 'Homework',
|
||||
'detail': u'Homework 2 - Test Sequential A - 0% (0/1)',
|
||||
'label': u'HW 02',
|
||||
'detail': 'Homework 2 - Test Sequential A - 0% (0/1)',
|
||||
'label': 'HW 02',
|
||||
'percent': 0.0
|
||||
},
|
||||
{
|
||||
'category': 'Homework',
|
||||
'detail': u'Homework Average = 25%',
|
||||
'label': u'HW Avg',
|
||||
'detail': 'Homework Average = 25%',
|
||||
'label': 'HW Avg',
|
||||
'percent': 0.25,
|
||||
'prominent': True
|
||||
},
|
||||
{
|
||||
'category': 'NoCredit',
|
||||
'detail': u'NoCredit Average = 0%',
|
||||
'label': u'NC Avg',
|
||||
'detail': 'NoCredit Average = 0%',
|
||||
'label': 'NC Avg',
|
||||
'percent': 0,
|
||||
'prominent': True
|
||||
},
|
||||
@@ -255,7 +253,7 @@ class TestGradeIteration(SharedModuleStoreTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestGradeIteration, cls).setUpClass()
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory.create(
|
||||
display_name=cls.COURSE_NAME,
|
||||
number=cls.COURSE_NUM
|
||||
@@ -265,7 +263,7 @@ class TestGradeIteration(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Create a course and a handful of users to assign grades
|
||||
"""
|
||||
super(TestGradeIteration, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
|
||||
self.students = [
|
||||
UserFactory.create(username='student1'),
|
||||
@@ -313,14 +311,14 @@ class TestGradeIteration(SharedModuleStoreTestCase):
|
||||
|
||||
student1, student2, student3, student4, student5 = self.students
|
||||
mock_course_grade.side_effect = [
|
||||
Exception(u"Error for {}.".format(student.username))
|
||||
Exception(f"Error for {student.username}.")
|
||||
if student.username in ['student3', 'student4']
|
||||
else mock_course_grade.return_value
|
||||
for student in self.students
|
||||
]
|
||||
with self.assertNumQueries(8):
|
||||
all_course_grades, all_errors = self._course_grades_and_errors_for(self.course, self.students)
|
||||
assert {student: text_type(all_errors[student]) for student in all_errors} == {
|
||||
assert {student: str(all_errors[student]) for student in all_errors} == {
|
||||
student3: 'Error for student3.',
|
||||
student4: 'Error for student4.'
|
||||
}
|
||||
|
||||
@@ -8,19 +8,20 @@ from base64 import b64encode
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from hashlib import sha1
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
import pytz
|
||||
import six
|
||||
from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from freezegun import freeze_time
|
||||
from mock import patch
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
|
||||
from lms.djangoapps.grades.constants import GradeOverrideFeatureEnum
|
||||
from lms.djangoapps.grades.models import (
|
||||
BLOCK_RECORD_LIST_VERSION,
|
||||
@@ -31,8 +32,6 @@ from lms.djangoapps.grades.models import (
|
||||
PersistentSubsectionGradeOverride,
|
||||
VisibleBlocks
|
||||
)
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
|
||||
|
||||
|
||||
class BlockRecordListTestCase(TestCase):
|
||||
@@ -41,7 +40,7 @@ class BlockRecordListTestCase(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(BlockRecordListTestCase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.course_key = CourseLocator(
|
||||
org='some_org',
|
||||
course='some_course',
|
||||
@@ -49,8 +48,8 @@ class BlockRecordListTestCase(TestCase):
|
||||
)
|
||||
|
||||
def test_empty_block_record_set(self):
|
||||
empty_json = u'{"blocks":[],"course_key":"%s","version":%s}' % (
|
||||
six.text_type(self.course_key),
|
||||
empty_json = '{{"blocks":[],"course_key":"{}","version":{}}}'.format(
|
||||
str(self.course_key),
|
||||
BLOCK_RECORD_LIST_VERSION,
|
||||
)
|
||||
|
||||
@@ -65,7 +64,7 @@ class GradesModelTestCase(TestCase):
|
||||
Base class for common setup of grades model tests.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(GradesModelTestCase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.course_key = CourseLocator(
|
||||
org='some_org',
|
||||
course='some_course',
|
||||
@@ -131,7 +130,7 @@ class VisibleBlocksTest(GradesModelTestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(VisibleBlocksTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.user_id = 12345
|
||||
|
||||
def _create_block_record_list(self, blocks, user_id=None):
|
||||
@@ -148,15 +147,15 @@ class VisibleBlocksTest(GradesModelTestCase):
|
||||
vblocks = self._create_block_record_list([self.record_a])
|
||||
list_of_block_dicts = [self.record_a._asdict()]
|
||||
for block_dict in list_of_block_dicts:
|
||||
block_dict['locator'] = six.text_type(block_dict['locator']) # BlockUsageLocator is not json-serializable
|
||||
block_dict['locator'] = str(block_dict['locator']) # BlockUsageLocator is not json-serializable
|
||||
expected_data = {
|
||||
'blocks': [{
|
||||
'locator': six.text_type(self.record_a.locator),
|
||||
'locator': str(self.record_a.locator),
|
||||
'raw_possible': 10,
|
||||
'weight': 1,
|
||||
'graded': self.record_a.graded,
|
||||
}],
|
||||
'course_key': six.text_type(self.record_a.locator.course_key),
|
||||
'course_key': str(self.record_a.locator.course_key),
|
||||
'version': BLOCK_RECORD_LIST_VERSION,
|
||||
}
|
||||
expected_json = json.dumps(expected_data, separators=(',', ':'), sort_keys=True)
|
||||
@@ -204,7 +203,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(PersistentSubsectionGradeTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.usage_key = BlockUsageLocator(
|
||||
course_key=self.course_key,
|
||||
block_type='subsection',
|
||||
@@ -329,21 +328,21 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
|
||||
was called with the expected info based on the passed grade.
|
||||
"""
|
||||
tracker_mock.emit.assert_called_with(
|
||||
u'edx.grades.subsection.grade_calculated',
|
||||
'edx.grades.subsection.grade_calculated',
|
||||
{
|
||||
'user_id': six.text_type(grade.user_id),
|
||||
'course_id': six.text_type(grade.course_id),
|
||||
'block_id': six.text_type(grade.usage_key),
|
||||
'course_version': six.text_type(grade.course_version),
|
||||
'user_id': str(grade.user_id),
|
||||
'course_id': str(grade.course_id),
|
||||
'block_id': str(grade.usage_key),
|
||||
'course_version': str(grade.course_version),
|
||||
'weighted_total_earned': grade.earned_all,
|
||||
'weighted_total_possible': grade.possible_all,
|
||||
'weighted_graded_earned': grade.earned_graded,
|
||||
'weighted_graded_possible': grade.possible_graded,
|
||||
'first_attempted': six.text_type(grade.first_attempted),
|
||||
'subtree_edited_timestamp': six.text_type(grade.subtree_edited_timestamp),
|
||||
'event_transaction_id': six.text_type(get_event_transaction_id()),
|
||||
'event_transaction_type': six.text_type(get_event_transaction_type()),
|
||||
'visible_blocks_hash': six.text_type(grade.visible_blocks_id),
|
||||
'first_attempted': str(grade.first_attempted),
|
||||
'subtree_edited_timestamp': str(grade.subtree_edited_timestamp),
|
||||
'event_transaction_id': str(get_event_transaction_id()),
|
||||
'event_transaction_type': str(get_event_transaction_type()),
|
||||
'visible_blocks_hash': str(grade.visible_blocks_id),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -355,7 +354,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(PersistentCourseGradesTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.params = {
|
||||
"user_id": 12345,
|
||||
"course_id": self.course_key,
|
||||
@@ -387,43 +386,43 @@ class PersistentCourseGradesTest(GradesModelTestCase):
|
||||
def test_passed_timestamp(self):
|
||||
# When the user has not passed, passed_timestamp is None
|
||||
self.params.update({
|
||||
u'percent_grade': 25.0,
|
||||
u'letter_grade': u'',
|
||||
u'passed': False,
|
||||
'percent_grade': 25.0,
|
||||
'letter_grade': '',
|
||||
'passed': False,
|
||||
})
|
||||
grade = PersistentCourseGrade.update_or_create(**self.params)
|
||||
assert grade.passed_timestamp is None
|
||||
|
||||
# After the user earns a passing grade, the passed_timestamp is set
|
||||
self.params.update({
|
||||
u'percent_grade': 75.0,
|
||||
u'letter_grade': u'C',
|
||||
u'passed': True,
|
||||
'percent_grade': 75.0,
|
||||
'letter_grade': 'C',
|
||||
'passed': True,
|
||||
})
|
||||
grade = PersistentCourseGrade.update_or_create(**self.params)
|
||||
passed_timestamp = grade.passed_timestamp
|
||||
assert grade.letter_grade == u'C'
|
||||
assert grade.letter_grade == 'C'
|
||||
assert isinstance(passed_timestamp, datetime)
|
||||
|
||||
# After the user improves their score, the new grade is reflected, but
|
||||
# the passed_timestamp remains the same.
|
||||
self.params.update({
|
||||
u'percent_grade': 95.0,
|
||||
u'letter_grade': u'A',
|
||||
u'passed': True,
|
||||
'percent_grade': 95.0,
|
||||
'letter_grade': 'A',
|
||||
'passed': True,
|
||||
})
|
||||
grade = PersistentCourseGrade.update_or_create(**self.params)
|
||||
assert grade.letter_grade == u'A'
|
||||
assert grade.letter_grade == 'A'
|
||||
assert grade.passed_timestamp == passed_timestamp
|
||||
|
||||
# If the grade later reverts to a failing grade, passed_timestamp remains the same.
|
||||
self.params.update({
|
||||
u'percent_grade': 20.0,
|
||||
u'letter_grade': u'',
|
||||
u'passed': False,
|
||||
'percent_grade': 20.0,
|
||||
'letter_grade': '',
|
||||
'passed': False,
|
||||
})
|
||||
grade = PersistentCourseGrade.update_or_create(**self.params)
|
||||
assert grade.letter_grade == u''
|
||||
assert grade.letter_grade == ''
|
||||
assert grade.passed_timestamp == passed_timestamp
|
||||
|
||||
def test_passed_timestamp_is_now(self):
|
||||
@@ -435,7 +434,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
|
||||
created_grade = PersistentCourseGrade.update_or_create(**self.params)
|
||||
read_grade = PersistentCourseGrade.read(self.params["user_id"], self.params["course_id"])
|
||||
for param in self.params:
|
||||
if param == u'passed':
|
||||
if param == 'passed':
|
||||
continue # passed/passed_timestamp takes special handling, and is tested separately
|
||||
assert self.params[param] == getattr(created_grade, param)
|
||||
assert isinstance(created_grade.passed_timestamp, datetime)
|
||||
@@ -476,16 +475,16 @@ class PersistentCourseGradesTest(GradesModelTestCase):
|
||||
was called with the expected info based on the passed grade.
|
||||
"""
|
||||
tracker_mock.emit.assert_called_with(
|
||||
u'edx.grades.course.grade_calculated',
|
||||
'edx.grades.course.grade_calculated',
|
||||
{
|
||||
'user_id': six.text_type(grade.user_id),
|
||||
'course_id': six.text_type(grade.course_id),
|
||||
'course_version': six.text_type(grade.course_version),
|
||||
'user_id': str(grade.user_id),
|
||||
'course_id': str(grade.course_id),
|
||||
'course_version': str(grade.course_version),
|
||||
'percent_grade': grade.percent_grade,
|
||||
'letter_grade': six.text_type(grade.letter_grade),
|
||||
'course_edited_timestamp': six.text_type(grade.course_edited_timestamp),
|
||||
'event_transaction_id': six.text_type(get_event_transaction_id()),
|
||||
'event_transaction_type': six.text_type(get_event_transaction_type()),
|
||||
'grading_policy_hash': six.text_type(grade.grading_policy_hash),
|
||||
'letter_grade': str(grade.letter_grade),
|
||||
'course_edited_timestamp': str(grade.course_edited_timestamp),
|
||||
'event_transaction_id': str(get_event_transaction_id()),
|
||||
'event_transaction_type': str(get_event_transaction_type()),
|
||||
'grading_policy_hash': str(grade.grading_policy_hash),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ def submission_value_repr(self):
|
||||
the "created_at" attribute that changes with each execution. Needed for
|
||||
consistency of ddt-generated test methods across pytest-xdist workers.
|
||||
"""
|
||||
return u'<SubmissionValue exists={}>'.format(self.exists)
|
||||
return f'<SubmissionValue exists={self.exists}>'
|
||||
|
||||
|
||||
def csm_value_repr(self):
|
||||
@@ -36,7 +36,7 @@ def csm_value_repr(self):
|
||||
the "created" attribute that changes with each execution. Needed for
|
||||
consistency of ddt-generated test methods across pytest-xdist workers.
|
||||
"""
|
||||
return u'<CSMValue exists={} raw_earned={}>'.format(self.exists, self.raw_earned)
|
||||
return f'<CSMValue exists={self.exists} raw_earned={self.raw_earned}>'
|
||||
|
||||
|
||||
def expected_result_repr(self):
|
||||
@@ -47,7 +47,7 @@ def expected_result_repr(self):
|
||||
"""
|
||||
included = ('raw_earned', 'raw_possible', 'weighted_earned', 'weighted_possible', 'weight', 'graded')
|
||||
attributes = ['{}={}'.format(name, getattr(self, name)) for name in included]
|
||||
return u'<ExpectedResult {}>'.format(' '.join(attributes))
|
||||
return '<ExpectedResult {}>'.format(' '.join(attributes))
|
||||
|
||||
|
||||
class TestScoredBlockTypes(TestCase):
|
||||
@@ -65,7 +65,7 @@ class TestScoredBlockTypes(TestCase):
|
||||
assert self.possibly_scored_block_types.issubset(scores._block_types_possibly_scored())
|
||||
|
||||
def test_possibly_scored(self):
|
||||
course_key = CourseLocator(u'org', u'course', u'run')
|
||||
course_key = CourseLocator('org', 'course', 'run')
|
||||
for block_type in self.possibly_scored_block_types:
|
||||
usage_key = BlockUsageLocator(course_key, block_type, 'mock_block_id')
|
||||
assert scores.possibly_scored(usage_key)
|
||||
@@ -77,7 +77,7 @@ class TestGetScore(TestCase):
|
||||
Tests for get_score
|
||||
"""
|
||||
display_name = 'test_name'
|
||||
course_key = CourseLocator(u'org', u'course', u'run')
|
||||
course_key = CourseLocator('org', 'course', 'run')
|
||||
location = BlockUsageLocator(course_key, 'problem', 'mock_block_id')
|
||||
|
||||
SubmissionValue = namedtuple('SubmissionValue', 'exists, points_earned, points_possible, created_at')
|
||||
@@ -298,7 +298,7 @@ class TestInternalGetScoreFromBlock(TestCase):
|
||||
"""
|
||||
Tests the internal helper method: _get_score_from_persisted_or_latest_block
|
||||
"""
|
||||
course_key = CourseLocator(u'org', u'course', u'run')
|
||||
course_key = CourseLocator('org', 'course', 'run')
|
||||
location = BlockUsageLocator(course_key, 'problem', 'mock_block_id')
|
||||
|
||||
def _create_block(self, raw_possible):
|
||||
|
||||
@@ -4,20 +4,16 @@ Grades Service Tests
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import ddt
|
||||
import pytz
|
||||
import six
|
||||
from freezegun import freeze_time
|
||||
from mock import call, patch
|
||||
|
||||
from lms.djangoapps.grades.constants import GradeOverrideFeatureEnum
|
||||
from lms.djangoapps.grades.models import (
|
||||
PersistentSubsectionGrade,
|
||||
PersistentSubsectionGradeOverride
|
||||
)
|
||||
from lms.djangoapps.grades.services import GradesService
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.grades.constants import GradeOverrideFeatureEnum
|
||||
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
|
||||
from lms.djangoapps.grades.services import GradesService
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
@@ -25,7 +21,7 @@ from ..config.waffle import REJECTED_EXAM_OVERRIDES_GRADE
|
||||
from ..constants import ScoreDatabaseTableEnum
|
||||
|
||||
|
||||
class MockWaffleFlag(object):
|
||||
class MockWaffleFlag:
|
||||
"""
|
||||
A Mock WaffleFlag object.
|
||||
"""
|
||||
@@ -44,7 +40,7 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(GradesServiceTests, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.service = GradesService()
|
||||
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course', run='Spring2019')
|
||||
self.subsection = ItemFactory.create(parent=self.course, category="subsection", display_name="Subsection")
|
||||
@@ -79,7 +75,7 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
super(GradesServiceTests, self).tearDown() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().tearDown()
|
||||
PersistentSubsectionGradeOverride.objects.all().delete() # clear out all previous overrides
|
||||
self.signal_patcher.stop()
|
||||
self.id_patcher.stop()
|
||||
@@ -111,8 +107,8 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
# test with id strings as parameters instead
|
||||
self.assertDictEqual(self.subsection_grade_to_dict(self.service.get_subsection_grade(
|
||||
user_id=self.user.id,
|
||||
course_key_or_id=six.text_type(self.course.id),
|
||||
usage_key_or_id=six.text_type(self.subsection.location)
|
||||
course_key_or_id=str(self.course.id),
|
||||
usage_key_or_id=str(self.subsection.location)
|
||||
)), {
|
||||
'earned_all': 6.0,
|
||||
'earned_graded': 5.0
|
||||
@@ -140,7 +136,7 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
# test with course key parameter as string instead
|
||||
self.assertDictEqual(self.subsection_grade_override_to_dict(self.service.get_subsection_grade_override(
|
||||
user_id=self.user.id,
|
||||
course_key_or_id=six.text_type(self.course.id),
|
||||
course_key_or_id=str(self.course.id),
|
||||
usage_key_or_id=self.subsection.location
|
||||
)), {
|
||||
'earned_all_override': override.earned_all_override,
|
||||
@@ -186,8 +182,8 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
assert self.mock_signal.call_args == call(
|
||||
sender=None,
|
||||
user_id=self.user.id,
|
||||
course_id=six.text_type(self.course.id),
|
||||
usage_id=six.text_type(self.subsection.location),
|
||||
course_id=str(self.course.id),
|
||||
usage_id=str(self.subsection.location),
|
||||
only_if_higher=False,
|
||||
modified=override_obj.modified,
|
||||
score_deleted=False,
|
||||
@@ -232,8 +228,8 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
assert self.mock_signal.call_args == call(
|
||||
sender=None,
|
||||
user_id=self.user.id,
|
||||
course_id=six.text_type(self.course.id),
|
||||
usage_id=six.text_type(self.subsection_without_grade.location),
|
||||
course_id=str(self.course.id),
|
||||
usage_id=str(self.subsection_without_grade.location),
|
||||
only_if_higher=False,
|
||||
modified=override_obj.modified,
|
||||
score_deleted=False,
|
||||
@@ -259,8 +255,8 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
assert self.mock_signal.call_args == call(
|
||||
sender=None,
|
||||
user_id=self.user.id,
|
||||
course_id=six.text_type(self.course.id),
|
||||
usage_id=six.text_type(self.subsection.location),
|
||||
course_id=str(self.course.id),
|
||||
usage_id=str(self.subsection.location),
|
||||
only_if_higher=False,
|
||||
modified=datetime.now().replace(tzinfo=pytz.UTC),
|
||||
score_deleted=True,
|
||||
|
||||
@@ -5,12 +5,12 @@ Tests for the score change signals defined in the courseware models module.
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
import pytz
|
||||
from django.test import TestCase
|
||||
from mock import MagicMock, patch
|
||||
from submissions.models import score_reset, score_set
|
||||
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
@@ -24,7 +24,7 @@ from ..signals.handlers import (
|
||||
)
|
||||
from ..signals.signals import PROBLEM_RAW_SCORE_CHANGED
|
||||
|
||||
UUID_REGEX = re.compile(u'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{12}' % {'hex': u'[0-9a-f]'})
|
||||
UUID_REGEX = re.compile('{hex}{{8}}-{hex}{{4}}-{hex}{{4}}-{hex}{{4}}-{hex}{{12}}'.format(hex='[0-9a-f]'))
|
||||
|
||||
FROZEN_NOW_DATETIME = datetime.now().replace(tzinfo=pytz.UTC)
|
||||
FROZEN_NOW_TIMESTAMP = to_timestamp(FROZEN_NOW_DATETIME)
|
||||
@@ -104,7 +104,7 @@ class ScoreChangedSignalRelayTest(TestCase):
|
||||
"""
|
||||
Configure mocks for all the dependencies of the render method
|
||||
"""
|
||||
super(ScoreChangedSignalRelayTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.signal_mock = self.setup_patch(
|
||||
'lms.djangoapps.grades.signals.signals.PROBLEM_WEIGHTED_SCORE_CHANGED.send',
|
||||
None,
|
||||
|
||||
@@ -3,13 +3,14 @@ Tests for the SubsectionGradeFactory class.
|
||||
"""
|
||||
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from mock import patch
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
||||
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
|
||||
from ..constants import GradeOverrideFeatureEnum
|
||||
from ..models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
|
||||
|
||||
@@ -7,18 +7,20 @@ import itertools
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
import pytz
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.db.utils import IntegrityError
|
||||
from django.utils import timezone
|
||||
from mock import MagicMock, patch
|
||||
from six.moves import range
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment, anonymous_id_for_user
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.djangoapps.track.event_transaction_utils import create_new_event_transaction_id, get_event_transaction_id
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
from lms.djangoapps.grades import tasks
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
|
||||
from lms.djangoapps.grades.config.waffle import ENFORCE_FREEZE_GRADE_AFTER_COURSE_END, waffle_flags
|
||||
@@ -35,10 +37,6 @@ from lms.djangoapps.grades.tasks import (
|
||||
recalculate_subsection_grade_v3
|
||||
)
|
||||
from openedx.core.djangoapps.content.block_structure.exceptions import BlockStructureNotFound
|
||||
from common.djangoapps.student.models import CourseEnrollment, anonymous_id_for_user
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.djangoapps.track.event_transaction_utils import create_new_event_transaction_id, get_event_transaction_id
|
||||
from common.djangoapps.util.date_utils import to_timestamp
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -47,7 +45,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec
|
||||
from .utils import mock_get_score
|
||||
|
||||
|
||||
class HasCourseWithProblemsMixin(object):
|
||||
class HasCourseWithProblemsMixin:
|
||||
"""
|
||||
Mixin to provide tests with a sample course with graded subsections
|
||||
"""
|
||||
@@ -80,8 +78,8 @@ class HasCourseWithProblemsMixin(object):
|
||||
('weighted_possible', 2.0),
|
||||
('user_id', self.user.id),
|
||||
('anonymous_user_id', 5),
|
||||
('course_id', six.text_type(self.course.id)),
|
||||
('usage_id', six.text_type(self.problem.location)),
|
||||
('course_id', str(self.course.id)),
|
||||
('usage_id', str(self.problem.location)),
|
||||
('only_if_higher', None),
|
||||
('modified', self.frozen_now_datetime),
|
||||
('score_db_table', ScoreDatabaseTableEnum.courseware_student_module),
|
||||
@@ -91,14 +89,14 @@ class HasCourseWithProblemsMixin(object):
|
||||
|
||||
self.recalculate_subsection_grade_kwargs = OrderedDict([
|
||||
('user_id', self.user.id),
|
||||
('course_id', six.text_type(self.course.id)),
|
||||
('usage_id', six.text_type(self.problem.location)),
|
||||
('course_id', str(self.course.id)),
|
||||
('usage_id', str(self.problem.location)),
|
||||
('anonymous_user_id', 5),
|
||||
('only_if_higher', None),
|
||||
('expected_modified_time', self.frozen_now_timestamp),
|
||||
('score_deleted', False),
|
||||
('event_transaction_id', six.text_type(get_event_transaction_id())),
|
||||
('event_transaction_type', u'edx.grades.problem.submitted'),
|
||||
('event_transaction_id', str(get_event_transaction_id())),
|
||||
('event_transaction_type', 'edx.grades.problem.submitted'),
|
||||
('score_db_table', ScoreDatabaseTableEnum.courseware_student_module),
|
||||
])
|
||||
|
||||
@@ -116,7 +114,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
|
||||
ENABLED_SIGNALS = ['course_published', 'pre_publish']
|
||||
|
||||
def setUp(self):
|
||||
super(RecalculateSubsectionGradeTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.user = UserFactory()
|
||||
PersistentGradesEnabledFlag.objects.create(enabled_for_all_courses=True, enabled=True)
|
||||
|
||||
@@ -136,7 +134,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
|
||||
self.set_up_course()
|
||||
send_args = self.problem_weighted_score_changed_kwargs
|
||||
local_task_args = self.recalculate_subsection_grade_kwargs.copy()
|
||||
local_task_args['event_transaction_type'] = u'edx.grades.problem.submitted'
|
||||
local_task_args['event_transaction_type'] = 'edx.grades.problem.submitted'
|
||||
local_task_args['force_update_subsections'] = False
|
||||
with self.mock_csm_get_score() and patch(
|
||||
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.apply_async',
|
||||
@@ -304,7 +302,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
|
||||
recalculate_subsection_grade_v3.apply(kwargs=self.recalculate_subsection_grade_kwargs)
|
||||
|
||||
self._assert_retry_called(mock_retry)
|
||||
assert u'Grades: tasks._has_database_updated_with_new_score is False.' in mock_log.info.call_args_list[0][0][0]
|
||||
assert 'Grades: tasks._has_database_updated_with_new_score is False.' in mock_log.info.call_args_list[0][0][0]
|
||||
|
||||
@ddt.data(
|
||||
*itertools.product(
|
||||
@@ -338,7 +336,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
|
||||
self._assert_retry_not_called(mock_retry)
|
||||
else:
|
||||
self._assert_retry_called(mock_retry)
|
||||
assert u'Grades: tasks._has_database_updated_with_new_score is False.'\
|
||||
assert 'Grades: tasks._has_database_updated_with_new_score is False.'\
|
||||
in mock_log.info.call_args_list[0][0][0]
|
||||
|
||||
@patch('lms.djangoapps.grades.tasks.log')
|
||||
@@ -407,7 +405,7 @@ class ComputeGradesForCourseTest(HasCourseWithProblemsMixin, ModuleStoreTestCase
|
||||
ENABLED_SIGNALS = ['course_published', 'pre_publish']
|
||||
|
||||
def setUp(self):
|
||||
super(ComputeGradesForCourseTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.users = [UserFactory.create() for _ in range(12)]
|
||||
self.set_up_course()
|
||||
for user in self.users:
|
||||
@@ -417,7 +415,7 @@ class ComputeGradesForCourseTest(HasCourseWithProblemsMixin, ModuleStoreTestCase
|
||||
def test_behavior(self, batch_size):
|
||||
with mock_get_score(1, 2):
|
||||
result = compute_grades_for_course_v2.delay(
|
||||
course_key=six.text_type(self.course.id),
|
||||
course_key=str(self.course.id),
|
||||
batch_size=batch_size,
|
||||
offset=4,
|
||||
)
|
||||
@@ -431,7 +429,7 @@ class ComputeGradesForCourseTest(HasCourseWithProblemsMixin, ModuleStoreTestCase
|
||||
for course_key, offset, batch_size in _course_task_args(
|
||||
batch_size=test_batch_size, course_key=self.course.id, from_settings=False
|
||||
):
|
||||
assert course_key == six.text_type(self.course.id)
|
||||
assert course_key == str(self.course.id)
|
||||
assert batch_size == test_batch_size
|
||||
assert offset == offset_expected
|
||||
offset_expected += test_batch_size
|
||||
@@ -442,7 +440,7 @@ class RecalculateGradesForUserTest(HasCourseWithProblemsMixin, ModuleStoreTestCa
|
||||
Test recalculate_course_and_subsection_grades_for_user task.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(RecalculateGradesForUserTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.set_up_course()
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
@@ -454,7 +452,7 @@ class RecalculateGradesForUserTest(HasCourseWithProblemsMixin, ModuleStoreTestCa
|
||||
|
||||
kwargs = {
|
||||
'user_id': self.user.id,
|
||||
'course_key': six.text_type(self.course.id),
|
||||
'course_key': str(self.course.id),
|
||||
}
|
||||
|
||||
task_result = tasks.recalculate_course_and_subsection_grades_for_user.apply_async(kwargs=kwargs)
|
||||
@@ -474,7 +472,7 @@ class RecalculateGradesForUserTest(HasCourseWithProblemsMixin, ModuleStoreTestCa
|
||||
|
||||
kwargs = {
|
||||
'user_id': self.user.id,
|
||||
'course_key': six.text_type(self.course.id),
|
||||
'course_key': str(self.course.id),
|
||||
}
|
||||
|
||||
task_result = tasks.recalculate_course_and_subsection_grades_for_user.apply_async(kwargs=kwargs)
|
||||
@@ -490,14 +488,14 @@ class FreezeGradingAfterCourseEndTest(HasCourseWithProblemsMixin, ModuleStoreTes
|
||||
Test enforce_freeze_grade_after_course_end waffle flag controlling grading tasks.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(FreezeGradingAfterCourseEndTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().setUp()
|
||||
self.users = [UserFactory.create() for _ in range(12)]
|
||||
self.user = self.users[0]
|
||||
self.freeze_grade_flag = waffle_flags()[ENFORCE_FREEZE_GRADE_AFTER_COURSE_END]
|
||||
|
||||
def _assert_log(self, mock_log, method_name):
|
||||
assert mock_log.info.called
|
||||
log_message = u"Attempted {} for course '%s', but grades are frozen.".format(method_name)
|
||||
log_message = "Attempted {} for course '%s', but grades are frozen.".format(method_name)
|
||||
assert log_message in mock_log.info.call_args_list[0][0][0]
|
||||
|
||||
def _assert_for_freeze_grade_flag( # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
@@ -536,7 +534,7 @@ class FreezeGradingAfterCourseEndTest(HasCourseWithProblemsMixin, ModuleStoreTes
|
||||
) as mock_compute_grades:
|
||||
result = compute_all_grades_for_course.apply_async(
|
||||
kwargs={
|
||||
'course_key': six.text_type(self.course.id)
|
||||
'course_key': str(self.course.id)
|
||||
}
|
||||
)
|
||||
self._assert_for_freeze_grade_flag(
|
||||
@@ -567,7 +565,7 @@ class FreezeGradingAfterCourseEndTest(HasCourseWithProblemsMixin, ModuleStoreTes
|
||||
with mock_get_score(1, 2):
|
||||
result = compute_grades_for_course.apply_async(
|
||||
kwargs={
|
||||
'course_key': six.text_type(self.course.id),
|
||||
'course_key': str(self.course.id),
|
||||
'batch_size': 2,
|
||||
'offset': 4,
|
||||
}
|
||||
@@ -597,7 +595,7 @@ class FreezeGradingAfterCourseEndTest(HasCourseWithProblemsMixin, ModuleStoreTes
|
||||
factory = mock_factory.return_value
|
||||
kwargs = {
|
||||
'user_id': self.user.id,
|
||||
'course_key': six.text_type(self.course.id),
|
||||
'course_key': str(self.course.id),
|
||||
}
|
||||
|
||||
result = tasks.recalculate_course_and_subsection_grades_for_user.apply_async(kwargs=kwargs)
|
||||
|
||||
@@ -9,13 +9,11 @@ from copy import deepcopy
|
||||
|
||||
import ddt
|
||||
import pytz
|
||||
import six
|
||||
from six.moves import range
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.course_blocks.transformers.tests.helpers import CourseStructureTestCase
|
||||
from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -35,15 +33,15 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
problem_metadata = {
|
||||
u'graded': True,
|
||||
u'weight': 1,
|
||||
u'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc),
|
||||
'graded': True,
|
||||
'weight': 1,
|
||||
'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc),
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(GradesTransformerTestCase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
password = u'test'
|
||||
self.student = UserFactory.create(is_staff=False, username=u'test_student', password=password)
|
||||
super().setUp()
|
||||
password = 'test'
|
||||
self.student = UserFactory.create(is_staff=False, username='test_student', password=password)
|
||||
self.client.login(username=self.student.username, password=password)
|
||||
|
||||
def _update_course_grading_policy(self, course, grading_policy):
|
||||
@@ -78,8 +76,8 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
# Append our custom message to the default assertEqual error message
|
||||
self.longMessage = True # pylint: disable=invalid-name
|
||||
assert expectations[field] == block_structure.get_xblock_field(usage_key, field),\
|
||||
u'in field {},'.format(repr(field))
|
||||
assert block_structure.get_xblock_field(usage_key, u'subtree_edited_on') is not None
|
||||
'in field {},'.format(repr(field))
|
||||
assert block_structure.get_xblock_field(usage_key, 'subtree_edited_on') is not None
|
||||
|
||||
def assert_collected_transformer_block_fields(self, block_structure, usage_key, transformer_class, **expectations):
|
||||
"""
|
||||
@@ -94,7 +92,7 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
for field in expectations:
|
||||
assert expectations[field] == block_structure.get_transformer_block_field(
|
||||
usage_key, transformer_class, field
|
||||
), u'in {} and field {}'.format(transformer_class, repr(field))
|
||||
), 'in {} and field {}'.format(transformer_class, repr(field))
|
||||
|
||||
def build_course_with_problems(self, data='<problem></problem>', metadata=None):
|
||||
"""
|
||||
@@ -109,18 +107,18 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
# `CourseStructureTestCase.build_course` for details.
|
||||
return self.build_course([
|
||||
{
|
||||
u'org': u'GradesTestOrg',
|
||||
u'course': u'GB101',
|
||||
u'run': u'cannonball',
|
||||
u'metadata': {u'format': u'homework'},
|
||||
u'#type': u'course',
|
||||
u'#ref': u'course',
|
||||
u'#children': [
|
||||
'org': 'GradesTestOrg',
|
||||
'course': 'GB101',
|
||||
'run': 'cannonball',
|
||||
'metadata': {'format': 'homework'},
|
||||
'#type': 'course',
|
||||
'#ref': 'course',
|
||||
'#children': [
|
||||
{
|
||||
u'metadata': metadata,
|
||||
u'#type': u'problem',
|
||||
u'#ref': u'problem',
|
||||
u'data': data,
|
||||
'metadata': metadata,
|
||||
'#type': 'problem',
|
||||
'#ref': 'problem',
|
||||
'data': data,
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -146,45 +144,45 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
"""
|
||||
return self.build_course([
|
||||
{
|
||||
u'org': u'GradesTestOrg',
|
||||
u'course': u'GB101',
|
||||
u'run': u'cannonball',
|
||||
u'metadata': {u'format': u'homework'},
|
||||
u'#type': u'course',
|
||||
u'#ref': u'course',
|
||||
u'#children': [
|
||||
'org': 'GradesTestOrg',
|
||||
'course': 'GB101',
|
||||
'run': 'cannonball',
|
||||
'metadata': {'format': 'homework'},
|
||||
'#type': 'course',
|
||||
'#ref': 'course',
|
||||
'#children': [
|
||||
{
|
||||
u'#type': u'chapter',
|
||||
u'#ref': u'chapter',
|
||||
u'#children': [
|
||||
'#type': 'chapter',
|
||||
'#ref': 'chapter',
|
||||
'#children': [
|
||||
{
|
||||
u'#type': u'sequential',
|
||||
u'#ref': 'sub_A',
|
||||
u'#children': [
|
||||
'#type': 'sequential',
|
||||
'#ref': 'sub_A',
|
||||
'#children': [
|
||||
{
|
||||
u'#type': u'vertical',
|
||||
u'#ref': 'vert_1',
|
||||
u'#children': [
|
||||
'#type': 'vertical',
|
||||
'#ref': 'vert_1',
|
||||
'#children': [
|
||||
{
|
||||
u'#type': u'vertical',
|
||||
u'#ref': u'vert_A11',
|
||||
u'#children': [{u'#type': u'problem', u'#ref': u'prob_A1aa'}]
|
||||
'#type': 'vertical',
|
||||
'#ref': 'vert_A11',
|
||||
'#children': [{'#type': 'problem', '#ref': 'prob_A1aa'}]
|
||||
},
|
||||
]
|
||||
},
|
||||
{u'#type': u'vertical', u'#ref': 'vert_2', '#parents': [u'vert_A11']},
|
||||
{'#type': 'vertical', '#ref': 'vert_2', '#parents': ['vert_A11']},
|
||||
]
|
||||
},
|
||||
{
|
||||
u'#type': u'sequential',
|
||||
u'#ref': u'sub_B',
|
||||
u'#children': [
|
||||
{u'#type': u'vertical', u'#ref': 'vert_3', '#parents': ['sub_A']},
|
||||
'#type': 'sequential',
|
||||
'#ref': 'sub_B',
|
||||
'#children': [
|
||||
{'#type': 'vertical', '#ref': 'vert_3', '#parents': ['sub_A']},
|
||||
{
|
||||
u'#type': u'sequential',
|
||||
u'#ref': 'sub_C',
|
||||
'#type': 'sequential',
|
||||
'#ref': 'sub_C',
|
||||
'#parents': ['sub_A'],
|
||||
u'#children': [{u'#type': u'problem', u'#ref': u'prob_BCb'}]
|
||||
'#children': [{'#type': 'problem', '#ref': 'prob_BCb'}]
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -209,8 +207,8 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
'prob_BCb': {'sub_A', 'sub_B', 'sub_C'},
|
||||
}
|
||||
blocks = self.build_complicated_hypothetical_course()
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
for block_ref, expected_subsections in six.iteritems(expected_subsections):
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
for block_ref, expected_subsections in expected_subsections.items():
|
||||
actual_subsections = block_structure.get_transformer_block_field(
|
||||
blocks[block_ref].location,
|
||||
self.TRANSFORMER_CLASS_TO_TEST,
|
||||
@@ -220,19 +218,19 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
|
||||
def test_unscored_block_collection(self):
|
||||
blocks = self.build_course_with_problems()
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
self.assert_collected_xblock_fields(
|
||||
block_structure,
|
||||
blocks[u'course'].location,
|
||||
blocks['course'].location,
|
||||
weight=None,
|
||||
graded=False,
|
||||
has_score=False,
|
||||
due=None,
|
||||
format=u'homework',
|
||||
format='homework',
|
||||
)
|
||||
self.assert_collected_transformer_block_fields(
|
||||
block_structure,
|
||||
blocks[u'course'].location,
|
||||
blocks['course'].location,
|
||||
self.TRANSFORMER_CLASS_TO_TEST,
|
||||
max_score=None,
|
||||
explicit_graded=None,
|
||||
@@ -241,20 +239,20 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
def test_grades_collected_basic(self):
|
||||
|
||||
blocks = self.build_course_with_problems()
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
|
||||
self.assert_collected_xblock_fields(
|
||||
block_structure,
|
||||
blocks[u'problem'].location,
|
||||
weight=self.problem_metadata[u'weight'],
|
||||
graded=self.problem_metadata[u'graded'],
|
||||
blocks['problem'].location,
|
||||
weight=self.problem_metadata['weight'],
|
||||
graded=self.problem_metadata['graded'],
|
||||
has_score=True,
|
||||
due=self.problem_metadata[u'due'],
|
||||
due=self.problem_metadata['due'],
|
||||
format=None,
|
||||
)
|
||||
self.assert_collected_transformer_block_fields(
|
||||
block_structure,
|
||||
blocks[u'problem'].location,
|
||||
blocks['problem'].location,
|
||||
self.TRANSFORMER_CLASS_TO_TEST,
|
||||
max_score=0,
|
||||
explicit_graded=True,
|
||||
@@ -263,15 +261,15 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
@ddt.data(True, False, None)
|
||||
def test_graded_at_problem(self, graded):
|
||||
problem_metadata = {
|
||||
u'has_score': True,
|
||||
'has_score': True,
|
||||
}
|
||||
if graded is not None:
|
||||
problem_metadata[u'graded'] = graded
|
||||
problem_metadata['graded'] = graded
|
||||
blocks = self.build_course_with_problems(metadata=problem_metadata)
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
self.assert_collected_transformer_block_fields(
|
||||
block_structure,
|
||||
blocks[u'problem'].location,
|
||||
blocks['problem'].location,
|
||||
self.TRANSFORMER_CLASS_TO_TEST,
|
||||
explicit_graded=graded,
|
||||
)
|
||||
@@ -280,27 +278,27 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
# Demonstrate that the problem data can by collected by the SystemUser
|
||||
# even if the block has access restrictions placed on it.
|
||||
problem_metadata = {
|
||||
u'graded': True,
|
||||
u'weight': 1,
|
||||
u'due': datetime.datetime(2016, 10, 16, 0, 4, 0, tzinfo=pytz.utc),
|
||||
u'visible_to_staff_only': True,
|
||||
'graded': True,
|
||||
'weight': 1,
|
||||
'due': datetime.datetime(2016, 10, 16, 0, 4, 0, tzinfo=pytz.utc),
|
||||
'visible_to_staff_only': True,
|
||||
}
|
||||
|
||||
blocks = self.build_course_with_problems(metadata=problem_metadata)
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
|
||||
self.assert_collected_xblock_fields(
|
||||
block_structure,
|
||||
blocks[u'problem'].location,
|
||||
weight=problem_metadata[u'weight'],
|
||||
graded=problem_metadata[u'graded'],
|
||||
blocks['problem'].location,
|
||||
weight=problem_metadata['weight'],
|
||||
graded=problem_metadata['graded'],
|
||||
has_score=True,
|
||||
due=problem_metadata[u'due'],
|
||||
due=problem_metadata['due'],
|
||||
format=None,
|
||||
)
|
||||
|
||||
def test_max_score_collection(self):
|
||||
problem_data = u'''
|
||||
problem_data = '''
|
||||
<problem>
|
||||
<numericalresponse answer="2">
|
||||
<textline label="1+1" trailing_text="%" />
|
||||
@@ -309,17 +307,17 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
'''
|
||||
|
||||
blocks = self.build_course_with_problems(data=problem_data)
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
|
||||
self.assert_collected_transformer_block_fields(
|
||||
block_structure,
|
||||
blocks[u'problem'].location,
|
||||
blocks['problem'].location,
|
||||
self.TRANSFORMER_CLASS_TO_TEST,
|
||||
max_score=1,
|
||||
)
|
||||
|
||||
def test_max_score_for_multiresponse_problem(self):
|
||||
problem_data = u'''
|
||||
problem_data = '''
|
||||
<problem>
|
||||
<numericalresponse answer="27">
|
||||
<textline label="3^3" />
|
||||
@@ -331,11 +329,11 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
'''
|
||||
|
||||
blocks = self.build_course_with_problems(problem_data)
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
|
||||
self.assert_collected_transformer_block_fields(
|
||||
block_structure,
|
||||
blocks[u'problem'].location,
|
||||
blocks['problem'].location,
|
||||
self.TRANSFORMER_CLASS_TO_TEST,
|
||||
max_score=2,
|
||||
)
|
||||
@@ -344,7 +342,7 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
"""
|
||||
Verify that for an invalid dropdown problem, the max score is set to zero.
|
||||
"""
|
||||
problem_data = u'''
|
||||
problem_data = '''
|
||||
<problem>
|
||||
<optionresponse>
|
||||
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown
|
||||
@@ -372,25 +370,25 @@ class GradesTransformerTestCase(CourseStructureTestCase):
|
||||
|
||||
def test_course_version_not_collected_in_old_mongo(self):
|
||||
blocks = self.build_course_with_problems()
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
assert block_structure.get_xblock_field(blocks[u'course'].location, u'course_version') is None
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
assert block_structure.get_xblock_field(blocks['course'].location, 'course_version') is None
|
||||
|
||||
def test_course_version_collected_in_split(self):
|
||||
with self.store.default_store(ModuleStoreEnum.Type.split):
|
||||
blocks = self.build_course_with_problems()
|
||||
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
assert block_structure.get_xblock_field(blocks[u'course'].location, u'course_version') is not None
|
||||
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
assert block_structure.get_xblock_field(blocks['course'].location, 'course_version') is not None
|
||||
assert block_structure.get_xblock_field(
|
||||
blocks[u'problem'].location, u'course_version'
|
||||
) == block_structure.get_xblock_field(blocks[u'course'].location, u'course_version')
|
||||
blocks['problem'].location, 'course_version'
|
||||
) == block_structure.get_xblock_field(blocks['course'].location, 'course_version')
|
||||
|
||||
def test_grading_policy_collected(self):
|
||||
# the calculated hash of the original and updated grading policies of the test course
|
||||
original_grading_policy_hash = u'ChVp0lHGQGCevD0t4njna/C44zQ='
|
||||
updated_grading_policy_hash = u'TsbX04qWOy1WRnC0NHy+94upPd4='
|
||||
original_grading_policy_hash = 'ChVp0lHGQGCevD0t4njna/C44zQ='
|
||||
updated_grading_policy_hash = 'TsbX04qWOy1WRnC0NHy+94upPd4='
|
||||
|
||||
blocks = self.build_course_with_problems()
|
||||
course_block = blocks[u'course']
|
||||
course_block = blocks['course']
|
||||
self._validate_grading_policy_hash(
|
||||
course_block.location,
|
||||
original_grading_policy_hash
|
||||
@@ -426,9 +424,9 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
|
||||
TRANSFORMER_CLASS_TO_TEST = GradesTransformer
|
||||
|
||||
def setUp(self):
|
||||
super(MultiProblemModulestoreAccessTestCase, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
password = u'test'
|
||||
self.student = UserFactory.create(is_staff=False, username=u'test_student', password=password)
|
||||
super().setUp()
|
||||
password = 'test'
|
||||
self.student = UserFactory.create(is_staff=False, username='test_student', password=password)
|
||||
self.client.login(username=self.student.username, password=password)
|
||||
|
||||
@ddt.data(
|
||||
@@ -443,26 +441,26 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
|
||||
"""
|
||||
course = [
|
||||
{
|
||||
u'org': u'GradesTestOrg',
|
||||
u'course': u'GB101',
|
||||
u'run': u'cannonball',
|
||||
u'metadata': {u'format': u'homework'},
|
||||
u'#type': u'course',
|
||||
u'#ref': u'course',
|
||||
u'#children': [],
|
||||
'org': 'GradesTestOrg',
|
||||
'course': 'GB101',
|
||||
'run': 'cannonball',
|
||||
'metadata': {'format': 'homework'},
|
||||
'#type': 'course',
|
||||
'#ref': 'course',
|
||||
'#children': [],
|
||||
},
|
||||
]
|
||||
for problem_number in range(random.randrange(10, 20)):
|
||||
course[0][u'#children'].append(
|
||||
course[0]['#children'].append(
|
||||
{
|
||||
u'metadata': {
|
||||
u'graded': True,
|
||||
u'weight': 1,
|
||||
u'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc),
|
||||
'metadata': {
|
||||
'graded': True,
|
||||
'weight': 1,
|
||||
'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc),
|
||||
},
|
||||
u'#type': u'problem',
|
||||
u'#ref': u'problem_{}'.format(problem_number),
|
||||
u'data': u'''
|
||||
'#type': 'problem',
|
||||
'#ref': f'problem_{problem_number}',
|
||||
'data': '''
|
||||
<problem>
|
||||
<numericalresponse answer="{number}">
|
||||
<textline label="1*{number}" />
|
||||
@@ -472,6 +470,6 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
|
||||
)
|
||||
with self.store.default_store(store_type):
|
||||
blocks = self.build_course(course)
|
||||
clear_course_from_cache(blocks[u'course'].id)
|
||||
clear_course_from_cache(blocks['course'].id)
|
||||
with check_mongo_calls(expected_mongo_queries):
|
||||
get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
|
||||
get_course_blocks(self.student, blocks['course'].location, self.transformers)
|
||||
|
||||
@@ -5,9 +5,9 @@ Utilities for grades related tests
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytz
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from lms.djangoapps.courseware.model_data import FieldDataCache
|
||||
from lms.djangoapps.courseware.module_render import get_module
|
||||
|
||||
@@ -40,14 +40,14 @@ class GradesTransformer(BlockStructureTransformer):
|
||||
WRITE_VERSION = 4
|
||||
READ_VERSION = 4
|
||||
FIELDS_TO_COLLECT = [
|
||||
u'due',
|
||||
u'format',
|
||||
u'graded',
|
||||
u'has_score',
|
||||
u'weight',
|
||||
u'course_version',
|
||||
u'subtree_edited_on',
|
||||
u'show_correctness',
|
||||
'due',
|
||||
'format',
|
||||
'graded',
|
||||
'has_score',
|
||||
'weight',
|
||||
'course_version',
|
||||
'subtree_edited_on',
|
||||
'show_correctness',
|
||||
]
|
||||
|
||||
EXPLICIT_GRADED_FIELD_NAME = 'explicit_graded'
|
||||
@@ -58,7 +58,7 @@ class GradesTransformer(BlockStructureTransformer):
|
||||
Unique identifier for the transformer's class;
|
||||
same identifier used in setup.py.
|
||||
"""
|
||||
return u'grades'
|
||||
return 'grades'
|
||||
|
||||
@classmethod
|
||||
def collect(cls, block_structure):
|
||||
@@ -152,7 +152,7 @@ class GradesTransformer(BlockStructureTransformer):
|
||||
max_score = module.max_score()
|
||||
block_structure.set_transformer_block_field(module.location, cls, 'max_score', max_score)
|
||||
if max_score is None:
|
||||
log.warning(u"GradesTransformer: max_score is None for {}".format(module.location))
|
||||
log.warning(f"GradesTransformer: max_score is None for {module.location}")
|
||||
|
||||
@classmethod
|
||||
def _collect_grading_policy_hash(cls, block_structure):
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
from . import grade_utils
|
||||
|
||||
|
||||
class GradesUtilService(object):
|
||||
class GradesUtilService:
|
||||
"""
|
||||
An interface to be used by xblocks.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super(GradesUtilService, self).__init__() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
super().__init__()
|
||||
self.course_id = kwargs.get('course_id', None)
|
||||
|
||||
def are_grades_frozen(self):
|
||||
|
||||
Reference in New Issue
Block a user