Skip to content

Commit fa03d89

Browse files
chore: create a waffle switch and flag (#4671)
1 parent 8c69de8 commit fa03d89

File tree

6 files changed

+98
-9
lines changed

6 files changed

+98
-9
lines changed

course_discovery/apps/api/v1/views/courses.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434
Collaborator, Course, CourseEditor, CourseEntitlement, CourseRun, CourseType, CourseUrlSlug, Organization, Program,
3535
Seat, Source, Video
3636
)
37+
from course_discovery.apps.course_metadata.toggles import IS_COURSE_RUN_FOR_DUMMY_SKU_GENERATION
3738
from course_discovery.apps.course_metadata.utils import (
38-
create_missing_entitlement, ensure_draft_world, validate_course_number, validate_slug_format
39+
create_missing_entitlement, ensure_draft_world, generate_sku, validate_course_number, validate_slug_format
3940
)
4041
from course_discovery.apps.publisher.utils import is_publisher_user
4142

@@ -213,7 +214,6 @@ def create(self, request, *args, **kwargs):
213214

214215
if error_message:
215216
return Response((_('Incorrect data sent. ') + error_message).strip(), status=status.HTTP_400_BAD_REQUEST)
216-
217217
partner = request.site.partner
218218
course_creation_fields['partner'] = partner.id
219219
course_creation_fields['key'] = self.get_course_key(course_creation_fields)
@@ -240,7 +240,6 @@ def create(self, request, *args, **kwargs):
240240

241241
course = serializer.save(draft=True)
242242
course.set_active_url_slug(url_slug)
243-
244243
organization = Organization.objects.get(key=course_creation_fields['org'])
245244
course.authoring_organizations.add(organization)
246245

@@ -250,6 +249,9 @@ def create(self, request, *args, **kwargs):
250249
course.collaborators.add(*collaborators)
251250

252251
entitlement_types = course.type.entitlement_types.all()
252+
# Waffle switch to control dummy SKU generation logic for 2U purpose.
253+
if IS_COURSE_RUN_FOR_DUMMY_SKU_GENERATION.is_enabled():
254+
generate_sku(partner, course) # Generates a SKU for the provide by entitlements
253255
prices = request.data.get('prices', {})
254256
for entitlement_type in entitlement_types:
255257
CourseEntitlement.objects.create(

course_discovery/apps/course_metadata/models.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@
5757
)
5858
from course_discovery.apps.course_metadata.query import CourseQuerySet, CourseRunQuerySet, ProgramQuerySet
5959
from course_discovery.apps.course_metadata.toggles import (
60-
IS_SUBDIRECTORY_SLUG_FORMAT_ENABLED, IS_SUBDIRECTORY_SLUG_FORMAT_FOR_BOOTCAMP_ENABLED,
61-
IS_SUBDIRECTORY_SLUG_FORMAT_FOR_EXEC_ED_ENABLED
60+
IS_COURSE_RUN_FOR_DUMMY_SKU_GENERATION, IS_SUBDIRECTORY_SLUG_FORMAT_ENABLED,
61+
IS_SUBDIRECTORY_SLUG_FORMAT_FOR_BOOTCAMP_ENABLED, IS_SUBDIRECTORY_SLUG_FORMAT_FOR_EXEC_ED_ENABLED
6262
)
6363
from course_discovery.apps.course_metadata.utils import (
6464
UploadToFieldNamePath, bulk_operation_upload_to_path, clean_query, clear_slug_request_cache_for_course,
65-
custom_render_variations, get_course_run_statuses, get_slug_for_course, is_ocm_course,
65+
custom_render_variations, generate_sku, get_course_run_statuses, get_slug_for_course, is_ocm_course,
6666
push_to_ecommerce_for_course_run, push_tracks_to_lms_for_course_run, set_official_state, subtract_deadline_delta,
6767
validate_ai_languages
6868
)
@@ -2813,12 +2813,13 @@ def get_seat_default_upgrade_deadline(self, seat_type):
28132813
def update_or_create_seat_helper(self, seat_type, prices, upgrade_deadline_override):
28142814
default_deadline = self.get_seat_default_upgrade_deadline(seat_type)
28152815
defaults = {'upgrade_deadline': default_deadline}
2816-
28172816
if seat_type.slug in prices:
28182817
defaults['price'] = prices[seat_type.slug]
28192818
if seat_type.slug == Seat.VERIFIED:
28202819
defaults['upgrade_deadline_override'] = upgrade_deadline_override
2821-
2820+
# Waffle switch to control dummy SKU generation logic for 2U purpose.
2821+
if IS_COURSE_RUN_FOR_DUMMY_SKU_GENERATION.is_enabled():
2822+
generate_sku(None, self) # Generates a SKU for the provide by Seat
28222823
seat, __ = Seat.everything.update_or_create(
28232824
course_run=self,
28242825
type=seat_type,

course_discovery/apps/course_metadata/tests/test_models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,28 @@ def test_verified_upgrade_deadline_reset_on_upgrade_deadline_override_change(sel
17431743
assert verified_seat.upgrade_deadline_override is not None
17441744
assert verified_seat.upgrade_deadline == new_deadline_override
17451745

1746+
@patch('course_discovery.apps.course_metadata.models.IS_COURSE_RUN_FOR_DUMMY_SKU_GENERATION')
1747+
@patch('course_discovery.apps.course_metadata.models.generate_sku')
1748+
def test_generate_sku_called_when_waffle_enabled(self, mock_generate_sku, mock_switch):
1749+
mock_switch.is_enabled.return_value = True # Force switch to be active
1750+
course_run = factories.CourseRunFactory.create(key='course-v1:org1+12+2T2025b')
1751+
factories.SeatFactory.create(course_run=course_run)
1752+
seat_type = SeatType.objects.create(slug='verified')
1753+
prices = {'verified': 500}
1754+
course_run.update_or_create_seat_helper(seat_type, prices, upgrade_deadline_override=None)
1755+
mock_generate_sku.assert_called_once_with(None, course_run)
1756+
1757+
@patch('course_discovery.apps.course_metadata.models.IS_COURSE_RUN_FOR_DUMMY_SKU_GENERATION')
1758+
@patch('course_discovery.apps.course_metadata.models.generate_sku')
1759+
def test_generate_sku_called_when_waffle_disable(self, mock_generate_sku, mock_switch):
1760+
mock_switch.is_enabled.return_value = False # Force switch to be active
1761+
course_run = factories.CourseRunFactory.create(key='course-v1:org1+12+2T2025b')
1762+
factories.SeatFactory.create(course_run=course_run)
1763+
seat_type = SeatType.objects.create(slug='verified')
1764+
prices = {'verified': 500}
1765+
course_run.update_or_create_seat_helper(seat_type, prices, upgrade_deadline_override=None)
1766+
mock_generate_sku.assert_not_called()
1767+
17461768

17471769
class CourseRunTestsThatNeedSetUp(OAuth2Mixin, TestCase):
17481770
"""

course_discovery/apps/course_metadata/tests/test_utils.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from course_discovery.apps.course_metadata.utils import (
4444
calculated_seat_upgrade_deadline, clean_html, convert_svg_to_png_from_url, create_missing_entitlement,
4545
download_and_save_course_image, download_and_save_program_image, ensure_draft_world, fetch_getsmarter_products,
46-
is_google_drive_url, serialize_entitlement_for_ecommerce_api, serialize_seat_for_ecommerce_api,
46+
generate_sku, is_google_drive_url, serialize_entitlement_for_ecommerce_api, serialize_seat_for_ecommerce_api,
4747
transform_skills_data, validate_slug_format
4848
)
4949

@@ -1676,3 +1676,34 @@ def test_validate_slug_format__raise_exception_for_bootcamp_course(self, slug, i
16761676
expected_error_message = expected_error_message.format(url_slug=slug)
16771677
actual_error_message = str(context.exception)
16781678
self.assertIn(expected_error_message, actual_error_message)
1679+
1680+
1681+
@ddt.ddt
1682+
class ValidateDummySKU(TestCase):
1683+
"""
1684+
Test suite for validate generated Dummy SKU by generate_sku method
1685+
"""
1686+
def test_generate_sku_with_partner(self):
1687+
partner = mock.Mock(id=101)
1688+
course = mock.Mock(uuid='abc-uuid')
1689+
sku = generate_sku(partner=partner, course=course)
1690+
self.assertIsInstance(sku, str)
1691+
self.assertEqual(len(sku), 7)
1692+
1693+
def test_generate_sku_without_partner(self):
1694+
course = mock.Mock(uuid='abc-uuid', key='course-key')
1695+
sku = generate_sku(partner=None, course=course)
1696+
self.assertIsInstance(sku, str)
1697+
self.assertEqual(len(sku), 7)
1698+
1699+
def test_generate_sku_invalid_combination(self):
1700+
partner = mock.Mock(id=None)
1701+
course = mock.Mock(uuid='abc-uuid')
1702+
with self.assertRaises(ValidationError) as context:
1703+
generate_sku(partner=partner, course=course)
1704+
self.assertIn("Unexpected combition SKU", str(context.exception))
1705+
1706+
def test_generate_sku_missing_course(self):
1707+
partner = mock.Mock(id=101)
1708+
with self.assertRaises(ValidationError):
1709+
generate_sku(partner=partner, course=None)

course_discovery/apps/course_metadata/toggles.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,14 @@
7878
IS_COURSE_RUN_VARIANT_ID_ECOMMERCE_CONSUMABLE = WaffleSwitch(
7979
'course_metadata.is_course_run_variant_id_ecommerce_consumable', __name__
8080
)
81+
# .. toggle_name: course_metadata.is_dummy_sku_generation
82+
# .. toggle_implementation: WaffleSwitch
83+
# .. toggle_default: False
84+
# .. toggle_description: Enable dummy SKU generation for 2U purposes,
85+
# .. toggle_use_cases: open_edx
86+
# .. toggle_creation_date: 2025-09-16
87+
# .. toggle_target_removal_date: None
88+
# .. toggle_tickets: PROD-4430
89+
IS_COURSE_RUN_FOR_DUMMY_SKU_GENERATION = WaffleSwitch(
90+
'course_metadata.is_dummy_sku_generation', __name__
91+
)

course_discovery/apps/course_metadata/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import string
66
import uuid
7+
from hashlib import md5
78
from tempfile import NamedTemporaryFile
89
from urllib.parse import urljoin, urlparse
910

@@ -1291,3 +1292,24 @@ def bulk_operation_upload_to_path(instance, filename): # pylint: disable=unused
12911292
Utility method used on BulkOperationTask csv_file field to generate unique file names.
12921293
"""
12931294
return f"bulk_operations/uploads/{str(uuid.uuid4())}/{filename}"
1295+
1296+
1297+
def generate_sku(partner=None, course=None):
1298+
"""
1299+
Generates a SKU for the provide by entitlements and seats combination.
1300+
Example: 76E4E71
1301+
"""
1302+
try:
1303+
if partner and getattr(partner, 'id', None) and course is not None:
1304+
_hash = ' '.join((str(course.uuid), str(partner.id))).encode('utf-8')
1305+
logger.info('Initiating SKU generation for the course entitlements.')
1306+
elif partner is None:
1307+
_hash = ' '.join((str(course.uuid), str(course.key))).encode('utf-8')
1308+
logger.info('Initiating SKU generation for the seats.')
1309+
else:
1310+
raise Exception('Unexpected entitlements and seats')
1311+
md5_hash = md5(_hash.lower())
1312+
digest = md5_hash.hexdigest()[-7:]
1313+
return digest.upper()
1314+
except Exception as exc:
1315+
raise ValidationError("Unexpected combition SKU") from exc

0 commit comments

Comments
 (0)