Skip to content

Commit

Permalink
feat: add create external certificate task
Browse files Browse the repository at this point in the history
  • Loading branch information
andrey-canon committed Aug 5, 2023
1 parent 3efc731 commit a85da6d
Show file tree
Hide file tree
Showing 10 changed files with 418 additions and 40 deletions.
11 changes: 11 additions & 0 deletions eox_nelp/edxapp_wrapper/backends/student_m_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This file contains all the necessary student dependencies from
https://github.com/eduNEXT/edunext-platform/tree/ednx-release/mango.master/common/djangoapps/student
"""
from common.djangoapps.student import models_api as student_api # pylint: disable=import-error
from common.djangoapps.student.models import ( # pylint: disable=import-error
CourseAccessRole,
CourseEnrollment,
Expand Down Expand Up @@ -34,3 +35,13 @@ def get_user_profile_model():
UserProfile Model.
"""
return UserProfile


def get_student_api():
"""Allow to get the student_api module from
https://github.com/eduNEXT/edunext-platform/blob/ednx-release/mango.master/common/djangoapps/student/models_api.py
Returns:
models_api module.
"""
return student_api
1 change: 1 addition & 0 deletions eox_nelp/edxapp_wrapper/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
CourseEnrollment = backend.get_course_enrollment_model()
CourseAccessRole = backend.get_course_access_role_model()
UserProfile = backend.get_user_profile_model()
student_api = backend.get_student_api()
8 changes: 8 additions & 0 deletions eox_nelp/edxapp_wrapper/test_backends/student_m_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ def get_user_profile_model():
Mock class.
"""
return Mock()


def get_student_api():
"""Return test Module.
Returns:
Mock class.
"""
return Mock()
2 changes: 2 additions & 0 deletions eox_nelp/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def plugin_settings(settings): # pylint: disable=function-redefined
settings.EXTERNAL_CERTIFICATES_API_URL = 'https://testing.com'
settings.EXTERNAL_CERTIFICATES_USER = 'test-user'
settings.EXTERNAL_CERTIFICATES_PASSWORD = 'test-password'
settings.ENABLE_CERTIFICATE_PUBLISHER = True


SETTINGS = SettingsClass()
Expand Down Expand Up @@ -97,6 +98,7 @@ def plugin_settings(settings): # pylint: disable=function-redefined

EOX_CORE_COURSEWARE_BACKEND = "eox_nelp.edxapp_wrapper.test_backends.courseware_m_v1"
EOX_CORE_GRADES_BACKEND = "eox_nelp.edxapp_wrapper.test_backends.grades_m_v1"
EOX_CORE_CERTIFICATES_BACKEND = "eox_core.edxapp_wrapper.backends.certificates_h_v1_test"

GET_SITE_CONFIGURATION_MODULE = 'eox_tenant.edxapp_wrapper.backends.site_configuration_module_test_v1'
GET_THEMING_HELPERS = 'eox_tenant.edxapp_wrapper.backends.theming_helpers_test_v1'
Expand Down
15 changes: 12 additions & 3 deletions eox_nelp/signals/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from django.conf import settings

from eox_nelp.notifications.tasks import create_course_notifications as create_course_notifications_task
from eox_nelp.signals.tasks import dispatch_futurex_progress
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__)

Expand Down Expand Up @@ -63,7 +64,7 @@ def create_course_notifications(course_key, **kwargs): # pylint: disable=unused
create_course_notifications_task.delay(course_id=str(course_key))


def certificate_publisher(certificate, **kwargs): # pylint: disable=unused-argument
def certificate_publisher(certificate, metadata, **kwargs): # pylint: disable=unused-argument
"""
Receiver that is connected to the CERTIFICATE_CREATED signal from 'openedx_events.learning.signals'.
Expand All @@ -78,6 +79,8 @@ def certificate_publisher(certificate, **kwargs): # pylint: disable=unused-argu
certificate<CertificateData>: This an instance of the class defined in this link
https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/data.py#L100
and will provide of the user certificate data.
metadata <EventsMetadata>: Instance of the class defined in this link
https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/data.py#L29
"""
if not getattr(settings, "ENABLE_CERTIFICATE_PUBLISHER", False):
return
Expand All @@ -95,9 +98,15 @@ def certificate_publisher(certificate, **kwargs): # pylint: disable=unused-argu
certificate.user.pii.username,
certificate.course.course_key,
)
create_external_certificate.delay(
external_certificate_data=_generate_external_certificate_data(
timestamp=metadata.time,
certificate_data=certificate,
)
)
else:
LOGGER.info(
"The %s certificate associated with the user <%s> and course <%s>"
"The %s certificate associated with the user <%s> and course <%s> "
"doesn't have a valid mode and therefore its data won't be published.",
certificate.mode,
certificate.user.pii.username,
Expand Down
32 changes: 16 additions & 16 deletions eox_nelp/signals/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,17 @@
from django.db.models import Q
from eox_core.edxapp_wrapper.courseware import get_courseware_courses
from eox_core.edxapp_wrapper.enrollments import get_enrollment
from eox_core.edxapp_wrapper.grades import get_course_grade_factory
from opaque_keys.edx.keys import CourseKey

from eox_nelp.api_clients.certificates import ExternalCertificatesApiClient
from eox_nelp.api_clients.futurex import FuturexApiClient
from eox_nelp.edxapp_wrapper.course_overviews import CourseOverview
from eox_nelp.signals.utils import _user_has_passing_grade

courses = get_courseware_courses()
CourseGradeFactory = get_course_grade_factory()
logger = logging.getLogger(__name__)


def _user_has_passing_grade(user, course_id):
"""Determines if a user has passed a course based on the grading policies.
Args:
user<User>: Instace of Django User model.
course_id<str>: Unique course identifier.
Returns:
course_grade.passed<bool>: True if the user has passed the course, otherwise False
"""
course_grade = CourseGradeFactory().read(user, course_key=CourseKey.from_string(course_id))

return course_grade.passed


@shared_task
def dispatch_futurex_progress(course_id, user_id, is_complete=None):
"""Dispatch the course progress of a user to Futurex platform.
Expand Down Expand Up @@ -148,3 +134,17 @@ def _generate_progress_enrollment_data(user, course_id, user_has_passing_grade):
progress_enrollment_data,
)
return progress_enrollment_data


@shared_task
def create_external_certificate(external_certificate_data):
"""This will create an external NELP certificate base on the input data
Args:
timestamp<Datetime>: Date when the certificate was created.
certificate<CertificateData>: This an instance of the class defined in this link
https://github.com/eduNEXT/openedx-events/blob/main/openedx_events/learning/data.py#L100
and will provide of the user certificate data.
"""
api_client = ExternalCertificatesApiClient()
api_client.create_external_certificate(external_certificate_data)
180 changes: 179 additions & 1 deletion eox_nelp/signals/tests/test_receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@
import unittest

from django.contrib.auth import get_user_model
from django.test import override_settings
from mock import patch
from opaque_keys.edx.keys import CourseKey
from openedx_events.data import EventsMetadata
from openedx_events.learning.data import CertificateData, CourseData, UserData, UserPersonalData

from eox_nelp.edxapp_wrapper.test_backends import create_test_model
from eox_nelp.signals.receivers import block_completion_progress_publisher, course_grade_changed_progress_publisher
from eox_nelp.signals import receivers
from eox_nelp.signals.receivers import (
block_completion_progress_publisher,
certificate_publisher,
course_grade_changed_progress_publisher,
)

User = get_user_model()

Expand Down Expand Up @@ -64,3 +72,173 @@ def test_call_dispatch(self, dispatch_mock):
course_id=str(course_key),
user_id=13,
)


class CertificatePublisherTestCase(unittest.TestCase):
"""Test class for certificate_publisher."""

def setUp(self):
"""Setup common conditions for every test case"""
self.username = "Harry"
self.course_key = CourseKey.from_string("course-v1:test+Cx105+2022_T4")
self.certificate_data = CertificateData(
user=UserData(
pii=UserPersonalData(
username=self.username,
email="[email protected]",
name="Harry Potter",
),
id=10,
is_active=True,
),
course=CourseData(
course_key=self.course_key,
),
mode="no-id-professional",
grade=5,
current_status="downloadable",
download_url="",
name="",
)
self.metadata = EventsMetadata(
event_type="org.openedx.learning.certificate.created.v1",
minorversion=0,
)

@override_settings(ENABLE_CERTIFICATE_PUBLISHER=False)
@patch("eox_nelp.signals.receivers.create_external_certificate")
def test_inactive_behavior(self, mock_task):
"""Test that the asynchronous task wont' be called when the setting is not active.
Expected behavior:
- create_external_certificate is not called
"""
certificate_publisher(self.certificate_data, self.metadata)

mock_task.delay.assert_not_called()

@patch("eox_nelp.signals.receivers.create_external_certificate")
def test_invalid_mode(self, mock_task):
"""Test when the certificate data 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} certificate associated with the user <{self.username}> and course <{self.course_key}> "
"doesn't have a valid mode and therefore its data won't be published."
)

certificate_data = CertificateData(
user=UserData(
pii=UserPersonalData(
username=self.username,
email="[email protected]",
name="Harry Potter",
),
id=10,
is_active=True,
),
course=CourseData(
course_key=self.course_key,
),
mode=invalid_mode,
grade=5,
current_status="downloadable",
download_url="",
name="",
)

with self.assertLogs(receivers.__name__, level="INFO") as logs:
certificate_publisher(certificate_data, self.metadata)

mock_task.delay.assert_not_called()
self.assertEqual(logs.output, [
f"INFO:{receivers.__name__}:{log_info}"
])

@patch("eox_nelp.signals.receivers._generate_external_certificate_data")
@patch("eox_nelp.signals.receivers.create_external_certificate")
def test_create_call(self, mock_task, generate_data_mock):
"""Test when the certificate 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 = {
"test": True,
}
log_info = (
f"The no-id-professional certificate associated with the user <{self.username}> and "
f"course <{self.course_key}> has been already generated and its data will be sent "
"to the NELC certificate service."
)

with self.assertLogs(receivers.__name__, level="INFO") as logs:
certificate_publisher(self.certificate_data, self.metadata)

generate_data_mock.assert_called_with(
timestamp=self.metadata.time,
certificate_data=self.certificate_data,
)
mock_task.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._generate_external_certificate_data")
@patch("eox_nelp.signals.receivers.create_external_certificate")
def test_alternative_mode(self, mock_task, generate_data_mock):
"""Test when the certificate data 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} certificate associated with the user <{self.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.username,
email="[email protected]",
name="Harry Potter",
),
id=10,
is_active=True,
),
course=CourseData(
course_key=self.course_key,
),
mode=alternative_mode,
grade=5,
current_status="downloadable",
download_url="",
name="",
)

with self.assertLogs(receivers.__name__, level="INFO") as logs:
certificate_publisher(certificate_data, self.metadata)

generate_data_mock.assert_called_with(
timestamp=self.metadata.time,
certificate_data=certificate_data,
)
mock_task.delay.assert_called_with(
external_certificate_data=generate_data_mock()
)
self.assertEqual(logs.output, [
f"INFO:{receivers.__name__}:{log_info}"
])
Loading

0 comments on commit a85da6d

Please sign in to comment.