From ef3d65284538af778c597d89b85fc90c4f97cf67 Mon Sep 17 00:00:00 2001 From: Asad Ali Date: Mon, 4 Nov 2024 16:16:58 +0500 Subject: [PATCH] feat: convert CEUs to decimal --- cms/factories.py | 2 +- .../0073_alter_certificatepage_ceus.py | 23 +++++++++++++++++++ cms/models.py | 22 +++++++++++++----- cms/models_test.py | 17 ++++++++------ cms/views_test.py | 2 +- courses/credentials.py | 4 ++-- courses/credentials_test.py | 4 ++-- courses/serializers.py | 4 ++-- courses/serializers_test.py | 9 ++++---- courses/sync_external_courses/emeritus_api.py | 3 ++- .../emeritus_api_test.py | 11 +++++---- ecommerce/mail_api_test.py | 2 +- ecommerce/serializers.py | 4 ++-- ecommerce/serializers_test.py | 2 +- 14 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 cms/migrations/0073_alter_certificatepage_ceus.py diff --git a/cms/factories.py b/cms/factories.py index 8368ff4ab..1ef55a62b 100644 --- a/cms/factories.py +++ b/cms/factories.py @@ -465,7 +465,7 @@ class CertificatePageFactory(wagtail_factories.PageFactory): """CertificatePage factory class""" product_name = factory.fuzzy.FuzzyText(prefix="product_name") - CEUs = factory.Faker("pystr_format", string_format="#.#") + CEUs = factory.fuzzy.FuzzyDecimal(low=1, high=10, precision=2) partner_logo = factory.SubFactory(wagtail_factories.ImageChooserBlockFactory) signatories = wagtail_factories.StreamFieldFactory( {"signatory": factory.SubFactory(SignatoryChooserBlockFactory)} diff --git a/cms/migrations/0073_alter_certificatepage_ceus.py b/cms/migrations/0073_alter_certificatepage_ceus.py new file mode 100644 index 000000000..e63e929ff --- /dev/null +++ b/cms/migrations/0073_alter_certificatepage_ceus.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.16 on 2024-11-06 06:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("cms", "0072_add_hybrid_courseware_format_option"), + ] + + operations = [ + migrations.AlterField( + model_name="certificatepage", + name="CEUs", + field=models.DecimalField( + blank=True, + decimal_places=2, + help_text="Optional field for CEU (continuing education unit).", + max_digits=5, + null=True, + ), + ), + ] diff --git a/cms/models.py b/cms/models.py index ce99a1e85..cd238d593 100644 --- a/cms/models.py +++ b/cms/models.py @@ -1074,7 +1074,9 @@ def get_context(self, request, *args, **kwargs): "techniques": self.techniques, "propel_career": self.propel_career, "news_and_events": self.news_and_events, - "ceus": self.certificate_page.CEUs if self.certificate_page else None, + "ceus": self.certificate_page.normalized_ceus + if self.certificate_page + else None, } def save(self, clean=True, user=None, log_action=False, **kwargs): # noqa: FBT002 @@ -2142,11 +2144,12 @@ class PartnerLogoPlacement(models.IntegerChoices): max_length=255, null=True, blank=True, help_text="Specify the institute text" ) - CEUs = models.CharField( # noqa: DJ001 - max_length=250, + CEUs = models.DecimalField( null=True, blank=True, - help_text="Optional text field for CEU (continuing education unit).", + decimal_places=2, + max_digits=5, + help_text="Optional field for CEU (continuing education unit).", ) partner_logo = models.ForeignKey( @@ -2242,6 +2245,13 @@ def parent(self): """ return self.get_parent().specific + @property + def normalized_ceus(self): + """ + Normalizes the CEUs from decimal to string. Removes the trailing zeros if any. + """ + return f"{self.CEUs.normalize():f}" if self.CEUs else None + def get_context(self, request, *args, **kwargs): preview_context = {} context = {} @@ -2255,14 +2265,14 @@ def get_context(self, request, *args, **kwargs): "end_date": self.parent.product.first_unexpired_run.end_date if self.parent.product.first_unexpired_run else datetime.now() + timedelta(days=45), # noqa: DTZ005 - "CEUs": self.CEUs, + "CEUs": self.normalized_ceus, } elif self.certificate: # Verify that the certificate in fact is for this same course if self.parent.product.id != self.certificate.get_courseware_object_id(): raise Http404 start_date, end_date = self.certificate.start_end_dates - CEUs = self.CEUs + CEUs = self.normalized_ceus for override in self.overrides: if ( diff --git a/cms/models_test.py b/cms/models_test.py index 119b7374d..44b396d06 100644 --- a/cms/models_test.py +++ b/cms/models_test.py @@ -2,6 +2,7 @@ import json from datetime import date, datetime, timedelta +from decimal import Decimal import factory import pytest @@ -1389,12 +1390,13 @@ def test_certificate_for_course_page(): certificate_page = CertificatePageFactory.create( parent=course_page, product_name="product_name", - CEUs="1.8", + CEUs=Decimal("1.8"), partner_logo__image__title="Partner Logo", signatories__0__signatory__page=signatory, ) assert certificate_page.get_parent() == course_page - assert certificate_page.CEUs == "1.8" + assert certificate_page.CEUs == Decimal("1.8") + assert certificate_page.normalized_ceus == "1.8" assert certificate_page.product_name == "product_name" assert certificate_page.partner_logo.title == "Partner Logo" for signatory in certificate_page.signatories: @@ -1424,13 +1426,14 @@ def test_certificate_for_program_page(): certificate_page = CertificatePageFactory.create( parent=program_page, product_name="product_name", - CEUs="2.8", + CEUs=Decimal("2.8"), partner_logo__image__title="Partner Logo", signatories__0__signatory__page=signatory, ) assert certificate_page.get_parent() == program_page - assert certificate_page.CEUs == "2.8" + assert certificate_page.CEUs == Decimal("2.8") + assert certificate_page.normalized_ceus == "2.8" assert certificate_page.product_name == "product_name" assert certificate_page.partner_logo.title == "Partner Logo" for signatory in certificate_page.signatories: @@ -1989,7 +1992,7 @@ def test_certificatepage_no_signatories_internal_courseware( certificate_page = CertificatePageFactory.create( parent=page, product_name="product_name", - CEUs="2.8", + CEUs=Decimal("2.8"), partner_logo=None, ) @@ -2032,7 +2035,7 @@ def test_certificatepage_with_signatories_internal_courseware( certificate_page = CertificatePageFactory.create( parent=page, product_name="product_name", - CEUs="2.8", + CEUs=Decimal("2.8"), partner_logo=None, signatories__0__signatory__page=signatory, ) @@ -2073,7 +2076,7 @@ def test_certificatepage_saved_no_signatories_external_courseware( certificate_page = CertificatePageFactory.create( parent=page, product_name="product_name", - CEUs="2.8", + CEUs=Decimal("2.8"), partner_logo=None, ) diff --git a/cms/views_test.py b/cms/views_test.py index fea0da444..03b12503b 100644 --- a/cms/views_test.py +++ b/cms/views_test.py @@ -686,6 +686,6 @@ def test_product_page_context_has_certificate( if published_certificate: assert resp.context["ceus"] is not None - assert resp.context["ceus"] == "12.0" + assert resp.context["ceus"] == "12" else: assert resp.context["ceus"] is None diff --git a/courses/credentials.py b/courses/credentials.py index a2a31a621..7d2817e7e 100644 --- a/courses/credentials.py +++ b/courses/credentials.py @@ -38,7 +38,7 @@ def build_program_credential(certificate: ProgramCertificate) -> dict: "name": certificate.program.title, "description": certificate.program.page.description, "numberOfCredits": { - "value": certificate.program.page.certificate_page.CEUs + "value": certificate.program.page.certificate_page.normalized_ceus }, "startDate": start_date.isoformat(), "endDate": end_date.isoformat(), @@ -72,7 +72,7 @@ def build_course_run_credential(certificate: CourseRunCertificate) -> dict: "courseCode": course.readable_id, "name": course.title, "description": course.page.description, - "numberOfCredits": {"value": course.page.certificate_page.CEUs}, + "numberOfCredits": {"value": course.page.certificate_page.normalized_ceus}, "startDate": start_date.isoformat(), "endDate": end_date.isoformat(), }, diff --git a/courses/credentials_test.py b/courses/credentials_test.py index 3a46f671a..f669b51d5 100644 --- a/courses/credentials_test.py +++ b/courses/credentials_test.py @@ -39,7 +39,7 @@ def test_build_program_credential(user): "type": "EducationalOccupationalProgram", "name": program.title, "description": program.page.description, - "numberOfCredits": {"value": program.page.certificate_page.CEUs}, + "numberOfCredits": {"value": program.page.certificate_page.normalized_ceus}, "startDate": course_run.start_date.isoformat(), "endDate": course_run.end_date.isoformat(), }, @@ -92,7 +92,7 @@ def test_build_course_run_credential(): "courseCode": course.readable_id, "name": course.title, "description": course.page.description, - "numberOfCredits": {"value": course.page.certificate_page.CEUs}, + "numberOfCredits": {"value": course.page.certificate_page.normalized_ceus}, "startDate": start_date.isoformat(), "endDate": end_date.isoformat(), }, diff --git a/courses/serializers.py b/courses/serializers.py index f2422ccd1..4313d25c4 100644 --- a/courses/serializers.py +++ b/courses/serializers.py @@ -193,7 +193,7 @@ def get_video_url(self, instance): def get_credits(self, instance): """Returns the credits for this Course""" return ( - instance.page.certificate_page.CEUs + instance.page.certificate_page.normalized_ceus if instance.page and instance.page.certificate_page else None ) @@ -394,7 +394,7 @@ def get_video_url(self, instance): def get_credits(self, instance): """Returns the credits for this Course""" return ( - instance.page.certificate_page.CEUs + instance.page.certificate_page.normalized_ceus if instance.page and instance.page.certificate_page else None ) diff --git a/courses/serializers_test.py b/courses/serializers_test.py index e88148053..a162d1d38 100644 --- a/courses/serializers_test.py +++ b/courses/serializers_test.py @@ -3,6 +3,7 @@ """ from datetime import UTC, datetime, timedelta +from decimal import Decimal import factory import pytest @@ -60,7 +61,7 @@ def test_base_program_serializer(): "2 Months", "2 Hours", "http://www.testvideourl.com", - "2 Test CEUs", + "2.0", "https://www.testexternalcourse1.com", "fb4f5b79-test-4972-92c3-test", ), @@ -156,7 +157,7 @@ def test_serialize_program( # noqa: PLR0913 "duration": duration, "format": program_format, "video_url": video_url, - "credits": ceus, + "credits": str(Decimal(ceus).normalize()) if ceus else None, "is_external": is_external, "external_marketing_url": external_marketing_url, "marketing_hubspot_form_id": marketing_hubspot_form_id, @@ -192,7 +193,7 @@ def test_base_course_serializer(): "2 Months", "2 Hours", "http://www.testvideourl.com", - "2 Test CEUs", + "2", "http://www.testexternalmarketingurl.com", "fb4f5b79-test-4972-92c3-test", ), @@ -290,7 +291,7 @@ def test_serialize_course( # noqa: PLR0913 "duration": duration if course_page else None, "format": course_format if course_page else None, "video_url": video_url if course_page else None, - "credits": ceus if course_page else None, + "credits": str(Decimal(ceus).normalize()) if course_page and ceus else None, "is_external": is_external, "external_marketing_url": external_marketing_url if course_page else None, "marketing_hubspot_form_id": ( diff --git a/courses/sync_external_courses/emeritus_api.py b/courses/sync_external_courses/emeritus_api.py index 8d0dee015..07b65b907 100644 --- a/courses/sync_external_courses/emeritus_api.py +++ b/courses/sync_external_courses/emeritus_api.py @@ -5,6 +5,7 @@ import re import time from datetime import timedelta +from decimal import Decimal from enum import Enum from pathlib import Path @@ -120,7 +121,7 @@ def __init__(self, emeritus_course_json): self.format = emeritus_course_json.get("format") self.category = emeritus_course_json.get("Category", None) self.image_name = emeritus_course_json.get("image_name", None) - self.CEUs = str(emeritus_course_json.get("ceu") or "") + self.CEUs = Decimal(str(emeritus_course_json.get("ceu"))) or None self.learning_outcomes_list = ( parse_emeritus_data_str(emeritus_course_json.get("learning_outcomes")) if emeritus_course_json.get("learning_outcomes") diff --git a/courses/sync_external_courses/emeritus_api_test.py b/courses/sync_external_courses/emeritus_api_test.py index e42e3c286..14438f02c 100644 --- a/courses/sync_external_courses/emeritus_api_test.py +++ b/courses/sync_external_courses/emeritus_api_test.py @@ -6,6 +6,7 @@ import logging import random from datetime import datetime, timedelta +from decimal import Decimal from pathlib import Path import pytest @@ -276,12 +277,12 @@ def test_create_or_update_certificate_page( ) if existing_cert_page: certificate_page = CertificatePageFactory.create( - parent=external_course_page, CEUs="" + parent=external_course_page, CEUs=None ) if publish_certificate: certificate_page.save_revision().publish() if is_live_and_draft: - certificate_page.CEUs = "1.2" + certificate_page.CEUs = Decimal("1.2") certificate_page.save_revision() else: certificate_page.unpublish() @@ -290,7 +291,7 @@ def test_create_or_update_certificate_page( external_course_page, EmeritusCourse(emeritus_course_data) ) certificate_page = certificate_page.revisions.last().as_object() - assert certificate_page.CEUs == emeritus_course_data["ceu"] + assert certificate_page.CEUs == Decimal(str(emeritus_course_data["ceu"])) assert is_created == (not existing_cert_page) assert is_updated == existing_cert_page @@ -481,7 +482,7 @@ def test_update_emeritus_course_runs( # noqa: PLR0915 description="", ) CertificatePageFactory.create( - parent=course_page, CEUs="1.0", partner_logo=None + parent=course_page, CEUs=Decimal("1.0"), partner_logo=None ) product = ProductFactory.create(content_object=course_run) ProductVersionFactory.create(product=product, price=run["list_price"]) @@ -553,7 +554,7 @@ def test_update_emeritus_course_runs( # noqa: PLR0915 CertificatePage ) assert certificate_page - assert certificate_page.CEUs == emeritus_course_run["ceu"] + assert certificate_page.CEUs == Decimal(str(emeritus_course_run["ceu"])) def test_fetch_emeritus_courses_success(settings, mocker): diff --git a/ecommerce/mail_api_test.py b/ecommerce/mail_api_test.py index 3d51707ab..867177c81 100644 --- a/ecommerce/mail_api_test.py +++ b/ecommerce/mail_api_test.py @@ -313,7 +313,7 @@ def test_send_ecommerce_order_receipt(mocker, receipt_data, settings): "start_date": None, "end_date": None, "content_title": "test_run_title", - "CEUs": line.product_version.product.content_object.course.page.certificate_page.CEUs, + "CEUs": line.product_version.product.content_object.course.page.certificate_page.normalized_ceus, } ], "order_total": "100.00", diff --git a/ecommerce/serializers.py b/ecommerce/serializers.py index dedbacadd..772980b18 100644 --- a/ecommerce/serializers.py +++ b/ecommerce/serializers.py @@ -1006,7 +1006,7 @@ def get_lines(self, instance): certificate_page = program.page.certificate_page if certificate_page: - CEUs = certificate_page.CEUs + CEUs = certificate_page.normalized_ceus for override in certificate_page.overrides: if ( override.value.get("readable_id") @@ -1021,7 +1021,7 @@ def get_lines(self, instance): tax_paid=str(tax_paid), discount=str(discount), total_before_tax=str(total_before_tax), - CEUs=str(CEUs) if CEUs else None, + CEUs=CEUs if CEUs else None, **BaseProductVersionSerializer(line.product_version).data, start_date=dates["start_date"], end_date=dates["end_date"], diff --git a/ecommerce/serializers_test.py b/ecommerce/serializers_test.py index ffa768055..d5468c912 100644 --- a/ecommerce/serializers_test.py +++ b/ecommerce/serializers_test.py @@ -465,7 +465,7 @@ def test_serialize_order_receipt(receipt_data): "tax_paid": "0.00", "total_before_tax": str(line.quantity * product_version.price), "quantity": line.quantity, - "CEUs": product_version.product.content_object.course.page.certificate_page.CEUs, + "CEUs": product_version.product.content_object.course.page.certificate_page.normalized_ceus, } ], "order": {