-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: adding enrollment receiver #91
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,4 +8,4 @@ def get_course_grade_factory(): | |
Returns: | ||
Mock class. | ||
""" | ||
return Mock | ||
return Mock() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,12 +11,15 @@ | |
import logging | ||
|
||
from django.conf import settings | ||
from eox_core.edxapp_wrapper.grades import get_course_grade_factory | ||
from openedx_events.learning.data import CertificateData, CourseData, UserData, UserPersonalData | ||
|
||
from eox_nelp.notifications.tasks import create_course_notifications as create_course_notifications_task | ||
from eox_nelp.signals.tasks import create_external_certificate, dispatch_futurex_progress | ||
from eox_nelp.signals.utils import _generate_external_certificate_data | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
CourseGradeFactory = get_course_grade_factory() | ||
|
||
|
||
def block_completion_progress_publisher(instance, **kwargs): # pylint: disable=unused-argument | ||
|
@@ -112,3 +115,73 @@ def certificate_publisher(certificate, metadata, **kwargs): # pylint: disable=u | |
certificate.user.pii.username, | ||
certificate.course.course_key, | ||
) | ||
|
||
|
||
def enrollment_publisher(instance, **kwargs): # pylint: disable=unused-argument | ||
""" | ||
Receiver that is connected to the course enrollment post_save signal and this will generate certificate | ||
data to publish it to the external service. That behavior is controlled by the following settings: | ||
|
||
- ENABLE_CERTIFICATE_PUBLISHER<boolean>: If this is true the receiver will publish the certificate data, | ||
default is False. | ||
- CERTIFICATE_PUBLISHER_VALID_MODES<list[string]>: List of valid modes, default ['no-id-professional'] | ||
|
||
Note: This keeps the same certificate receiver settings since this will create an external certificate at | ||
the beginning of the course, then the certificate receiver will update the grade. | ||
|
||
Args: | ||
instance<CourseEnrollment>: This an instance of the model CourseEnrollment. | ||
""" | ||
if not getattr(settings, "ENABLE_CERTIFICATE_PUBLISHER", False): | ||
return | ||
|
||
default_modes = [ | ||
"no-id-professional", | ||
] | ||
valid_modes = getattr(settings, "CERTIFICATE_PUBLISHER_VALID_MODES", default_modes) | ||
|
||
if instance.mode in valid_modes: | ||
LOGGER.info( | ||
"The %s enrollment associated with the user <%s> and course <%s> has been already generated " | ||
"and its data will be sent to the NELC certificate service.", | ||
instance.mode, | ||
instance.user.username, | ||
instance.course_id, | ||
) | ||
time = instance.created | ||
user = instance.user | ||
course_grade = CourseGradeFactory().read(user, course_key=instance.course_id) | ||
certificate = CertificateData( | ||
user=UserData( | ||
pii=UserPersonalData( | ||
username=user.username, | ||
email=user.email, | ||
name=user.profile.name, | ||
), | ||
id=user.id, | ||
is_active=user.is_active, | ||
), | ||
course=CourseData( | ||
course_key=instance.course_id, | ||
), | ||
mode=instance.mode, | ||
grade=course_grade.percent, | ||
current_status='downloadable' if course_grade.passed else 'not-passing', | ||
download_url='', | ||
name='', | ||
) | ||
|
||
create_external_certificate.delay( | ||
external_certificate_data=_generate_external_certificate_data( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, the good new is that you found a line that is not necessary |
||
time=time, | ||
certificate_data=certificate, | ||
) | ||
) | ||
else: | ||
LOGGER.info( | ||
"The %s enrollment associated with the user <%s> and course <%s> " | ||
"doesn't have a valid mode and therefore its data won't be published.", | ||
instance.mode, | ||
instance.user.username, | ||
instance.course_id, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,8 @@ | |
|
||
from django.contrib.auth import get_user_model | ||
from django.test import override_settings | ||
from mock import patch | ||
from django.utils import timezone | ||
from mock import Mock, patch | ||
from opaque_keys.edx.keys import CourseKey | ||
from openedx_events.data import EventsMetadata | ||
from openedx_events.learning.data import CertificateData, CourseData, UserData, UserPersonalData | ||
|
@@ -18,7 +19,9 @@ | |
block_completion_progress_publisher, | ||
certificate_publisher, | ||
course_grade_changed_progress_publisher, | ||
enrollment_publisher, | ||
) | ||
from eox_nelp.tests.utils import set_key_values | ||
|
||
User = get_user_model() | ||
|
||
|
@@ -242,3 +245,177 @@ def test_alternative_mode(self, create_external_certificate_mock, generate_data_ | |
self.assertEqual(logs.output, [ | ||
f"INFO:{receivers.__name__}:{log_info}" | ||
]) | ||
|
||
|
||
class EnrollmentPublisherTestCase(unittest.TestCase): | ||
"""Test class for enrollment_publisher.""" | ||
|
||
def setUp(self): | ||
"""Setup common conditions for every test case""" | ||
self.user, _ = User.objects.update_or_create( | ||
username="Newt", | ||
email="[email protected]" | ||
) | ||
self.course_key = CourseKey.from_string("course-v1:test+Cx105+2022_T4") | ||
profile_data = { | ||
"name": "Newt Scamander" | ||
} | ||
setattr(self.user, "profile", set_key_values(profile_data)) | ||
course_enrollment_data = { | ||
"user": self.user, | ||
"created": timezone.now(), | ||
"mode": "no-id-professional", | ||
"course_id": self.course_key | ||
} | ||
self.course_enrollment = set_key_values(course_enrollment_data) | ||
|
||
@override_settings(ENABLE_CERTIFICATE_PUBLISHER=False) | ||
@patch("eox_nelp.signals.receivers.create_external_certificate") | ||
def test_inactive_behavior(self, create_external_certificate_mock): | ||
"""Test that the asynchronous task wont' be called when the setting is not active. | ||
|
||
Expected behavior: | ||
- create_external_certificate is not called | ||
""" | ||
enrollment_publisher(self.course_enrollment) | ||
|
||
create_external_certificate_mock.delay.assert_not_called() | ||
|
||
@patch("eox_nelp.signals.receivers.create_external_certificate") | ||
def test_invalid_mode(self, create_external_certificate_mock): | ||
"""Test when the course enrollment has an invalid mode. | ||
|
||
Expected behavior: | ||
- create_external_certificate is not called. | ||
- Invalid error was logged. | ||
""" | ||
invalid_mode = "audit" | ||
log_info = ( | ||
f"The {invalid_mode} enrollment associated with the user <{self.user.username}>" | ||
f" and course <{self.course_key}> doesn't have a valid mode and therefore its data won't be published." | ||
) | ||
invalid_course_enrollment = self.course_enrollment | ||
|
||
setattr(invalid_course_enrollment, "mode", "audit") | ||
|
||
with self.assertLogs(receivers.__name__, level="INFO") as logs: | ||
enrollment_publisher(invalid_course_enrollment) | ||
|
||
create_external_certificate_mock.delay.assert_not_called() | ||
self.assertEqual(logs.output, [ | ||
f"INFO:{receivers.__name__}:{log_info}" | ||
]) | ||
|
||
@patch("eox_nelp.signals.receivers.CourseGradeFactory") | ||
@patch("eox_nelp.signals.receivers._generate_external_certificate_data") | ||
@patch("eox_nelp.signals.receivers.create_external_certificate") | ||
def test_create_call(self, create_external_certificate_mock, generate_data_mock, course_grade_factory_mock): | ||
"""Test when the enrollment mode is valid and the asynchronous task is called | ||
|
||
Expected behavior: | ||
- _generate_external_certificate_data is called with the right parameters. | ||
- create_external_certificate is called with the _generate_external_certificate_data output. | ||
- Info was logged. | ||
""" | ||
generate_data_mock.return_value = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also as above, this is mocked so t when this doesn't exist the process turns into an error. |
||
"test": True, | ||
} | ||
log_info = ( | ||
f"The no-id-professional enrollment associated with the user <{self.user.username}> and " | ||
f"course <{self.course_key}> has been already generated and its data will be sent " | ||
"to the NELC certificate service." | ||
) | ||
certificate_data = CertificateData( | ||
user=UserData( | ||
pii=UserPersonalData( | ||
username=self.user.username, | ||
email=self.user.email, | ||
name=self.user.profile.name, | ||
), | ||
id=self.user.id, | ||
is_active=self.user.is_active, | ||
), | ||
course=CourseData( | ||
course_key=self.course_key, | ||
), | ||
mode=self.course_enrollment.mode, | ||
grade=0, | ||
current_status='not-passing', | ||
download_url='', | ||
name='', | ||
) | ||
course_grade_factory = Mock() | ||
course_grade_factory.read.return_value = set_key_values({"passed": False, "percent": 0}) | ||
course_grade_factory_mock.return_value = course_grade_factory | ||
|
||
with self.assertLogs(receivers.__name__, level="INFO") as logs: | ||
enrollment_publisher(self.course_enrollment) | ||
|
||
generate_data_mock.assert_called_with( | ||
time=self.course_enrollment.created, | ||
certificate_data=certificate_data, | ||
) | ||
create_external_certificate_mock.delay.assert_called_with( | ||
external_certificate_data=generate_data_mock() | ||
) | ||
self.assertEqual(logs.output, [ | ||
f"INFO:{receivers.__name__}:{log_info}" | ||
]) | ||
|
||
@override_settings(CERTIFICATE_PUBLISHER_VALID_MODES=["another-mode"]) | ||
@patch("eox_nelp.signals.receivers.CourseGradeFactory") | ||
@patch("eox_nelp.signals.receivers._generate_external_certificate_data") | ||
@patch("eox_nelp.signals.receivers.create_external_certificate") | ||
def test_alternative_mode(self, create_external_certificate_mock, generate_data_mock, course_grade_factory_mock): | ||
"""Test when the CERTIFICATE_PUBLISHER_VALID_MODES setting has an alternative mode. | ||
|
||
Expected behavior: | ||
- _generate_external_certificate_data is called with the right parameters. | ||
- create_external_certificate is called with the _generate_external_certificate_data output. | ||
- Info was logged. | ||
""" | ||
alternative_mode = "another-mode" | ||
log_info = ( | ||
f"The {alternative_mode} enrollment associated with the user <{self.user.username}> and " | ||
f"course <{self.course_key}> has been already generated and its data will be sent " | ||
"to the NELC certificate service." | ||
) | ||
certificate_data = CertificateData( | ||
user=UserData( | ||
pii=UserPersonalData( | ||
username=self.user.username, | ||
email=self.user.email, | ||
name=self.user.profile.name, | ||
), | ||
id=self.user.id, | ||
is_active=self.user.is_active, | ||
), | ||
course=CourseData( | ||
course_key=self.course_key, | ||
), | ||
mode=alternative_mode, | ||
grade=0, | ||
current_status='not-passing', | ||
download_url='', | ||
name='', | ||
) | ||
alternative_course_enrollment = self.course_enrollment | ||
setattr(alternative_course_enrollment, "mode", alternative_mode) | ||
|
||
course_grade_factory = Mock() | ||
course_grade_factory.read.return_value = set_key_values({"passed": False, "percent": 0}) | ||
course_grade_factory_mock.return_value = course_grade_factory | ||
|
||
with self.assertLogs(receivers.__name__, level="INFO") as logs: | ||
enrollment_publisher(alternative_course_enrollment) | ||
|
||
generate_data_mock.assert_called_with( | ||
time=self.course_enrollment.created, | ||
certificate_data=certificate_data, | ||
) | ||
create_external_certificate_mock.delay.assert_called_with( | ||
external_certificate_data=generate_data_mock() | ||
) | ||
self.assertEqual(logs.output, [ | ||
f"INFO:{receivers.__name__}:{log_info}" | ||
]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey Here I have some doubt in some signal we uses instance in other the name of the instance.
https://github.com/eduNEXT/eox-nelp/pull/91/files#diff-51a002e5f9d204c9c4fb523f904e54043838095b1a1a2b498d768d5f2d09cb43R70
So instead of instance is not easier course_enrollment ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that names depends on how the signal is emitted