diff --git a/course_discovery/apps/api/serializers.py b/course_discovery/apps/api/serializers.py index c83e993bb21..039ca551f95 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') @@ -1720,6 +1721,9 @@ 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.save() @@ -2451,7 +2455,12 @@ 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' ) diff --git a/course_discovery/apps/api/v1/views/search.py b/course_discovery/apps/api/v1/views/search.py index 0eb118f29af..d982c95451f 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,19 @@ class ProgramSearchViewSet(BaseHaystackViewSet): document_uid_field = 'uuid' lookup_field = 'uuid' index_models = (Program,) + filter_backends = [filters.HaystackFilter] 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..73dd814b3cd 100644 --- a/course_discovery/apps/course_metadata/search_indexes.py +++ b/course_discovery/apps/course_metadata/search_indexes.py @@ -375,9 +375,14 @@ 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) def prepare_aggregation_key(self, obj): return 'program:{}'.format(obj.uuid) @@ -410,6 +415,17 @@ 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: + return obj.banner_image.url + return None + + def prepare_bundle_price(self, obj): + return 0 class PersonIndex(BaseIndex, indexes.Indexable):