From bb0683df65127eddf272e1e7c94c728c9b2a786b Mon Sep 17 00:00:00 2001 From: CP Date: Wed, 8 Apr 2026 13:24:00 -0400 Subject: [PATCH 1/7] Use in_bulk for batched DB lookups Replace repeated per-item queries with Model.objects.in_bulk across multiple modules to reduce DB hits and avoid exceptions when related objects are missing. - courses migration: backfill_paidcourserun now collects CourseRun IDs and loads them with in_bulk, skipping missing runs before creating PaidCourseRun entries. - ecommerce serializers: OrderHistorySerializer (two versions) now bulk-loads Product records and looks them up by id, reducing queries and guarding against missing products. - hubspot_sync API: make_*_update_message_list_from_* functions use in_bulk for Users, Orders, Lines, and Products and skip missing records when building request payloads. - users tests: retire_users_test updated to load users via in_bulk (using openedx_users__edx_username as field_name) and assert presence before checks. These changes improve performance and robustness by batching DB access and handling absent referenced objects gracefully. --- .../0013_backfill_paid_courserun.py | 15 ++-- ecommerce/serializers/__init__.py | 25 ++++--- ecommerce/serializers/v0/__init__.py | 25 ++++--- hubspot_sync/api.py | 72 ++++++++++--------- users/management/tests/retire_users_test.py | 12 +++- 5 files changed, 86 insertions(+), 63 deletions(-) diff --git a/courses/migrations/0013_backfill_paid_courserun.py b/courses/migrations/0013_backfill_paid_courserun.py index a3f48d3297..200bbb3243 100644 --- a/courses/migrations/0013_backfill_paid_courserun.py +++ b/courses/migrations/0013_backfill_paid_courserun.py @@ -13,11 +13,16 @@ def backfill_paidcourserun(apps, schema_editor): for order in Order.objects.filter(state__in=["fulfilled", "review"]): # couldn't use order.purchased_runs here from app defined model content_type = ContentType.objects.get_for_model(CourseRun) - for run_obj in order.lines.filter(purchased_content_type=content_type): - course_run = CourseRun.objects.get(pk=run_obj.purchased_object_id) - PaidCourseRun.objects.get_or_create( - order=order, course_run=course_run, user=order.purchaser - ) + run_objects = order.lines.filter(purchased_content_type=content_type) + course_run_ids = [run_obj.purchased_object_id for run_obj in run_objects] + course_runs_by_id = CourseRun.objects.in_bulk(course_run_ids) + + for run_obj in run_objects: + course_run = course_runs_by_id.get(run_obj.purchased_object_id) + if course_run: + PaidCourseRun.objects.get_or_create( + order=order, course_run=course_run, user=order.purchaser + ) class Migration(migrations.Migration): diff --git a/ecommerce/serializers/__init__.py b/ecommerce/serializers/__init__.py index ede1fed06c..6a22e9fd6f 100644 --- a/ecommerce/serializers/__init__.py +++ b/ecommerce/serializers/__init__.py @@ -567,17 +567,20 @@ class OrderHistorySerializer(serializers.ModelSerializer): @extend_schema_field(serializers.ListField) def get_titles(self, instance): titles = [] - - for line in instance.lines.all(): - product = models.Product.all_objects.get( - pk=line.product_version.field_dict["id"] - ) - if product.content_type.model == "courserun": - titles.append(product.purchasable_object.course.title) - elif product.content_type.model == "programrun": - titles.append(product.description) - else: - titles.append(f"No Title - {product.id}") + lines = instance.lines.all() + product_ids = [line.product_version.field_dict["id"] for line in lines] + products_by_id = models.Product.all_objects.in_bulk(product_ids) + + for line in lines: + product_id = line.product_version.field_dict["id"] + product = products_by_id.get(product_id) + if product: + if product.content_type.model == "courserun": + titles.append(product.purchasable_object.course.title) + elif product.content_type.model == "programrun": + titles.append(product.description) + else: + titles.append(f"No Title - {product.id}") return titles diff --git a/ecommerce/serializers/v0/__init__.py b/ecommerce/serializers/v0/__init__.py index be18ffd375..9af27ad8e3 100644 --- a/ecommerce/serializers/v0/__init__.py +++ b/ecommerce/serializers/v0/__init__.py @@ -644,17 +644,20 @@ class OrderHistorySerializer(serializers.ModelSerializer): @extend_schema_field(serializers.ListField) def get_titles(self, instance): titles = [] - - for line in instance.lines.all(): - product = models.Product.all_objects.get( - pk=line.product_version.field_dict["id"] - ) - if product.content_type.model == "courserun" and product.purchasable_object: - titles.append(product.purchasable_object.course.title) - elif product.content_type.model == "programrun": - titles.append(product.description) - else: - titles.append(f"No Title - {product.id}") + lines = instance.lines.all() + product_ids = [line.product_version.field_dict["id"] for line in lines] + products_by_id = models.Product.all_objects.in_bulk(product_ids) + + for line in lines: + product_id = line.product_version.field_dict["id"] + product = products_by_id.get(product_id) + if product: + if product.content_type.model == "courserun" and product.purchasable_object: + titles.append(product.purchasable_object.course.title) + elif product.content_type.model == "programrun": + titles.append(product.description) + else: + titles.append(f"No Title - {product.id}") return titles diff --git a/hubspot_sync/api.py b/hubspot_sync/api.py index eab7a156ee..73936c0964 100644 --- a/hubspot_sync/api.py +++ b/hubspot_sync/api.py @@ -770,16 +770,17 @@ def make_contact_update_message_list_from_user_ids( List[dict]: List of dictionaries containing User properties. """ chunk_dictionary = dict(chunk) - users = User.objects.filter(id__in=chunk_dictionary.keys()) + users_by_id = User.objects.in_bulk(chunk_dictionary.keys()) request_input = [] for user_id, hubspot_id in chunk_dictionary.items(): - user = users.filter(id=user_id).first() - request_input.append( - { - "id": hubspot_id, - "properties": make_contact_sync_message_from_user(user).properties, - } - ) + user = users_by_id.get(user_id) + if user: + request_input.append( + { + "id": hubspot_id, + "properties": make_contact_sync_message_from_user(user).properties, + } + ) return request_input @@ -856,16 +857,17 @@ def make_deal_update_message_list_from_order_ids( List[dict]: List of dictionaries containing Order properties. """ chunk_dictionary = dict(chunk) - orders = Order.objects.filter(id__in=chunk_dictionary.keys()) + orders_by_id = Order.objects.in_bulk(chunk_dictionary.keys()) request_input = [] for order_id, hubspot_id in chunk_dictionary.items(): - order = orders.filter(id=order_id).first() - request_input.append( - { - "id": hubspot_id, - "properties": make_deal_sync_message_from_order(order).properties, - } - ) + order = orders_by_id.get(order_id) + if order: + request_input.append( + { + "id": hubspot_id, + "properties": make_deal_sync_message_from_order(order).properties, + } + ) return request_input @@ -917,16 +919,17 @@ def make_line_item_update_message_list_from_line_ids( List[dict]: List of dictionaries containing Line properties. """ chunk_dictionary = dict(chunk) - lines = Line.objects.filter(id__in=chunk_dictionary.keys()) + lines_by_id = Line.objects.in_bulk(chunk_dictionary.keys()) request_input = [] for line_id, hubspot_id in chunk_dictionary.items(): - line = lines.filter(id=line_id).first() - request_input.append( - { - "id": hubspot_id, - "properties": make_line_item_sync_message_from_line(line).properties, - } - ) + line = lines_by_id.get(line_id) + if line: + request_input.append( + { + "id": hubspot_id, + "properties": make_line_item_sync_message_from_line(line).properties, + } + ) return request_input @@ -978,18 +981,19 @@ def make_product_update_message_list_from_product_ids( List[dict]: List of dictionaries containing Product properties. """ chunk_dictionary = dict(chunk) - products = Product.objects.filter(id__in=chunk_dictionary.keys()) + products_by_id = Product.objects.in_bulk(chunk_dictionary.keys()) request_input = [] for product_id, hubspot_id in chunk_dictionary.items(): - product = products.filter(id=product_id).first() - request_input.append( - { - "id": hubspot_id, - "properties": make_product_sync_message_from_product( - product - ).properties, - } - ) + product = products_by_id.get(product_id) + if product: + request_input.append( + { + "id": hubspot_id, + "properties": make_product_sync_message_from_product( + product + ).properties, + } + ) return request_input diff --git a/users/management/tests/retire_users_test.py b/users/management/tests/retire_users_test.py index 8a14f254f9..da0dfa781c 100644 --- a/users/management/tests/retire_users_test.py +++ b/users/management/tests/retire_users_test.py @@ -55,8 +55,12 @@ def test_multiple_success(mocker): COMMAND.handle("retire_users", users=test_usernames) + users_by_username = User.objects.in_bulk( + test_usernames, field_name="openedx_users__edx_username" + ) for user_name in test_usernames: - user = User.objects.get(openedx_users__edx_username=user_name) + user = users_by_username.get(user_name) + assert user is not None assert user.is_active is False assert "retired_email" in user.email mock_bulk_retire_edx_users.assert_called() @@ -129,8 +133,12 @@ def test_multiple_success_blocking_user(mocker): COMMAND.handle("retire_users", users=test_usernames, block_users=True) + users_by_username = User.objects.in_bulk( + test_usernames, field_name="openedx_users__edx_username" + ) for user_name in test_usernames: - user = User.objects.get(openedx_users__edx_username=user_name) + user = users_by_username.get(user_name) + assert user is not None assert user.is_active is False assert "retired_email" in user.email From e629003fa3993832961b0ec7e9c962be06a980b0 Mon Sep 17 00:00:00 2001 From: CP Date: Wed, 8 Apr 2026 13:27:10 -0400 Subject: [PATCH 2/7] Use in_bulk for CourseRun and Order lookups Replace per-iteration DB queries with in_bulk lookups to reduce database hits and improve performance. In create_local_enrollments, load CourseRun objects via in_bulk by courseware_id and use the map instead of CourseRun.filter(...).first(); skip missing runs with a logged error. In hubspot_sync.tasks, load Orders via in_bulk and use the map instead of querying Order.objects.get(...) on each loop iteration; skip missing orders to avoid exceptions. These changes reduce N+1 queries and add safer handling for missing records. --- courses/management/commands/create_local_enrollments.py | 7 ++++++- hubspot_sync/tasks.py | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/courses/management/commands/create_local_enrollments.py b/courses/management/commands/create_local_enrollments.py index 46ddb8b2c5..1ffa3b3546 100644 --- a/courses/management/commands/create_local_enrollments.py +++ b/courses/management/commands/create_local_enrollments.py @@ -33,16 +33,21 @@ def handle(self, *args, **options): # noqa: ARG002 courseware_ids = options["runs"] edx_client = get_edx_api_service_client() + + runs_by_courseware_id = CourseRun.objects.in_bulk( + courseware_ids, field_name="courseware_id" + ) created_count = {} for courseware_id in courseware_ids: - run = CourseRun.objects.filter(courseware_id=courseware_id).first() + run = runs_by_courseware_id.get(courseware_id) if run is None: self.stderr.write( self.style.ERROR( f"Could not find course run with courseware_id={courseware_id}" ) ) + continue edx_enrollments = edx_client.enrollments.get_enrollments( course_id=courseware_id diff --git a/hubspot_sync/tasks.py b/hubspot_sync/tasks.py index af2366a11a..879f8a8602 100644 --- a/hubspot_sync/tasks.py +++ b/hubspot_sync/tasks.py @@ -500,8 +500,11 @@ def batch_upsert_associations_chunked(order_ids: List[int]): # noqa: UP006 line_associations_batch = [] hubspot_client = HubspotApi() deal_count = len(order_ids) + deals_by_id = Order.objects.in_bulk(order_ids) for idx, order_id in enumerate(order_ids): - deal = Order.objects.get(id=order_id) + deal = deals_by_id.get(order_id) + if not deal: + continue contact_id = get_hubspot_id_for_object(deal.purchaser) deal_id = get_hubspot_id_for_object(deal) for line in deal.lines.iterator(): From 1a38dd6b72083d284212e26a6d97ac4e944d68be Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:36:52 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- courses/management/commands/create_local_enrollments.py | 2 +- courses/migrations/0013_backfill_paid_courserun.py | 2 +- ecommerce/serializers/v0/__init__.py | 5 ++++- hubspot_sync/api.py | 4 +++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/courses/management/commands/create_local_enrollments.py b/courses/management/commands/create_local_enrollments.py index 1ffa3b3546..c992595e88 100644 --- a/courses/management/commands/create_local_enrollments.py +++ b/courses/management/commands/create_local_enrollments.py @@ -33,7 +33,7 @@ def handle(self, *args, **options): # noqa: ARG002 courseware_ids = options["runs"] edx_client = get_edx_api_service_client() - + runs_by_courseware_id = CourseRun.objects.in_bulk( courseware_ids, field_name="courseware_id" ) diff --git a/courses/migrations/0013_backfill_paid_courserun.py b/courses/migrations/0013_backfill_paid_courserun.py index 200bbb3243..33b7630aec 100644 --- a/courses/migrations/0013_backfill_paid_courserun.py +++ b/courses/migrations/0013_backfill_paid_courserun.py @@ -16,7 +16,7 @@ def backfill_paidcourserun(apps, schema_editor): run_objects = order.lines.filter(purchased_content_type=content_type) course_run_ids = [run_obj.purchased_object_id for run_obj in run_objects] course_runs_by_id = CourseRun.objects.in_bulk(course_run_ids) - + for run_obj in run_objects: course_run = course_runs_by_id.get(run_obj.purchased_object_id) if course_run: diff --git a/ecommerce/serializers/v0/__init__.py b/ecommerce/serializers/v0/__init__.py index 1755cd61b6..5d41fe6d27 100644 --- a/ecommerce/serializers/v0/__init__.py +++ b/ecommerce/serializers/v0/__init__.py @@ -661,7 +661,10 @@ def get_titles(self, instance): product_id = line.product_version.field_dict["id"] product = products_by_id.get(product_id) if product: - if product.content_type.model == "courserun" and product.purchasable_object: + if ( + product.content_type.model == "courserun" + and product.purchasable_object + ): titles.append(product.purchasable_object.course.title) elif product.content_type.model == "programrun": titles.append(product.description) diff --git a/hubspot_sync/api.py b/hubspot_sync/api.py index 73936c0964..41f93adfff 100644 --- a/hubspot_sync/api.py +++ b/hubspot_sync/api.py @@ -927,7 +927,9 @@ def make_line_item_update_message_list_from_line_ids( request_input.append( { "id": hubspot_id, - "properties": make_line_item_sync_message_from_line(line).properties, + "properties": make_line_item_sync_message_from_line( + line + ).properties, } ) return request_input From 54eb7547a1eea933778c5948138fd64ec86f40d3 Mon Sep 17 00:00:00 2001 From: CP Date: Fri, 10 Apr 2026 14:48:41 -0400 Subject: [PATCH 4/7] pre-commit --- .../commands/create_local_enrollments.py | 2 +- .../migrations/0013_backfill_paid_courserun.py | 2 +- ecommerce/serializers/__init__.py | 17 +++++++++++++---- ecommerce/serializers/v0/__init__.py | 18 ++++++++++++++---- ecommerce/views/legacy/__init__.py | 6 ++++++ ecommerce/views/v0/__init__.py | 6 ++++++ hubspot_sync/api.py | 4 +++- 7 files changed, 44 insertions(+), 11 deletions(-) diff --git a/courses/management/commands/create_local_enrollments.py b/courses/management/commands/create_local_enrollments.py index 1ffa3b3546..c992595e88 100644 --- a/courses/management/commands/create_local_enrollments.py +++ b/courses/management/commands/create_local_enrollments.py @@ -33,7 +33,7 @@ def handle(self, *args, **options): # noqa: ARG002 courseware_ids = options["runs"] edx_client = get_edx_api_service_client() - + runs_by_courseware_id = CourseRun.objects.in_bulk( courseware_ids, field_name="courseware_id" ) diff --git a/courses/migrations/0013_backfill_paid_courserun.py b/courses/migrations/0013_backfill_paid_courserun.py index 200bbb3243..33b7630aec 100644 --- a/courses/migrations/0013_backfill_paid_courserun.py +++ b/courses/migrations/0013_backfill_paid_courserun.py @@ -16,7 +16,7 @@ def backfill_paidcourserun(apps, schema_editor): run_objects = order.lines.filter(purchased_content_type=content_type) course_run_ids = [run_obj.purchased_object_id for run_obj in run_objects] course_runs_by_id = CourseRun.objects.in_bulk(course_run_ids) - + for run_obj in run_objects: course_run = course_runs_by_id.get(run_obj.purchased_object_id) if course_run: diff --git a/ecommerce/serializers/__init__.py b/ecommerce/serializers/__init__.py index 6a22e9fd6f..1e627853e1 100644 --- a/ecommerce/serializers/__init__.py +++ b/ecommerce/serializers/__init__.py @@ -567,7 +567,8 @@ class OrderHistorySerializer(serializers.ModelSerializer): @extend_schema_field(serializers.ListField) def get_titles(self, instance): titles = [] - lines = instance.lines.all() + # Use prefetched lines data + lines = [line for line in instance.lines.all()] product_ids = [line.product_version.field_dict["id"] for line in lines] products_by_id = models.Product.all_objects.in_bulk(product_ids) @@ -704,7 +705,11 @@ def to_representation(self, instance): if not isinstance(instance, Order): raise AttributeError # noqa: TRY004 - transaction = instance.transactions.order_by("-created_on").first() + # Use prefetched transactions data + transactions = [t for t in instance.transactions.all()] + transaction = ( + max(transactions, key=lambda t: t.created_on) if transactions else None + ) return transaction # noqa: RET504 @@ -821,7 +826,9 @@ class Meta: class TransactionLineSerializer(serializers.BaseSerializer): def to_representation(self, instance): - coupon_redemption = instance.order.discounts.first() + # Use prefetched discounts data + discounts = [d for d in instance.order.discounts.all()] + coupon_redemption = discounts[0] if discounts else None discount = 0.0 if coupon_redemption: @@ -891,7 +898,9 @@ def get_order(self, instance): def get_coupon(self, instance): """Get discount code from the discount redemption if available""" - coupon_redemption = instance.discounts.first() + # Use prefetched discounts data + discounts = [d for d in instance.discounts.all()] + coupon_redemption = discounts[0] if discounts else None if not coupon_redemption: return None return DiscountRedemptionSerializer(coupon_redemption).data diff --git a/ecommerce/serializers/v0/__init__.py b/ecommerce/serializers/v0/__init__.py index 1755cd61b6..8e06fbba1f 100644 --- a/ecommerce/serializers/v0/__init__.py +++ b/ecommerce/serializers/v0/__init__.py @@ -653,7 +653,8 @@ class OrderHistorySerializer(serializers.ModelSerializer): @extend_schema_field(serializers.ListField) def get_titles(self, instance): titles = [] - lines = instance.lines.all() + # Use prefetched lines data + lines = [line for line in instance.lines.all()] product_ids = [line.product_version.field_dict["id"] for line in lines] products_by_id = models.Product.all_objects.in_bulk(product_ids) @@ -661,7 +662,10 @@ def get_titles(self, instance): product_id = line.product_version.field_dict["id"] product = products_by_id.get(product_id) if product: - if product.content_type.model == "courserun" and product.purchasable_object: + if ( + product.content_type.model == "courserun" + and product.purchasable_object + ): titles.append(product.purchasable_object.course.title) elif product.content_type.model == "programrun": titles.append(product.description) @@ -807,7 +811,11 @@ def to_representation(self, instance): if not isinstance(instance, Order): raise AttributeError # noqa: TRY004 - transaction = instance.transactions.order_by("-created_on").first() + # Use prefetched transactions data + transactions = [t for t in instance.transactions.all()] + transaction = ( + max(transactions, key=lambda t: t.created_on) if transactions else None + ) return transaction # noqa: RET504 @@ -951,7 +959,9 @@ def get_order(self, instance): def get_coupon(self, instance): """Get discount code from the discount redemption if available""" - coupon_redemption = instance.discounts.first() + # Use prefetched discounts data + discounts = [d for d in instance.discounts.all()] + coupon_redemption = discounts[0] if discounts else None if not coupon_redemption: return None return DiscountRedemptionSerializer(coupon_redemption).data diff --git a/ecommerce/views/legacy/__init__.py b/ecommerce/views/legacy/__init__.py index 6e19388fd1..f2117d253e 100644 --- a/ecommerce/views/legacy/__init__.py +++ b/ecommerce/views/legacy/__init__.py @@ -1065,6 +1065,12 @@ def get_queryset(self): Order.objects.filter(purchaser=self.request.user) .filter(state__in=[OrderStatus.FULFILLED, OrderStatus.REFUNDED]) .order_by("-created_on") + .prefetch_related( + "lines__product__purchasable_object__course", + "lines__product__content_type", + "transactions", + "discounts__discount", + ) .all() ) diff --git a/ecommerce/views/v0/__init__.py b/ecommerce/views/v0/__init__.py index 02321b59bd..4b68c5a089 100644 --- a/ecommerce/views/v0/__init__.py +++ b/ecommerce/views/v0/__init__.py @@ -889,6 +889,12 @@ def get_queryset(self): Order.objects.filter(purchaser=self.request.user) .filter(state__in=[OrderStatus.FULFILLED, OrderStatus.REFUNDED]) .order_by("-created_on") + .prefetch_related( + "lines__product__purchasable_object__course", + "lines__product__content_type", + "transactions", + "discounts__discount", + ) .all() ) diff --git a/hubspot_sync/api.py b/hubspot_sync/api.py index 73936c0964..41f93adfff 100644 --- a/hubspot_sync/api.py +++ b/hubspot_sync/api.py @@ -927,7 +927,9 @@ def make_line_item_update_message_list_from_line_ids( request_input.append( { "id": hubspot_id, - "properties": make_line_item_sync_message_from_line(line).properties, + "properties": make_line_item_sync_message_from_line( + line + ).properties, } ) return request_input From 547894c10d1b0b9fe1738f118148187ccc214dd3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:50:08 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ecommerce/serializers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecommerce/serializers/__init__.py b/ecommerce/serializers/__init__.py index 1e627853e1..5bc371b97d 100644 --- a/ecommerce/serializers/__init__.py +++ b/ecommerce/serializers/__init__.py @@ -705,7 +705,7 @@ def to_representation(self, instance): if not isinstance(instance, Order): raise AttributeError # noqa: TRY004 - # Use prefetched transactions data + # Use prefetched transactions data transactions = [t for t in instance.transactions.all()] transaction = ( max(transactions, key=lambda t: t.created_on) if transactions else None From 6f9ec41344793478a6530a7a1ea596b64d460395 Mon Sep 17 00:00:00 2001 From: CP Date: Fri, 10 Apr 2026 14:53:47 -0400 Subject: [PATCH 6/7] Revert "pre-commit" This reverts commit 54eb7547a1eea933778c5948138fd64ec86f40d3. --- .../commands/create_local_enrollments.py | 2 +- .../migrations/0013_backfill_paid_courserun.py | 2 +- ecommerce/serializers/__init__.py | 17 ++++------------- ecommerce/serializers/v0/__init__.py | 18 ++++-------------- ecommerce/views/legacy/__init__.py | 6 ------ ecommerce/views/v0/__init__.py | 6 ------ hubspot_sync/api.py | 4 +--- 7 files changed, 11 insertions(+), 44 deletions(-) diff --git a/courses/management/commands/create_local_enrollments.py b/courses/management/commands/create_local_enrollments.py index c992595e88..1ffa3b3546 100644 --- a/courses/management/commands/create_local_enrollments.py +++ b/courses/management/commands/create_local_enrollments.py @@ -33,7 +33,7 @@ def handle(self, *args, **options): # noqa: ARG002 courseware_ids = options["runs"] edx_client = get_edx_api_service_client() - + runs_by_courseware_id = CourseRun.objects.in_bulk( courseware_ids, field_name="courseware_id" ) diff --git a/courses/migrations/0013_backfill_paid_courserun.py b/courses/migrations/0013_backfill_paid_courserun.py index 33b7630aec..200bbb3243 100644 --- a/courses/migrations/0013_backfill_paid_courserun.py +++ b/courses/migrations/0013_backfill_paid_courserun.py @@ -16,7 +16,7 @@ def backfill_paidcourserun(apps, schema_editor): run_objects = order.lines.filter(purchased_content_type=content_type) course_run_ids = [run_obj.purchased_object_id for run_obj in run_objects] course_runs_by_id = CourseRun.objects.in_bulk(course_run_ids) - + for run_obj in run_objects: course_run = course_runs_by_id.get(run_obj.purchased_object_id) if course_run: diff --git a/ecommerce/serializers/__init__.py b/ecommerce/serializers/__init__.py index 1e627853e1..6a22e9fd6f 100644 --- a/ecommerce/serializers/__init__.py +++ b/ecommerce/serializers/__init__.py @@ -567,8 +567,7 @@ class OrderHistorySerializer(serializers.ModelSerializer): @extend_schema_field(serializers.ListField) def get_titles(self, instance): titles = [] - # Use prefetched lines data - lines = [line for line in instance.lines.all()] + lines = instance.lines.all() product_ids = [line.product_version.field_dict["id"] for line in lines] products_by_id = models.Product.all_objects.in_bulk(product_ids) @@ -705,11 +704,7 @@ def to_representation(self, instance): if not isinstance(instance, Order): raise AttributeError # noqa: TRY004 - # Use prefetched transactions data - transactions = [t for t in instance.transactions.all()] - transaction = ( - max(transactions, key=lambda t: t.created_on) if transactions else None - ) + transaction = instance.transactions.order_by("-created_on").first() return transaction # noqa: RET504 @@ -826,9 +821,7 @@ class Meta: class TransactionLineSerializer(serializers.BaseSerializer): def to_representation(self, instance): - # Use prefetched discounts data - discounts = [d for d in instance.order.discounts.all()] - coupon_redemption = discounts[0] if discounts else None + coupon_redemption = instance.order.discounts.first() discount = 0.0 if coupon_redemption: @@ -898,9 +891,7 @@ def get_order(self, instance): def get_coupon(self, instance): """Get discount code from the discount redemption if available""" - # Use prefetched discounts data - discounts = [d for d in instance.discounts.all()] - coupon_redemption = discounts[0] if discounts else None + coupon_redemption = instance.discounts.first() if not coupon_redemption: return None return DiscountRedemptionSerializer(coupon_redemption).data diff --git a/ecommerce/serializers/v0/__init__.py b/ecommerce/serializers/v0/__init__.py index 8e06fbba1f..1755cd61b6 100644 --- a/ecommerce/serializers/v0/__init__.py +++ b/ecommerce/serializers/v0/__init__.py @@ -653,8 +653,7 @@ class OrderHistorySerializer(serializers.ModelSerializer): @extend_schema_field(serializers.ListField) def get_titles(self, instance): titles = [] - # Use prefetched lines data - lines = [line for line in instance.lines.all()] + lines = instance.lines.all() product_ids = [line.product_version.field_dict["id"] for line in lines] products_by_id = models.Product.all_objects.in_bulk(product_ids) @@ -662,10 +661,7 @@ def get_titles(self, instance): product_id = line.product_version.field_dict["id"] product = products_by_id.get(product_id) if product: - if ( - product.content_type.model == "courserun" - and product.purchasable_object - ): + if product.content_type.model == "courserun" and product.purchasable_object: titles.append(product.purchasable_object.course.title) elif product.content_type.model == "programrun": titles.append(product.description) @@ -811,11 +807,7 @@ def to_representation(self, instance): if not isinstance(instance, Order): raise AttributeError # noqa: TRY004 - # Use prefetched transactions data - transactions = [t for t in instance.transactions.all()] - transaction = ( - max(transactions, key=lambda t: t.created_on) if transactions else None - ) + transaction = instance.transactions.order_by("-created_on").first() return transaction # noqa: RET504 @@ -959,9 +951,7 @@ def get_order(self, instance): def get_coupon(self, instance): """Get discount code from the discount redemption if available""" - # Use prefetched discounts data - discounts = [d for d in instance.discounts.all()] - coupon_redemption = discounts[0] if discounts else None + coupon_redemption = instance.discounts.first() if not coupon_redemption: return None return DiscountRedemptionSerializer(coupon_redemption).data diff --git a/ecommerce/views/legacy/__init__.py b/ecommerce/views/legacy/__init__.py index f2117d253e..6e19388fd1 100644 --- a/ecommerce/views/legacy/__init__.py +++ b/ecommerce/views/legacy/__init__.py @@ -1065,12 +1065,6 @@ def get_queryset(self): Order.objects.filter(purchaser=self.request.user) .filter(state__in=[OrderStatus.FULFILLED, OrderStatus.REFUNDED]) .order_by("-created_on") - .prefetch_related( - "lines__product__purchasable_object__course", - "lines__product__content_type", - "transactions", - "discounts__discount", - ) .all() ) diff --git a/ecommerce/views/v0/__init__.py b/ecommerce/views/v0/__init__.py index 4b68c5a089..02321b59bd 100644 --- a/ecommerce/views/v0/__init__.py +++ b/ecommerce/views/v0/__init__.py @@ -889,12 +889,6 @@ def get_queryset(self): Order.objects.filter(purchaser=self.request.user) .filter(state__in=[OrderStatus.FULFILLED, OrderStatus.REFUNDED]) .order_by("-created_on") - .prefetch_related( - "lines__product__purchasable_object__course", - "lines__product__content_type", - "transactions", - "discounts__discount", - ) .all() ) diff --git a/hubspot_sync/api.py b/hubspot_sync/api.py index 41f93adfff..73936c0964 100644 --- a/hubspot_sync/api.py +++ b/hubspot_sync/api.py @@ -927,9 +927,7 @@ def make_line_item_update_message_list_from_line_ids( request_input.append( { "id": hubspot_id, - "properties": make_line_item_sync_message_from_line( - line - ).properties, + "properties": make_line_item_sync_message_from_line(line).properties, } ) return request_input From b53edde2d8cc489b4acfbf4db1c4af96e903dcc6 Mon Sep 17 00:00:00 2001 From: CP Date: Sat, 11 Apr 2026 07:24:32 -0400 Subject: [PATCH 7/7] hold --- .../commands/create_local_enrollments.py | 2 +- .../0013_backfill_paid_courserun.py | 2 +- ecommerce/serializers/__init__.py | 31 ++++++++++++++++--- ecommerce/serializers/v0/__init__.py | 28 ++++++++++++++--- ecommerce/views/legacy/__init__.py | 11 ++++++- ecommerce/views/v0/__init__.py | 11 ++++++- hubspot_sync/api.py | 4 ++- 7 files changed, 74 insertions(+), 15 deletions(-) diff --git a/courses/management/commands/create_local_enrollments.py b/courses/management/commands/create_local_enrollments.py index 1ffa3b3546..c992595e88 100644 --- a/courses/management/commands/create_local_enrollments.py +++ b/courses/management/commands/create_local_enrollments.py @@ -33,7 +33,7 @@ def handle(self, *args, **options): # noqa: ARG002 courseware_ids = options["runs"] edx_client = get_edx_api_service_client() - + runs_by_courseware_id = CourseRun.objects.in_bulk( courseware_ids, field_name="courseware_id" ) diff --git a/courses/migrations/0013_backfill_paid_courserun.py b/courses/migrations/0013_backfill_paid_courserun.py index 200bbb3243..33b7630aec 100644 --- a/courses/migrations/0013_backfill_paid_courserun.py +++ b/courses/migrations/0013_backfill_paid_courserun.py @@ -16,7 +16,7 @@ def backfill_paidcourserun(apps, schema_editor): run_objects = order.lines.filter(purchased_content_type=content_type) course_run_ids = [run_obj.purchased_object_id for run_obj in run_objects] course_runs_by_id = CourseRun.objects.in_bulk(course_run_ids) - + for run_obj in run_objects: course_run = course_runs_by_id.get(run_obj.purchased_object_id) if course_run: diff --git a/ecommerce/serializers/__init__.py b/ecommerce/serializers/__init__.py index 6a22e9fd6f..34cb624292 100644 --- a/ecommerce/serializers/__init__.py +++ b/ecommerce/serializers/__init__.py @@ -567,7 +567,11 @@ class OrderHistorySerializer(serializers.ModelSerializer): @extend_schema_field(serializers.ListField) def get_titles(self, instance): titles = [] - lines = instance.lines.all() + # Access prefetched lines data + lines = ( + getattr(instance, "_prefetched_objects_cache", {}).get("lines") + or instance.lines.all() + ) product_ids = [line.product_version.field_dict["id"] for line in lines] products_by_id = models.Product.all_objects.in_bulk(product_ids) @@ -704,9 +708,18 @@ def to_representation(self, instance): if not isinstance(instance, Order): raise AttributeError # noqa: TRY004 - transaction = instance.transactions.order_by("-created_on").first() + # Access prefetched transactions + transactions = getattr(instance, "_prefetched_objects_cache", {}).get( + "transactions" + ) + if transactions: + transaction = ( + max(transactions, key=lambda t: t.created_on) if transactions else None + ) + else: + transaction = instance.transactions.order_by("-created_on").first() - return transaction # noqa: RET504 + return transaction class TransactionPurchaseSerializer(TransactionDataSerializer): @@ -821,7 +834,13 @@ class Meta: class TransactionLineSerializer(serializers.BaseSerializer): def to_representation(self, instance): - coupon_redemption = instance.order.discounts.first() + # Access prefetched discounts + discounts = getattr(instance.order, "_prefetched_objects_cache", {}).get( + "discounts" + ) + coupon_redemption = ( + discounts[0] if discounts else instance.order.discounts.first() + ) discount = 0.0 if coupon_redemption: @@ -891,7 +910,9 @@ def get_order(self, instance): def get_coupon(self, instance): """Get discount code from the discount redemption if available""" - coupon_redemption = instance.discounts.first() + # Access prefetched discounts + discounts = getattr(instance, "_prefetched_objects_cache", {}).get("discounts") + coupon_redemption = discounts[0] if discounts else instance.discounts.first() if not coupon_redemption: return None return DiscountRedemptionSerializer(coupon_redemption).data diff --git a/ecommerce/serializers/v0/__init__.py b/ecommerce/serializers/v0/__init__.py index 1755cd61b6..a62e34d50e 100644 --- a/ecommerce/serializers/v0/__init__.py +++ b/ecommerce/serializers/v0/__init__.py @@ -653,7 +653,11 @@ class OrderHistorySerializer(serializers.ModelSerializer): @extend_schema_field(serializers.ListField) def get_titles(self, instance): titles = [] - lines = instance.lines.all() + # Access prefetched lines data + lines = ( + getattr(instance, "_prefetched_objects_cache", {}).get("lines") + or instance.lines.all() + ) product_ids = [line.product_version.field_dict["id"] for line in lines] products_by_id = models.Product.all_objects.in_bulk(product_ids) @@ -661,7 +665,10 @@ def get_titles(self, instance): product_id = line.product_version.field_dict["id"] product = products_by_id.get(product_id) if product: - if product.content_type.model == "courserun" and product.purchasable_object: + if ( + product.content_type.model == "courserun" + and product.purchasable_object + ): titles.append(product.purchasable_object.course.title) elif product.content_type.model == "programrun": titles.append(product.description) @@ -807,9 +814,18 @@ def to_representation(self, instance): if not isinstance(instance, Order): raise AttributeError # noqa: TRY004 - transaction = instance.transactions.order_by("-created_on").first() + # Access prefetched transactions + transactions = getattr(instance, "_prefetched_objects_cache", {}).get( + "transactions" + ) + if transactions: + transaction = ( + max(transactions, key=lambda t: t.created_on) if transactions else None + ) + else: + transaction = instance.transactions.order_by("-created_on").first() - return transaction # noqa: RET504 + return transaction class TransactionPurchaseSerializer(TransactionDataSerializer): @@ -951,7 +967,9 @@ def get_order(self, instance): def get_coupon(self, instance): """Get discount code from the discount redemption if available""" - coupon_redemption = instance.discounts.first() + # Access prefetched discounts + discounts = getattr(instance, "_prefetched_objects_cache", {}).get("discounts") + coupon_redemption = discounts[0] if discounts else instance.discounts.first() if not coupon_redemption: return None return DiscountRedemptionSerializer(coupon_redemption).data diff --git a/ecommerce/views/legacy/__init__.py b/ecommerce/views/legacy/__init__.py index 6e19388fd1..f85c8fa13d 100644 --- a/ecommerce/views/legacy/__init__.py +++ b/ecommerce/views/legacy/__init__.py @@ -1074,4 +1074,13 @@ class OrderReceiptView(RetrieveAPIView): permission_classes = [IsAuthenticated] def get_queryset(self): - return Order.objects.filter(purchaser=self.request.user).all() + return ( + Order.objects.filter(purchaser=self.request.user) + .prefetch_related( + "lines__product__purchasable_object__course", + "lines__product__content_type", + "transactions", + "discounts__discount", + ) + .all() + ) diff --git a/ecommerce/views/v0/__init__.py b/ecommerce/views/v0/__init__.py index 02321b59bd..7d1565b003 100644 --- a/ecommerce/views/v0/__init__.py +++ b/ecommerce/views/v0/__init__.py @@ -901,4 +901,13 @@ class OrderReceiptView(RetrieveAPIView): def get_queryset(self): """Return only the user's orders""" - return Order.objects.filter(purchaser=self.request.user).all() + return ( + Order.objects.filter(purchaser=self.request.user) + .prefetch_related( + "lines__product__purchasable_object__course", + "lines__product__content_type", + "transactions", + "discounts__discount", + ) + .all() + ) diff --git a/hubspot_sync/api.py b/hubspot_sync/api.py index 73936c0964..41f93adfff 100644 --- a/hubspot_sync/api.py +++ b/hubspot_sync/api.py @@ -927,7 +927,9 @@ def make_line_item_update_message_list_from_line_ids( request_input.append( { "id": hubspot_id, - "properties": make_line_item_sync_message_from_line(line).properties, + "properties": make_line_item_sync_message_from_line( + line + ).properties, } ) return request_input