Merge pull request #25709 from edx/asheehan-edx/ENT-3648-assessment-level-signals
Adding signals and receivers for assessment level reporting
This commit is contained in:
@@ -6,9 +6,11 @@ SubsectionGrade Factory Class
|
||||
from collections import OrderedDict
|
||||
from logging import getLogger
|
||||
|
||||
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 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
|
||||
@@ -104,6 +106,14 @@ class SubsectionGradeFactory(object):
|
||||
)
|
||||
self._update_saved_subsection_grade(subsection.location, grade_model)
|
||||
|
||||
if settings.FEATURES.get('ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL'):
|
||||
COURSE_ASSESSMENT_GRADE_CHANGED.send_robust(
|
||||
sender=self,
|
||||
user=self.student,
|
||||
subsection_id=calculated_grade.location,
|
||||
subsection_grade=calculated_grade.graded_total.earned
|
||||
)
|
||||
|
||||
return calculated_grade
|
||||
|
||||
@lazy
|
||||
|
||||
@@ -49,14 +49,19 @@ class TestSubsectionGradeFactory(ProblemSubmissionTestMixin, GradeTestBase):
|
||||
self.assertIsInstance(grade, ZeroSubsectionGrade)
|
||||
self.assert_grade(grade, 0.0, 1.0)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL': True})
|
||||
def test_update(self):
|
||||
"""
|
||||
Assuming the underlying score reporting methods work,
|
||||
test that the score is calculated properly.
|
||||
"""
|
||||
with mock_get_score(1, 2):
|
||||
grade = self.subsection_grade_factory.update(self.sequence)
|
||||
with patch(
|
||||
'openedx.core.djangoapps.signals.signals.COURSE_ASSESSMENT_GRADE_CHANGED.send_robust'
|
||||
) as mock_update_grades_signal:
|
||||
grade = self.subsection_grade_factory.update(self.sequence)
|
||||
self.assert_grade(grade, 1, 2)
|
||||
assert mock_update_grades_signal.called
|
||||
|
||||
def test_write_only_if_engaged(self):
|
||||
"""
|
||||
|
||||
@@ -692,6 +692,18 @@ FEATURES = {
|
||||
# .. toggle_tickets: https://openedx.atlassian.net/browse/TNL-7273
|
||||
# .. toggle_warnings: This temporary feature toggle does not have a target removal date.
|
||||
'ENABLE_ORA_USERNAMES_ON_DATA_EXPORT': False,
|
||||
|
||||
# .. toggle_name: ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL
|
||||
# .. toggle_implementation: DjangoSetting
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Set to True to start sending signals for assessment level grade updates. Notably, the only
|
||||
# handler of this signal at the time of this writing sends assessment updates to enterprise integrated channels.
|
||||
# .. toggle_use_cases: temporary
|
||||
# .. toggle_creation_date: 2020-12-09
|
||||
# .. toggle_target_removal_date: 2021-02-01
|
||||
# .. toggle_tickets: https://openedx.atlassian.net/browse/ENT-3818
|
||||
# .. toggle_warnings: None.
|
||||
'ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL': False,
|
||||
}
|
||||
|
||||
# Specifies extra XBlock fields that should available when requested via the Course Blocks API
|
||||
|
||||
@@ -17,6 +17,15 @@ COURSE_CERT_REVOKED = Signal(providing_args=["user", "course_key", "mode", "stat
|
||||
COURSE_CERT_DATE_CHANGE = Signal(providing_args=["course_key"])
|
||||
|
||||
|
||||
COURSE_ASSESSMENT_GRADE_CHANGED = Signal(
|
||||
providing_args=[
|
||||
'user',
|
||||
'course_id',
|
||||
'subsection_id',
|
||||
'subsection_grade',
|
||||
]
|
||||
)
|
||||
|
||||
# Signal that indicates that a user has passed a course.
|
||||
COURSE_GRADE_NOW_PASSED = Signal(
|
||||
providing_args=[
|
||||
|
||||
@@ -11,12 +11,12 @@ from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
from enterprise.models import EnterpriseCourseEnrollment, EnterpriseCustomer, EnterpriseCustomerUser
|
||||
from integrated_channels.integrated_channel.tasks import transmit_single_learner_data
|
||||
from integrated_channels.integrated_channel.tasks import transmit_single_learner_data, transmit_subsection_learner_data
|
||||
from slumber.exceptions import HttpClientError
|
||||
|
||||
from lms.djangoapps.email_marketing.tasks import update_user
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED
|
||||
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED, COURSE_ASSESSMENT_GRADE_CHANGED
|
||||
from openedx.features.enterprise_support.api import enterprise_enabled
|
||||
from openedx.features.enterprise_support.tasks import clear_enterprise_customer_data_consent_share_cache
|
||||
from openedx.features.enterprise_support.utils import clear_data_consent_share_cache, is_enterprise_learner
|
||||
@@ -86,6 +86,22 @@ def handle_enterprise_learner_passing_grade(sender, user, course_id, **kwargs):
|
||||
transmit_single_learner_data.apply_async(kwargs=kwargs)
|
||||
|
||||
|
||||
@receiver(COURSE_ASSESSMENT_GRADE_CHANGED)
|
||||
def handle_enterprise_learner_subsection(sender, user, course_id, subsection_id, subsection_grade, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Listen for an enterprise learner completing a subsection, transmit data to relevant integrated channel.
|
||||
"""
|
||||
if enterprise_enabled() and is_enterprise_learner(user):
|
||||
kwargs = {
|
||||
'username': str(user.username),
|
||||
'course_run_id': str(course_id),
|
||||
'subsection_id': str(subsection_id),
|
||||
'grade': str(subsection_grade),
|
||||
}
|
||||
|
||||
transmit_subsection_learner_data.apply_async(kwargs=kwargs)
|
||||
|
||||
|
||||
@receiver(UNENROLL_DONE)
|
||||
def refund_order_voucher(sender, course_enrollment, skip_refund=False, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
|
||||
@@ -17,7 +17,7 @@ from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.certificates.signals import listen_for_passing_grade
|
||||
from openedx.core.djangoapps.commerce.utils import ECOMMERCE_DATE_FORMAT
|
||||
from openedx.core.djangoapps.credit.tests.test_api import TEST_ECOMMERCE_WORKER
|
||||
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED
|
||||
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED, COURSE_ASSESSMENT_GRADE_CHANGED
|
||||
from openedx.features.enterprise_support.tests import FEATURES_WITH_ENTERPRISE_ENABLED
|
||||
from openedx.features.enterprise_support.tests.factories import (
|
||||
EnterpriseCourseEnrollmentFactory,
|
||||
@@ -207,3 +207,39 @@ class EnterpriseSupportSignals(SharedModuleStoreTestCase):
|
||||
COURSE_GRADE_NOW_PASSED.send(sender=None, user=self.user, course_id=course_key)
|
||||
mock_task_apply.assert_called_once_with(kwargs=task_kwargs)
|
||||
COURSE_GRADE_NOW_PASSED.connect(listen_for_passing_grade, dispatch_uid='new_passing_learner')
|
||||
|
||||
def test_handle_enterprise_learner_subsection(self):
|
||||
"""
|
||||
Test to assert transmit_subsection_learner_data is called when COURSE_ASSESSMENT_GRADE_CHANGED signal is fired.
|
||||
"""
|
||||
with patch(
|
||||
'integrated_channels.integrated_channel.tasks.transmit_subsection_learner_data.apply_async',
|
||||
return_value=None
|
||||
) as mock_task_apply:
|
||||
course_key = CourseKey.from_string(self.course_id)
|
||||
COURSE_ASSESSMENT_GRADE_CHANGED.disconnect()
|
||||
COURSE_ASSESSMENT_GRADE_CHANGED.send(
|
||||
sender=None,
|
||||
user=self.user,
|
||||
course_id=course_key,
|
||||
subsection_id='subsection_id',
|
||||
subsection_grade=1.0
|
||||
)
|
||||
self.assertFalse(mock_task_apply.called)
|
||||
|
||||
self._create_enterprise_enrollment(self.user.id, self.course_id)
|
||||
task_kwargs = {
|
||||
'username': self.user.username,
|
||||
'course_run_id': self.course_id,
|
||||
'subsection_id': 'subsection_id',
|
||||
'grade': '1.0'
|
||||
}
|
||||
COURSE_ASSESSMENT_GRADE_CHANGED.send(
|
||||
sender=None,
|
||||
user=self.user,
|
||||
course_id=course_key,
|
||||
subsection_id='subsection_id',
|
||||
subsection_grade=1.0
|
||||
)
|
||||
mock_task_apply.assert_called_once_with(kwargs=task_kwargs)
|
||||
COURSE_ASSESSMENT_GRADE_CHANGED.connect(listen_for_passing_grade)
|
||||
|
||||
@@ -450,11 +450,21 @@ class TestEnterpriseUtils(TestCase):
|
||||
assert '' == generic_name
|
||||
|
||||
def test_is_enterprise_learner(self):
|
||||
EnterpriseCustomerUserFactory.create(active=True, user_id=self.user.id)
|
||||
self.assertTrue(is_enterprise_learner(self.user))
|
||||
with mock.patch(
|
||||
'django.core.cache.cache.set'
|
||||
) as mock_cache_set:
|
||||
EnterpriseCustomerUserFactory.create(active=True, user_id=self.user.id)
|
||||
self.assertTrue(is_enterprise_learner(self.user))
|
||||
|
||||
self.assertTrue(mock_cache_set.called)
|
||||
|
||||
def test_is_enterprise_learner_no_enterprise_user(self):
|
||||
self.assertFalse(is_enterprise_learner(self.user))
|
||||
with mock.patch(
|
||||
'django.core.cache.cache.set'
|
||||
) as mock_cache_set:
|
||||
self.assertFalse(is_enterprise_learner(self.user))
|
||||
|
||||
self.assertFalse(mock_cache_set.called)
|
||||
|
||||
@mock.patch('openedx.features.enterprise_support.utils.reverse')
|
||||
def test_get_enterprise_slug_login_url_no_reverse_match(self, mock_reverse):
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
|
||||
from crum import get_current_request
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from edx_django_utils.cache import TieredCache, get_cache_key
|
||||
@@ -33,6 +34,13 @@ def get_data_consent_share_cache_key(user_id, course_id):
|
||||
return get_cache_key(type='data_sharing_consent_needed', user_id=user_id, course_id=course_id)
|
||||
|
||||
|
||||
def get_is_enterprise_cache_key(user_id):
|
||||
"""
|
||||
Returns cache key for the enterprise learner validation method needed against user_id.
|
||||
"""
|
||||
return get_cache_key(type='is_enterprise_learner', user_id=user_id)
|
||||
|
||||
|
||||
def clear_data_consent_share_cache(user_id, course_id):
|
||||
"""
|
||||
clears data_sharing_consent_needed cache
|
||||
@@ -393,7 +401,7 @@ def get_enterprise_learner_generic_name(request):
|
||||
|
||||
def is_enterprise_learner(user):
|
||||
"""
|
||||
Check if the given user belongs to an enterprise.
|
||||
Check if the given user belongs to an enterprise. Cache the value if an enterprise learner is found.
|
||||
|
||||
Arguments:
|
||||
user (User): Django User object.
|
||||
@@ -401,7 +409,15 @@ def is_enterprise_learner(user):
|
||||
Returns:
|
||||
(bool): True if given user is an enterprise learner.
|
||||
"""
|
||||
return EnterpriseCustomerUser.objects.filter(user_id=user.id).exists()
|
||||
cached_is_enterprise_key = get_is_enterprise_cache_key(user.id)
|
||||
if cache.get(cached_is_enterprise_key):
|
||||
return True
|
||||
|
||||
if EnterpriseCustomerUser.objects.filter(user_id=user.id).exists():
|
||||
# Cache the enterprise user for one hour.
|
||||
cache.set(cached_is_enterprise_key, True, 3600)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_enterprise_slug_login_url():
|
||||
|
||||
Reference in New Issue
Block a user