Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions eox_nelp/edxapp_wrapper/backends/certificates_m_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,23 @@ def get_generate_course_certificate_method():
generate_course_certificate method.
"""
return certificates.generation.generate_course_certificate


def get_certificates_utils():
"""Allow to get the certificates utils module.
https://github.com/nelc/edx-platform/tree/open-release/redwood.nelp/lms/djangoapps/certificates/utils.py

Returns:
certificates.utils module.
"""
return certificates.utils


def get_certificates_models():
"""Allow to get the certificates models module.
https://github.com/nelc/edx-platform/tree/open-release/redwood.nelp/lms/djangoapps/certificates/models.py

Returns:
certificates.models module.
"""
return certificates.models
2 changes: 2 additions & 0 deletions eox_nelp/edxapp_wrapper/certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@

GeneratedCertificateAdmin = backend.get_generated_certificates_admin()
generate_course_certificate = backend.get_generate_course_certificate_method()
utils = backend.get_certificates_utils()
models = backend.get_certificates_models()
16 changes: 16 additions & 0 deletions eox_nelp/edxapp_wrapper/test_backends/certificates_m_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,19 @@ def get_generate_course_certificate_method():
Mock instance.
"""
return Mock()


def get_certificates_utils():
"""Return utils mock module
Returns:
Mock instance.
"""
return Mock()


def get_certificates_models():
"""Return models mock module
Returns:
Mock instance.
"""
return Mock()
13 changes: 13 additions & 0 deletions eox_nelp/programs/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,16 @@ class ProgramLookupSerializer(serializers.Serializer, ProgramValidationMixin):
training_location = serializers.CharField(read_only=True, default="FutureX")
trainer_type = serializers.IntegerField(read_only=True, default=10)
unit = serializers.CharField(read_only=True, default="hour")

certificate_url = serializers.SerializerMethodField()
completion_date = serializers.CharField(required=True, allow_null=True)
completion_date_hijri = serializers.CharField(required=True, allow_null=True)

def get_certificate_url(self, program):
"""Generate certificate_url based on user and course code.
Args:
program: Program data dictionary.
"""
if program.get("certificate_path"):
return self.context["request"].build_absolute_uri(program["certificate_path"])
return None
56 changes: 44 additions & 12 deletions eox_nelp/programs/api/v1/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,14 @@ class GetProgramLookupRepresentationTestCase(unittest.TestCase):
"""Test cases for get_program_lookup_representation."""

@patch("eox_nelp.programs.api.v1.utils.get_program_metadata")
@patch("eox_nelp.programs.api.v1.utils.convert_to_isoformat")
@patch("eox_nelp.programs.api.v1.utils.hms_to_int")
@patch("eox_nelp.programs.api.v1.utils.Gregorian")
def test_get_program_lookup_representation(self, mock_gregorian, mock_hms_to_int, mock_convert, mock_get_metadata):
@patch("eox_nelp.programs.api.v1.utils.get_user_lms_certificate_path")
@patch("eox_nelp.programs.api.v1.utils.get_user_generated_certificate")
def test_get_program_lookup_representation(
self,
mock_get_user_generated_certificate,
mock_get_user_lms_certificate_path,
mock_get_metadata,
):
"""
Test generating program lookup representation.
Expected behavior:
Expand All @@ -94,24 +98,26 @@ def test_get_program_lookup_representation(self, mock_gregorian, mock_hms_to_int
"mandatory": "01",
"program_approve": "00",
}
mock_convert.side_effect = lambda x: x
mock_hms_to_int.return_value = 5
mock_gregorian.fromisoformat.return_value.to_hijri.return_value.isoformat.return_value = "1440-01-01"
mock_get_user_generated_certificate.return_value = MagicMock(
modified_date=MagicMock(strftime=MagicMock(return_value="2024-06-01T00:00:00Z"))
)
mock_get_user_lms_certificate_path.return_value = "/certificates/abc-123"
course_api_data = {
"course_id": "course-v1:edx+test+2024",
"name": "Test Course",
"start": "2020-01-01T00:00:00Z",
"end": "2020-12-31T00:00:00Z",
"effort": "5:00",
}
user = MagicMock(id=123)
expected_data = {
"program_name": "Test Course",
"program_code": "CODE",
"training_location": "FutureX",
"date_start": "2020-01-01T00:00:00Z",
"date_start_hijri": "1440-01-01",
"date_end": "2020-12-31T00:00:00Z",
"date_end_hijri": "1440-01-01",
"date_start": "2020-01-01",
"date_start_hijri": "1441-05-06",
"date_end": "2020-12-31",
"date_end_hijri": "1442-05-16",
"trainer_type": 10,
"type_of_activity": None,
"type_of_activity_id": 1,
Expand All @@ -120,8 +126,12 @@ def test_get_program_lookup_representation(self, mock_gregorian, mock_hms_to_int
"mandatory": "01",
"program_approve": "00",
"code": "course-v1:edx+test+2024",
"certificate_path": "/certificates/abc-123",
"completion_date": "2024-06-01",
"completion_date_hijri": "1445-11-24",
}
result = utils.get_program_lookup_representation(course_api_data)

result = utils.get_program_lookup_representation(user, course_api_data)

self.assertDictEqual(result, expected_data)

Expand Down Expand Up @@ -203,3 +213,25 @@ def test_hms_to_int_minutes_out_of_range(self):
"""
self.assertEqual(utils.hms_to_int("2:99"), 2)
self.assertEqual(utils.hms_to_int("2:-5"), 2)

class GetUserLmsCertificatePathTestCase(unittest.TestCase):
"""Test cases for get_user_lms_certificate_path."""

@patch("eox_nelp.programs.api.v1.utils.certificates_utils._certificate_html_url")
@patch("eox_nelp.programs.api.v1.utils.certificates_utils.certificate_status_for_student")
def test_returns_certificate_path(self, mock_cert_status, mock_cert_url):
"""
Test that the function returns the correct certificate path.
Expected behavior:
- Returns the LMS certificate path for the user and course.
- Calls certificate_status_for_student and _certificate_html_url with correct parameters.
"""
generated_certificate = MagicMock()
mock_cert_status.return_value = {"uuid": "abc-123"}
mock_cert_url.return_value = "/certificates/abc-123"

result = utils.get_user_lms_certificate_path(generated_certificate)

mock_cert_status.assert_called_once_with(generated_certificate)
mock_cert_url.assert_called_once_with(uuid="abc-123")
self.assertEqual(result, "/certificates/abc-123")
148 changes: 140 additions & 8 deletions eox_nelp/programs/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,11 +405,108 @@ def test_get_programs_list_authenticated_without_national_id(self):

@patch("eox_nelp.programs.api.v1.views.CourseDetailSerializer")
@patch("eox_nelp.programs.api.v1.utils.get_program_metadata")
@patch("eox_nelp.programs.api.v1.utils.get_user_lms_certificate_path")
@patch("eox_nelp.programs.api.v1.utils.get_user_generated_certificate")
@patch("eox_nelp.programs.api.v1.views.CourseEnrollment.is_enrolled")
@patch("eox_nelp.programs.api.v1.views.courses")
def test_get_programs_filtered_by_certificated(
self,
mock_courses,
mock_is_enrolled,
mock_get_user_generated_certificate,
mock_get_user_lms_certificate_path,
mock_get_program_metadata,
mock_course_serializer,
): # pylint: disable=too-many-arguments, too-many-positional-arguments
"""
Test GET returns program list for authenticated user filtered by certificated only.
Expected behavior:
- Response without filter Status code 200.
- Response without filter matches expected data.
- Response with certificated_only=true Status code 404.
- Response with certificated_only=true returns error as no certificated programs found. """
user_by_national_id_instance, _ = User.objects.get_or_create(username="user0", password="pass0")
national_id = "1222888555"
ExtraInfo.objects.get_or_create( # pylint: disable=no-member
user=user_by_national_id_instance,
arabic_name="مسؤل",
national_id=national_id,
)
mock_courses.get_courses.return_value = [MagicMock(id="course-v1:edx+special+2024")]
serializer_side_effect = []
mock_is_enrolled.return_value = True
for course_data in COURSE_API_SERIALIZER_DATA:
mock_serializer_instance = MagicMock()
mock_serializer_instance.data = course_data
serializer_side_effect.append(mock_serializer_instance)
mock_course_serializer.side_effect = serializer_side_effect
mock_get_user_lms_certificate_path.return_value = ""
mock_get_program_metadata.return_value = {
"trainer_type": 10,
"type_of_activity": 165,
"mandatory": "01",
"program_approve": "00",
"program_code": "eltesst",
"program_name": "small-graded",
}
mock_get_user_generated_certificate.return_value = MagicMock(
modified_date=MagicMock(strftime=MagicMock(return_value="2024-06-01T00:00:00Z"))
)
expected_data = {
"results": [
{
"program_name": "testigngg",
"program_code": "eltesst",
"type_of_activity": "برنامج الاستثمار الأمثل (برامج قصيرة)",
"type_of_activity_id": 165,
"mandatory": "01",
"program_approve": "00",
"code": "course-v1:edx+cd101+2020323",
"date_start": "2030-01-01",
"date_end": None,
"date_start_hijri": "1451-08-26",
"date_end_hijri": None,
"duration": 1,
"training_location": "FutureX",
"trainer_type": 10,
"unit": "hour",
"certificate_url": None,
"completion_date": "2024-06-01",
"completion_date_hijri": "1445-11-24",
}
],
"pagination": {"next": None, "previous": None, "count": 1, "num_pages": 1},
}

response = self.client.get(self.url, {"national_id": national_id})
response_filtered = self.client.get(self.url, {"national_id": national_id, "certificated_only": "true"})

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(response.data, expected_data)
self.assertDictEqual(
response_filtered.data,
{
"error": "NO_PROGRAM_FOR_NATIONAL_ID",
"message": "No program found for the provided National ID 1222888555."
},
)
self.assertEqual(response_filtered.status_code, status.HTTP_404_NOT_FOUND)

@patch("eox_nelp.programs.api.v1.views.CourseDetailSerializer")
@patch("eox_nelp.programs.api.v1.utils.get_program_metadata")
@patch("eox_nelp.programs.api.v1.utils.get_user_lms_certificate_path")
@patch("eox_nelp.programs.api.v1.utils.get_user_generated_certificate")
@patch("eox_nelp.programs.api.v1.views.CourseEnrollment.is_enrolled")
@patch("eox_nelp.programs.api.v1.views.courses")
def test_get_programs_list_with_national_id(
self, mock_courses, mock_is_enrolled, mock_get_program_metadata, mock_course_serializer,
):
self,
mock_courses,
mock_is_enrolled,
mock_get_user_generated_certificate,
mock_get_user_lms_certificate_path,
mock_get_program_metadata,
mock_course_serializer,
): # pylint: disable=too-many-arguments, too-many-positional-arguments
"""
Test GET returns program list for user with given national_id.
Expected behavior:
Expand Down Expand Up @@ -441,6 +538,10 @@ def test_get_programs_list_with_national_id(
"program_approve": "01",
"program_code": "nationalidtest",
}
mock_get_user_lms_certificate_path.return_value = "/certificates/def-456"
mock_get_user_generated_certificate.return_value = MagicMock(
modified_date=MagicMock(strftime=MagicMock(return_value="2024-06-01T00:00:00Z"))
)
expected_data = [
{
"program_name": "testigngg",
Expand All @@ -458,6 +559,9 @@ def test_get_programs_list_with_national_id(
"training_location": "FutureX",
"trainer_type": 10,
"unit": "hour",
"certificate_url": "http://testserver/certificates/def-456",
"completion_date": "2024-06-01",
"completion_date_hijri": "1445-11-24",
}
]

Expand Down Expand Up @@ -499,9 +603,16 @@ def test_get_programs_list_unauthenticated_national_id(self):
@patch("eox_nelp.programs.api.v1.views.courses")
@patch("eox_nelp.programs.api.v1.views.CourseDetailSerializer")
@patch("eox_nelp.programs.api.v1.utils.get_program_metadata")
@patch("eox_nelp.programs.api.v1.utils.get_user_lms_certificate_path")
@patch("eox_nelp.programs.api.v1.utils.get_user_generated_certificate")
def test_get_programs_list_missing_data(
self, mock_get_program_metadata, mock_course_serializer, mock_courses
):
self,
mock_get_user_generated_certificate,
mock_get_user_lms_certificate_path,
mock_get_program_metadata,
mock_course_serializer,
mock_courses,
): # pylint: disable=too-many-arguments, too-many-positional-arguments
"""
Test GET returns error if ProgramLookupSerializer is invalid.
Expected behavior:
Expand All @@ -525,6 +636,8 @@ def test_get_programs_list_missing_data(
serializer_side_effect.append(mock_serializer_instance)
mock_course_serializer.side_effect = serializer_side_effect
mock_get_program_metadata.return_value = {}
mock_get_user_lms_certificate_path.return_value = ""
mock_get_user_generated_certificate.return_value = None
expected_data = [
{
"program_name": "testigngg",
Expand All @@ -542,6 +655,9 @@ def test_get_programs_list_missing_data(
"training_location": "FutureX",
"trainer_type": 10,
"unit": "hour",
"certificate_url": None,
"completion_date": None,
"completion_date_hijri": None,
},
{
"program_name": "small-graded",
Expand All @@ -559,6 +675,9 @@ def test_get_programs_list_missing_data(
"training_location": "FutureX",
"trainer_type": 10,
"unit": "hour",
"certificate_url": None,
"completion_date": None,
"completion_date_hijri": None,
},
]

Expand Down Expand Up @@ -592,13 +711,21 @@ def test_get_programs_list_not_found_national_id(self, mock_super_get):

@patch("eox_nelp.programs.api.v1.views.CourseDetailSerializer")
@patch("eox_nelp.programs.api.v1.utils.get_program_metadata")
@patch("eox_nelp.programs.api.v1.utils.get_user_lms_certificate_path")
@patch("eox_nelp.programs.api.v1.utils.get_user_generated_certificate")
@patch("eox_nelp.programs.api.v1.views.CourseEnrollment.is_enrolled")
@patch("eox_nelp.programs.api.v1.views.courses")
def test_get_programs_list_no_courses_enrolled(
self, mock_courses, mock_is_enrolled, _, mock_course_serializer,
):
"""
Test GET returns program list for user with given national_id.
self,
mock_courses,
mock_is_enrolled,
mock_get_user_generated_certificate,
mock_get_user_lms_certificate_path,
mock_get_program_metadata,
mock_course_serializer,
): # pylint: disable=too-many-arguments, too-many-positional-arguments
"""
Test GET not returns program list for user with given national_id.
Expected behavior:
- Status code 400.
- The expected data matches with the response.
Expand All @@ -621,6 +748,11 @@ def test_get_programs_list_no_courses_enrolled(
course = MagicMock(id="course-v1:edx+special+2024")
mock_courses.get_courses.return_value = [course]
mock_is_enrolled.return_value = False
mock_get_program_metadata.return_value = {}
mock_get_user_lms_certificate_path.return_value = ""
mock_get_user_generated_certificate.return_value = MagicMock(
modified_date=MagicMock(strftime=MagicMock(return_value="2024-06-01T00:00:00Z"))
)

response = self.client.get(self.url, {"national_id": national_id})

Expand Down
Loading
Loading