From 1b507cffb19f79e5af1f9e2a4923977d3f1d0d87 Mon Sep 17 00:00:00 2001 From: Jonathan Reveille Date: Mon, 24 Feb 2025 19:37:18 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(backend)=20generate=20certificates?= =?UTF-8?q?=20with=20product=20type=20certificate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is now possible to launch the generation of certificates for orders with product of type certificate from the BO. Fix #1063 --- CHANGELOG.md | 4 + .../core/utils/course_product_relation.py | 10 +- .../test_generate_certificates.py | 74 ++++++++- .../test_utils_course_product_relation.py | 142 +++++++++++++----- 4 files changed, 187 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74526f0ec..7719a9ba9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to - Remove `owner` and `is_main` in CreditCard model permanently - Remove `is_active` on order group client serializer +## Fixed + +- BO: generate certificates for orders with product of type certificate + ## [2.16.0] - 2025-02-13 ### Added diff --git a/src/backend/joanie/core/utils/course_product_relation.py b/src/backend/joanie/core/utils/course_product_relation.py index aa5cb5513..02a47ed3d 100644 --- a/src/backend/joanie/core/utils/course_product_relation.py +++ b/src/backend/joanie/core/utils/course_product_relation.py @@ -1,17 +1,20 @@ """Utility methods to get all orders and/or certificates from a course product relation.""" +from django.db.models import Q + from joanie.core.enums import ORDER_STATE_COMPLETED, PRODUCT_TYPE_CERTIFICATE_ALLOWED from joanie.core.models import Certificate, Order def get_orders(course_product_relation): """ - Returns a list of all validated orders ids for a course product relation. + Returns a list of all completed orders ids for a course product relation. """ return [ str(order_id) for order_id in Order.objects.filter( - course=course_product_relation.course, + Q(course=course_product_relation.course, enrollment__isnull=True) + | Q(course__isnull=True, enrollment__isnull=False), product=course_product_relation.product, product__type__in=PRODUCT_TYPE_CERTIFICATE_ALLOWED, state=ORDER_STATE_COMPLETED, @@ -27,8 +30,9 @@ def get_generated_certificates(course_product_relation): Return certificates that were published for a course product relation. """ return Certificate.objects.filter( + Q(order__course=course_product_relation.course, order__enrollment__isnull=True) + | Q(order__course__isnull=True, order__enrollment__isnull=False), order__product=course_product_relation.product, - order__course=course_product_relation.course, order__certificate__isnull=False, order__state=ORDER_STATE_COMPLETED, ) diff --git a/src/backend/joanie/tests/core/api/admin/course_product_relations/test_generate_certificates.py b/src/backend/joanie/tests/core/api/admin/course_product_relations/test_generate_certificates.py index 238425453..3c5fb2084 100644 --- a/src/backend/joanie/tests/core/api/admin/course_product_relations/test_generate_certificates.py +++ b/src/backend/joanie/tests/core/api/admin/course_product_relations/test_generate_certificates.py @@ -11,7 +11,7 @@ from django.utils import timezone from joanie.core import enums, factories -from joanie.core.models import Certificate, CourseProductRelation +from joanie.core.models import Certificate, CourseProductRelation, CourseState from joanie.lms_handler.backends.dummy import DummyLMSBackend @@ -683,3 +683,75 @@ def test_api_admin_course_product_relation_check_certificates_generation_complet # Verify that certificates were generated for order in orders: self.assertTrue(Certificate.objects.filter(order=order).exists()) + + @mock.patch("joanie.core.api.admin.generate_certificates_task") + @mock.patch( + "joanie.lms_handler.backends.dummy.DummyLMSBackend.get_grades", + return_value={"passed": True}, + ) + def test_api_admin_course_product_relation_generate_certificate_product_certificate( + self, _mock_get_grades, mock_generate_certificates_task + ): + """ + Authenticated admin user should be able to generate certificate for order with products + of type certificate. The task should be called with the orders that are eligible to get + their certificate generated. + """ + admin = factories.UserFactory(is_staff=True, is_superuser=True) + self.client.login(username=admin.username, password="password") + + course_run = factories.CourseRunFactory( + is_listed=True, state=CourseState.ONGOING_OPEN + ) + enrollments = factories.EnrollmentFactory.create_batch(7, course_run=course_run) + product = factories.ProductFactory( + price=0, + type=enums.PRODUCT_TYPE_CERTIFICATE, + ) + relation = factories.CourseProductRelationFactory( + product=product, course=enrollments[0].course_run.course + ) + + # Create 3 orders where the certificate will be generated + order_ids = [] + for enrollment in enrollments[:3]: + order_ids.append( + factories.OrderFactory( + product=relation.product, + enrollment=enrollment, + course=None, + state=enums.ORDER_STATE_COMPLETED, + ).id + ) + # Create 4 orders where the certificate has been generated + for enrollment in enrollments[3:]: + factories.OrderCertificateFactory( + order=factories.OrderFactory( + product=relation.product, + course=None, + enrollment=enrollment, + state=enums.ORDER_STATE_COMPLETED, + ) + ) + + response = self.client.post( + f"/api/v1.0/admin/course-product-relations/{relation.id}/generate_certificates/", + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.CREATED) + self.assertDictEqual( + response.json(), + { + "course_product_relation_id": str(relation.id), + "count_certificate_to_generate": 3, + "count_exist_before_generation": 4, + }, + ) + self.assertTrue(mock_generate_certificates_task.delay.called) + self.assertTrue( + mock_generate_certificates_task.delay.called_with( + order_ids=order_ids, + cache_key=f"celery_certificate_generation_{relation.id}", + ) + ) diff --git a/src/backend/joanie/tests/core/test_utils_course_product_relation.py b/src/backend/joanie/tests/core/test_utils_course_product_relation.py index f4147e13e..7d6e68b3e 100644 --- a/src/backend/joanie/tests/core/test_utils_course_product_relation.py +++ b/src/backend/joanie/tests/core/test_utils_course_product_relation.py @@ -1,12 +1,9 @@ """Test suite utility methods for course product relation to get orders and certificates""" -import datetime - from django.test import TestCase -from django.utils import timezone from joanie.core import enums, factories -from joanie.core.models import CourseProductRelation +from joanie.core.models import CourseProductRelation, CourseState from joanie.core.utils.course_product_relation import ( get_generated_certificates, get_orders, @@ -16,69 +13,93 @@ class UtilsCourseProductRelationTestCase(TestCase): """Test suite utility methods for course product relation to get orders and certificates""" - def test_utils_course_product_relation_get_orders_made(self): + def test_utils_course_product_relation_get_orders_for_product_type_credential(self): """ - It should return the amount of orders that are validated for this course product - relation. + It should return the list of orders ids that are completed for this course product relation + with a product of type credential where the certificate has not been published yet. """ - course = factories.CourseFactory(products=None) - product = factories.ProductFactory( - price="0.00", - type=enums.PRODUCT_TYPE_CREDENTIAL, - certificate_definition=factories.CertificateDefinitionFactory(), - courses=[course], - ) + course = factories.CourseFactory() factories.CourseRunFactory( course=course, - enrollment_end=timezone.now() + datetime.timedelta(hours=1), - enrollment_start=timezone.now() - datetime.timedelta(hours=1), + state=CourseState.ONGOING_OPEN, + is_listed=True, is_gradable=True, - start=timezone.now() - datetime.timedelta(hours=1), ) - factories.ProductTargetCourseRelationFactory( - product=product, - course=course, - is_graded=True, + product = factories.ProductFactory( + price=0, + type=enums.PRODUCT_TYPE_CREDENTIAL, + courses=[course], ) course_product_relation = CourseProductRelation.objects.get( product=product, course=course ) - # Generate orders for the course product relation - orders = factories.OrderFactory.create_batch( + # Generate orders for the course product relation with the course + factories.OrderFactory.create_batch( 10, product=course_product_relation.product, course=course_product_relation.course, + enrollment=None, + state=enums.ORDER_STATE_COMPLETED, ) - for order in orders: - order.init_flow() result = get_orders(course_product_relation=course_product_relation) self.assertEqual(len(result), 10) - def test_utils_course_product_relation_get_generated_certificates(self): + def test_utils_course_product_relation_get_orders_for_product_type_certificate( + self, + ): + """ + It should return the list of orders ids that are completed for the course product relation + with a product of type certificate where the certificate has not been published yet. + """ + course_run = factories.CourseRunFactory( + is_gradable=True, is_listed=True, state=CourseState.ONGOING_OPEN + ) + enrollments = factories.EnrollmentFactory.create_batch(5, course_run=course_run) + product = factories.ProductFactory( + price=0, + type=enums.PRODUCT_TYPE_CERTIFICATE, + ) + relation = factories.CourseProductRelationFactory( + product=product, course=enrollments[0].course_run.course + ) + + orders = get_orders(course_product_relation=relation) + + self.assertEqual(len(orders), 0) + + # Generate orders for the course product relation with the enrollments + for enrollment in enrollments: + factories.OrderFactory( + product=relation.product, + enrollment=enrollment, + course=None, + state=enums.ORDER_STATE_COMPLETED, + ) + + orders = get_orders(course_product_relation=relation) + + self.assertEqual(len(orders), 5) + + def test_utils_course_product_relation_get_generated_certificates_for_product_type_credential( + self, + ): """ It should return the amount of certificates that were published for this course product - relation. + relation with a product of type credential. """ - course = factories.CourseFactory(products=None) + course = factories.CourseFactory() product = factories.ProductFactory( - price="0.00", + price=0, type=enums.PRODUCT_TYPE_CREDENTIAL, - certificate_definition=factories.CertificateDefinitionFactory(), courses=[course], ) factories.CourseRunFactory( course=course, - enrollment_end=timezone.now() + datetime.timedelta(hours=1), - enrollment_start=timezone.now() - datetime.timedelta(hours=1), + state=CourseState.ONGOING_OPEN, + is_listed=True, is_gradable=True, - start=timezone.now() - datetime.timedelta(hours=1), - ) - factories.ProductTargetCourseRelationFactory( - product=product, - course=course, - is_graded=True, ) course_product_relation = CourseProductRelation.objects.get( product=product, course=course @@ -87,6 +108,7 @@ def test_utils_course_product_relation_get_generated_certificates(self): generated_certificates_queryset = get_generated_certificates( course_product_relation=course_product_relation ) + self.assertEqual(generated_certificates_queryset.count(), 0) # Generate certificates for the course product relation @@ -94,9 +116,10 @@ def test_utils_course_product_relation_get_generated_certificates(self): 5, product=course_product_relation.product, course=course_product_relation.course, + enrollment=None, + state=enums.ORDER_STATE_COMPLETED, ) for order in orders: - order.init_flow() factories.OrderCertificateFactory(order=order) generated_certificates_queryset = get_generated_certificates( @@ -104,3 +127,44 @@ def test_utils_course_product_relation_get_generated_certificates(self): ) self.assertEqual(generated_certificates_queryset.count(), 5) + + def test_utils_course_product_relation_get_generated_certificated_for_product_type_certificate( + self, + ): + """ + It should return the amount of certificates that were published for this course product + relation with a product of type certificate. + """ + course_run = factories.CourseRunFactory( + is_gradable=True, is_listed=True, state=CourseState.ONGOING_OPEN + ) + enrollments = factories.EnrollmentFactory.create_batch( + 10, course_run=course_run + ) + product = factories.ProductFactory(price=0, type=enums.PRODUCT_TYPE_CERTIFICATE) + relation = factories.CourseProductRelationFactory( + product=product, course=enrollments[0].course_run.course + ) + + generated_certificates_queryset = get_generated_certificates( + course_product_relation=relation + ) + + self.assertEqual(generated_certificates_queryset.count(), 0) + + # Generate certificates for the course product relation + for enrollment in enrollments: + factories.OrderCertificateFactory( + order=factories.OrderFactory( + product=relation.product, + enrollment=enrollment, + course=None, + state=enums.ORDER_STATE_COMPLETED, + ) + ) + + generated_certificates_queryset = get_generated_certificates( + course_product_relation=relation + ) + + self.assertEqual(generated_certificates_queryset.count(), 10)