From dacf6c51ce1738acd1137d0fc55629eff9c2b23d 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 | 25 ++++++++--- course_discovery/apps/api/v1/views/search.py | 11 +++++ .../apps/course_metadata/admin.py | 2 +- .../migrations/0275_auto_20241031_0917.py | 23 +++++++++++ .../apps/course_metadata/models.py | 4 +- .../apps/course_metadata/search_indexes.py | 41 ++++++++++++++++++- 6 files changed, 97 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 c83e993bb21..67ab34ef399 100644 --- a/course_discovery/apps/api/serializers.py +++ b/course_discovery/apps/api/serializers.py @@ -813,7 +813,7 @@ 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 +928,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 +1483,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 +1513,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 +1634,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 +1680,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 +1722,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 +2457,14 @@ 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' ) diff --git a/course_discovery/apps/api/v1/views/search.py b/course_discovery/apps/api/v1/views/search.py index 0eb118f29af..df24057a059 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') + 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 ba0c04f22b8..3cf2f5aad66 100644 --- a/course_discovery/apps/course_metadata/admin.py +++ b/course_discovery/apps/course_metadata/admin.py @@ -203,7 +203,7 @@ 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', + '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 00000000000..8a61bb87f37 --- /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 f65d9fc3ad3..9c56b25eb3f 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 bce1ab9f587..44e440128d1 100644 --- a/course_discovery/apps/course_metadata/search_indexes.py +++ b/course_discovery/apps/course_metadata/search_indexes.py @@ -349,7 +349,7 @@ class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin): model = Program uuid = indexes.CharField(model_attr='uuid') - title = indexes.CharField(model_attr='title', boost=TITLE_FIELD_BOOST) + title = indexes.CharField(boost=TITLE_FIELD_BOOST) 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 +375,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 +417,38 @@ 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): + bundle_price = 0 + course_runs = list(obj.course_runs) + for course_run in course_runs: + first_enrollable_paid_seat_price = course_run.first_enrollable_paid_seat_price + if first_enrollable_paid_seat_price: + bundle_price += first_enrollable_paid_seat_price + return bundle_price + + def prepare_bundle_currency(self, obj): + course_runs = list(obj.course_runs) + if course_runs: + first_course_run = course_runs[0] + first_seat = first_course_run.seats.first() + if first_seat: + return first_seat.currency.code + return None + + def prepare_title(self, obj): + return obj.title.title() + class PersonIndex(BaseIndex, indexes.Indexable):