From 7f110c7ebc17cfc35b0fb1de004a82d2f3672702 Mon Sep 17 00:00:00 2001 From: Abdul Manan Date: Thu, 31 Oct 2024 15:16:05 +0500 Subject: [PATCH] Updated Program APIs for WP --- course_discovery/apps/api/serializers.py | 27 +++++++++++---- course_discovery/apps/api/v1/views/search.py | 11 ++++++ .../apps/course_metadata/admin.py | 4 +-- .../migrations/0275_auto_20241031_0917.py | 23 +++++++++++++ .../apps/course_metadata/models.py | 4 ++- .../apps/course_metadata/search_indexes.py | 34 +++++++++++++++++++ 6 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 course_discovery/apps/course_metadata/migrations/0275_auto_20241031_0917.py diff --git a/course_discovery/apps/api/serializers.py b/course_discovery/apps/api/serializers.py index c83e993bb2..1091e04c7b 100644 --- a/course_discovery/apps/api/serializers.py +++ b/course_discovery/apps/api/serializers.py @@ -813,7 +813,8 @@ class Meta: model = CourseRun fields = ('key', 'uuid', 'title', 'external_key', 'image', 'short_description', 'marketing_url', 'seats', 'start', 'end', 'go_live_date', 'enrollment_start', 'enrollment_end', - 'pacing_type', 'type', 'run_type', 'status', 'is_enrollable', 'is_marketable', 'term', 'subjects',) + 'pacing_type', 'type', 'run_type', 'status', 'is_enrollable', 'is_marketable', 'term', 'subjects', + 'card_image_url') def get_marketing_url(self, obj): include_archived = self.context.get('include_archived') @@ -928,7 +929,7 @@ class Meta(MinimalCourseRunSerializer.Meta): 'first_enrollable_paid_seat_price', 'has_ofac_restrictions', 'ofac_comment', 'enrollment_count', 'recent_enrollment_count', 'expected_program_type', 'expected_program_name', 'course_uuid', 'estimated_hours', 'invite_only', 'subjects', - 'is_marketing_price_set', 'marketing_price_value', 'is_marketing_price_hidden', 'featured', 'card_image_url', + 'is_marketing_price_set', 'marketing_price_value', 'is_marketing_price_hidden', 'featured', 'average_rating', 'total_raters', 'yt_video_url', 'course_duration_override', 'course_difficulty', 'course_job_role', 'course_format', 'course_industry_certified_training', 'course_owner', 'course_language' ) @@ -1483,6 +1484,7 @@ class MinimalProgramSerializer(DynamicFieldsMixin, BaseModelSerializer): authoring_organizations = MinimalOrganizationSerializer(many=True) banner_image = StdImageSerializerField(allow_null=True, required=False) + card_image = StdImageSerializerField(allow_null=True, required=False) courses = serializers.SerializerMethodField() type = serializers.SlugRelatedField(slug_field='slug', queryset=ProgramType.objects.all()) type_attrs = ProgramTypeAttrsSerializer(source='type') @@ -1512,8 +1514,8 @@ class Meta: model = Program fields = ( 'uuid', 'title', 'subtitle', 'type', 'type_attrs', 'status', 'marketing_slug', 'marketing_url', - 'banner_image', 'hidden', 'courses', 'authoring_organizations', 'card_image_url', - 'is_program_eligible_for_one_click_purchase', 'degree', 'curricula', 'marketing_hook', + 'banner_image', 'card_image', 'hidden', 'courses', 'authoring_organizations', 'card_image_url', + 'is_program_eligible_for_one_click_purchase', 'degree', 'curricula', 'marketing_hook', 'featured' ) read_only_fields = ('uuid', 'marketing_url', 'banner_image') @@ -1633,6 +1635,7 @@ class ProgramSerializer(MinimalProgramSerializer): marketing_slug = CharField() type_attrs = ProgramTypeAttrsSerializer(source='type', required=False) curricula = CurriculumSerializer(many=True, required=False) + banner_image_url = serializers.URLField(required=False, allow_null=True) @classmethod def prefetch_queryset(cls, partner, queryset=None): @@ -1678,7 +1681,7 @@ class Meta(MinimalProgramSerializer.Meta): 'faq', 'credit_backing_organizations', 'corporate_endorsements', 'job_outlook_items', 'individual_endorsements', 'languages', 'transcript_languages', 'subjects', 'price_ranges', 'staff', 'credit_redemption_overview', 'applicable_seat_types', 'instructor_ordering', - 'enrollment_count', 'recent_enrollment_count', 'topics', 'credit_value', + 'enrollment_count', 'recent_enrollment_count', 'topics', 'credit_value', 'banner_image_url' ) def create(self, validated_data): @@ -1720,6 +1723,10 @@ def update(self, instance, validated_data): instance.min_hours_effort_per_week = validated_data.get('min_hours_effort_per_week', instance.min_hours_effort_per_week) instance.max_hours_effort_per_week = validated_data.get('max_hours_effort_per_week', instance.max_hours_effort_per_week) instance.marketing_slug = validated_data.get('marketing_slug', instance.marketing_slug) + instance.featured = validated_data.get('featured', instance.featured) + instance.overview = validated_data.get('overview', instance.overview) + instance.card_image = validated_data.get('card_image', instance.card_image) + instance.banner_image_url = validated_data.get('banner_image_url', instance.banner_image_url) instance.save() @@ -2451,7 +2458,15 @@ class Meta: 'subject_uuids', 'weeks_to_complete_max', 'weeks_to_complete_min', - 'search_card_display' + 'search_card_display', + 'num_of_courses', + 'featured', + 'overview', + 'banner_image_url', + 'bundle_price', + 'bundle_currency', + 'created', + 'title_override' ) diff --git a/course_discovery/apps/api/v1/views/search.py b/course_discovery/apps/api/v1/views/search.py index 0eb118f29a..24e49a967d 100644 --- a/course_discovery/apps/api/v1/views/search.py +++ b/course_discovery/apps/api/v1/views/search.py @@ -18,6 +18,7 @@ from rest_framework.views import APIView from course_discovery.apps.api import filters, mixins, serializers +from course_discovery.apps.api.utils import get_query_param from course_discovery.apps.course_metadata.choices import ProgramStatus from course_discovery.apps.course_metadata.models import Course, CourseRun, Person, Program @@ -157,10 +158,20 @@ class ProgramSearchViewSet(BaseHaystackViewSet): document_uid_field = 'uuid' lookup_field = 'uuid' index_models = (Program,) + ordering_fields = ('created', 'start', 'title', 'title_override') + filter_backends = [filters.HaystackFilter, OrderingFilter] detail_serializer_class = serializers.ProgramSearchModelSerializer facet_serializer_class = serializers.ProgramFacetSerializer serializer_class = serializers.ProgramSearchSerializer + def get_serializer_context(self): + context = super().get_serializer_context() + query_params = ['exclude_utm', 'use_full_course_serializer', 'published_course_runs_only', + 'marketable_enrollable_course_runs_with_archived'] + for query_param in query_params: + context[query_param] = get_query_param(self.request, query_param) + return context + class AggregateSearchViewSet(BaseHaystackViewSet, CatalogDataViewSet): """ Search all content types. """ diff --git a/course_discovery/apps/course_metadata/admin.py b/course_discovery/apps/course_metadata/admin.py index ba0c04f22b..5466ac8975 100644 --- a/course_discovery/apps/course_metadata/admin.py +++ b/course_discovery/apps/course_metadata/admin.py @@ -203,8 +203,8 @@ class ProgramAdmin(admin.ModelAdmin): 'card_image', 'marketing_slug', 'overview', 'credit_redemption_overview', 'video', 'total_hours_of_effort', 'weeks_to_complete', 'min_hours_effort_per_week', 'max_hours_effort_per_week', 'courses', 'order_courses_by_start_date', 'custom_course_runs_display', 'excluded_course_runs', 'authoring_organizations', - 'credit_backing_organizations', 'one_click_purchase_enabled', 'hidden', 'corporate_endorsements', 'faq', - 'individual_endorsements', 'job_outlook_items', 'expected_learning_items', 'instructor_ordering', + 'credit_backing_organizations', 'one_click_purchase_enabled', 'hidden', 'featured', 'corporate_endorsements', + 'faq', 'individual_endorsements', 'job_outlook_items', 'expected_learning_items', 'instructor_ordering', 'enrollment_count', 'recent_enrollment_count', 'credit_value', ) diff --git a/course_discovery/apps/course_metadata/migrations/0275_auto_20241031_0917.py b/course_discovery/apps/course_metadata/migrations/0275_auto_20241031_0917.py new file mode 100644 index 0000000000..8a61bb87f3 --- /dev/null +++ b/course_discovery/apps/course_metadata/migrations/0275_auto_20241031_0917.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.24 on 2024-10-31 09:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_metadata', '0274_auto_20240911_0805'), + ] + + operations = [ + migrations.AddField( + model_name='historicalprogram', + name='featured', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='program', + name='featured', + field=models.BooleanField(default=False), + ), + ] diff --git a/course_discovery/apps/course_metadata/models.py b/course_discovery/apps/course_metadata/models.py index f65d9fc3ad..9c56b25eb3 100644 --- a/course_discovery/apps/course_metadata/models.py +++ b/course_discovery/apps/course_metadata/models.py @@ -2162,6 +2162,8 @@ class Program(PkSearchableMixin, TimeStampedModel): blank=True, default=0, help_text=_( 'Number of credits a learner will earn upon successful completion of the program') ) + featured = models.BooleanField(default=False) + objects = ProgramQuerySet.as_manager() history = HistoricalRecords() @@ -2233,7 +2235,7 @@ def weeks_to_complete_max(self): @property def marketing_url(self): if self.marketing_slug: - path = '{type}/{slug}'.format(type=self.type.slug.lower(), slug=self.marketing_slug) + path = 'program/{slug}'.format(slug=self.marketing_slug) return urljoin(self.partner.marketing_site_url_root, path) return None diff --git a/course_discovery/apps/course_metadata/search_indexes.py b/course_discovery/apps/course_metadata/search_indexes.py index bce1ab9f58..c54ca41c7e 100644 --- a/course_discovery/apps/course_metadata/search_indexes.py +++ b/course_discovery/apps/course_metadata/search_indexes.py @@ -350,6 +350,7 @@ class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin): uuid = indexes.CharField(model_attr='uuid') title = indexes.CharField(model_attr='title', boost=TITLE_FIELD_BOOST) + title_override = indexes.CharField(indexed=False, stored=True) title_autocomplete = indexes.NgramField(model_attr='title', boost=TITLE_FIELD_BOOST) subtitle = indexes.CharField(model_attr='subtitle') type = indexes.CharField(model_attr='type__name_t', faceted=True) @@ -375,9 +376,16 @@ class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin): weeks_to_complete_max = indexes.IntegerField(model_attr='weeks_to_complete_max', null=True) language = indexes.MultiValueField(faceted=True) hidden = indexes.BooleanField(model_attr='hidden', faceted=True) + num_of_courses = indexes.IntegerField(null=True) + featured = indexes.BooleanField(model_attr='featured', faceted=True) is_program_eligible_for_one_click_purchase = indexes.BooleanField( model_attr='is_program_eligible_for_one_click_purchase', null=False ) + overview = indexes.CharField(model_attr='overview', null=True) + banner_image_url = indexes.CharField(null=True) + bundle_price = indexes.IntegerField(null=True) + bundle_currency = indexes.CharField(null=True) + created = indexes.DateTimeField(model_attr='created', null=True, faceted=True) def prepare_aggregation_key(self, obj): return 'program:{}'.format(obj.uuid) @@ -410,6 +418,32 @@ def prepare_search_card_display(self, obj): return [] return [degree.search_card_ranking, degree.search_card_cost, degree.search_card_courses] + + def prepare_num_of_courses(self, obj): + return obj.courses.count() + + def prepare_banner_image_url(self, obj): + if obj.banner_image_url: + return obj.banner_image_url + elif obj.banner_image: + return obj.banner_image.url + return None + + def prepare_bundle_price(self, obj): + course_runs = list(obj.course_runs) + return sum( + course_run.first_enrollable_paid_seat_price + for course_run in course_runs + if course_run.first_enrollable_paid_seat_price + ) + + def prepare_bundle_currency(self, obj): + course_runs = list(obj.course_runs) + first_seat = course_runs[0].seats.first() if course_runs else None + return first_seat.currency.code if first_seat else None + + def prepare_title_override(self, obj): + return obj.title.title() class PersonIndex(BaseIndex, indexes.Indexable):