diff --git a/courses/management/commands/create_local_enrollments.py b/courses/management/commands/create_local_enrollments.py index 46ddb8b2c5..c992595e88 100644 --- a/courses/management/commands/create_local_enrollments.py +++ b/courses/management/commands/create_local_enrollments.py @@ -34,15 +34,20 @@ def handle(self, *args, **options): # noqa: ARG002 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/courses/migrations/0013_backfill_paid_courserun.py b/courses/migrations/0013_backfill_paid_courserun.py index a3f48d3297..33b7630aec 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..34cb624292 100644 --- a/ecommerce/serializers/__init__.py +++ b/ecommerce/serializers/__init__.py @@ -567,17 +567,24 @@ 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}") + # 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) + + 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 @@ -701,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): @@ -818,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: @@ -888,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 dc10628858..a62e34d50e 100644 --- a/ecommerce/serializers/v0/__init__.py +++ b/ecommerce/serializers/v0/__init__.py @@ -653,17 +653,27 @@ 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}") + # 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) + + 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 @@ -804,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): @@ -948,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 eab7a156ee..41f93adfff 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,19 @@ 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 +983,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/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(): 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