Skip to content

Commit

Permalink
feat: add create and update api for programs
Browse files Browse the repository at this point in the history
  • Loading branch information
hinakhadim committed Oct 13, 2023
1 parent 20ad9e5 commit c7ddacf
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 26 deletions.
123 changes: 101 additions & 22 deletions course_discovery/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from course_discovery.apps.course_metadata.utils import get_course_run_estimated_hours, parse_course_key_fragment
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api.serializers import GroupUserSerializer
from course_discovery.apps.discovery_dataloader_app.tasks import update_indexes

User = get_user_model()
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -1912,21 +1913,21 @@ class MinimalProgramSerializer(TaggitSerializer, FlexFieldsSerializerMixin, Base
"""

authoring_organizations = MinimalOrganizationSerializer(many=True)
banner_image = StdImageSerializerField()
banner_image = StdImageSerializerField(allow_null=True, required=False)
courses = serializers.SerializerMethodField()
type = serializers.SlugRelatedField(slug_field='name_t', queryset=ProgramType.objects.all())
type_attrs = ProgramTypeAttrsSerializer(source='type')
degree = DegreeSerializer()
curricula = CurriculumSerializer(many=True)
card_image_url = serializers.SerializerMethodField()
type = serializers.SlugRelatedField(slug_field='slug', queryset=ProgramType.objects.all())
type_attrs = ProgramTypeAttrsSerializer(source='type', required=False)
degree = DegreeSerializer(allow_null=True, required=False)
curricula = CurriculumSerializer(many=True, required=False)
card_image_url = serializers.SerializerMethodField(read_only=False)
organization_short_code_override = serializers.CharField(required=False, allow_blank=True)
organization_logo_override_url = serializers.SerializerMethodField()
primary_subject_override = SubjectSerializer()
level_type_override = LevelTypeSerializer()
primary_subject_override = SubjectSerializer(required=False)
level_type_override = LevelTypeSerializer(required=False)
language_override = serializers.SlugRelatedField(slug_field='code', read_only=True)
labels = TagListSerializerField()
taxi_form = TaxiFormSerializer()
subscription = ProgramSubscriptionSerializer()
labels = TagListSerializerField(required=False)
taxi_form = TaxiFormSerializer(required=False)
subscription = ProgramSubscriptionSerializer(required=False)

def get_organization_logo_override_url(self, obj):
logo_image_override = getattr(obj, 'organization_logo_override', None)
Expand Down Expand Up @@ -2116,14 +2117,14 @@ class Meta(MinimalProgramSerializer.Meta):


class ProgramSerializer(MinimalProgramSerializer):
authoring_organizations = OrganizationSerializer(many=True)
video = VideoSerializer()
authoring_organizations = OrganizationSerializer(many=True, read_only=True)
video = VideoSerializer(allow_null=True, required=False)
expected_learning_items = serializers.SlugRelatedField(many=True, read_only=True, slug_field='value')
faq = FAQSerializer(many=True)
credit_backing_organizations = OrganizationSerializer(many=True)
corporate_endorsements = CorporateEndorsementSerializer(many=True)
faq = FAQSerializer(many=True, required=False)
credit_backing_organizations = OrganizationSerializer(many=True, required=False)
corporate_endorsements = CorporateEndorsementSerializer(many=True, required=False)
job_outlook_items = serializers.SlugRelatedField(many=True, read_only=True, slug_field='value')
individual_endorsements = EndorsementSerializer(many=True)
individual_endorsements = EndorsementSerializer(many=True, required=False)
languages = serializers.SlugRelatedField(
many=True, read_only=True, slug_field='code',
help_text=_('Languages that course runs in this program are offered in.'),
Expand All @@ -2132,15 +2133,15 @@ class ProgramSerializer(MinimalProgramSerializer):
many=True, read_only=True, slug_field='code',
help_text=_('Languages that course runs in this program have available transcripts in.'),
)
subjects = SubjectSerializer(many=True)
staff = MinimalPersonSerializer(many=True)
instructor_ordering = MinimalPersonSerializer(many=True)
subjects = SubjectSerializer(many=True, required=False)
staff = MinimalPersonSerializer(many=True, required=False)
instructor_ordering = MinimalPersonSerializer(many=True, required=False)
applicable_seat_types = serializers.SerializerMethodField()
topics = serializers.SerializerMethodField()
enterprise_subscription_inclusion = serializers.BooleanField()
enterprise_subscription_inclusion = serializers.BooleanField(required=False)
geolocation = GeoLocationSerializer(required=False, allow_null=True)
location_restriction = ProgramLocationRestrictionSerializer(read_only=True)
is_2u_degree_program = serializers.BooleanField()
is_2u_degree_program = serializers.BooleanField(required=False)
in_year_value = ProductValueSerializer(required=False)
skill_names = serializers.SerializerMethodField()
skills = serializers.SerializerMethodField()
Expand Down Expand Up @@ -2212,6 +2213,84 @@ class Meta(MinimalProgramSerializer.Meta):
read_only_fields = ('enterprise_subscription_inclusion', 'product_source',)


def create(self, validated_data):
authoring_organizations = self.context.get('authoring_organizations')
credit_backing_organizations = self.context.get('credit_backing_organizations')
courses = self.context.get('courses')
excluded_course_runs = self.context.get('excluded_course_runs')

instructor_ordering = validated_data.pop('instructor_ordering', [])
corporate_endorsements = validated_data.pop('corporate_endorsements', [])
individual_endorsements = validated_data.pop('individual_endorsements', [])
faq = validated_data.pop('faq', [])

program = Program(**validated_data, card_image_url=self.initial_data.get('card_image_url', ''))
program.partner = self.context.get("partner")
program.save()

program.authoring_organizations.set(authoring_organizations)
program.credit_backing_organizations.set(credit_backing_organizations)

program.courses.set(courses)
program.excluded_course_runs.set(excluded_course_runs)

program.instructor_ordering.set(instructor_ordering)
program.corporate_endorsements.set(corporate_endorsements)
program.individual_endorsements.set(individual_endorsements)
program.faq.set(faq)

update_indexes.delay(model_names=['course_metadata.program'])

return program

def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.status = validated_data.get('status', instance.status)
instance.card_image_url = self.initial_data.get('card_image_url', instance.card_image_url)
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.overview = validated_data.get('overview', instance.overview)

instance.save()

courses = self.context.get('courses')
if courses is not None:
instance.courses.set(courses, clear=True)

exluded_course_runs = self.context.get('excluded_course_runs')
if exluded_course_runs is not None:
instance.excluded_course_runs.set(exluded_course_runs, clear=True)

authoring_organizations = self.context.get('authoring_organizations')
if authoring_organizations is not None:
instance.authoring_organizations.set(authoring_organizations, clear=True)

credit_backing_organizations = self.context.get('credit_backing_organizations')
if credit_backing_organizations is not None:
instance.credit_backing_organizations.set(credit_backing_organizations, clear=True)

instructor_ordering = validated_data.pop('instructor_ordering', [])
if instructor_ordering:
instance.instructor_ordering.set(instructor_ordering, clear=True)

corporate_endorsements = validated_data.pop('corporate_endorsements', [])
if corporate_endorsements:
instance.corporate_endorsements.set(corporate_endorsements, clear=True)

individual_endorsements = validated_data.pop('individual_endorsements', [])
if corporate_endorsements:
instance.individual_endorsements.set(individual_endorsements, clear=True)

faq = validated_data.pop('faq', [])
if faq:
instance.faq.set(faq, clear=True)

update_indexes.delay(model_names=['course_metadata.program'])

return instance


class PathwaySerializer(BaseModelSerializer):
""" Serializer for Pathway. """
uuid = serializers.CharField()
Expand Down
62 changes: 60 additions & 2 deletions course_discovery/apps/api/v1/views/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from course_discovery.apps.api.cache import CompressedCacheResponseMixin
from course_discovery.apps.api.pagination import ProxiedPagination
from course_discovery.apps.api.utils import get_query_param
from course_discovery.apps.course_metadata.models import Program
from course_discovery.apps.course_metadata.models import Course, CourseRun, Organization, Program


class ProgramViewSet(CompressedCacheResponseMixin, viewsets.ReadOnlyModelViewSet):
class ProgramViewSet(CompressedCacheResponseMixin, viewsets.ModelViewSet):
""" Program resource. """
lookup_field = 'uuid'
lookup_value_regex = '[0-9a-f-]+'
Expand Down Expand Up @@ -57,6 +57,64 @@ def get_serializer_context(self):

return context

def prepare_and_set_read_only_data(self, data, context):
"""
Extracts read only data from data dictionary and sets it in context.
"""
authoring_organizations = data.pop('authoring_organizations', None)
if authoring_organizations is not None:
context['authoring_organizations'] = Organization.objects.filter(key__in=authoring_organizations).distinct()

credit_backing_organizations = data.pop('credit_backing_organizations', None)
if credit_backing_organizations is not None:
context['credit_backing_organizations'] = Organization.objects.filter(key__in=credit_backing_organizations).distinct()

course_run_keys = data.pop('course_runs', None)
if course_run_keys is not None:
course_runs = CourseRun.objects.filter(key__in=course_run_keys).distinct()
courses = Course.objects.filter(key__in=course_runs.values('course__key').distinct())
excluded_course_runs = CourseRun.objects.filter(course__in=courses).exclude(key__in=course_run_keys).distinct()

context['courses'] = courses
context['excluded_course_runs'] = excluded_course_runs


def create(self, request, *args, **kwargs):
"""
Create a new program.
"""
data = request.data
context = {}

self.prepare_and_set_read_only_data(data, context)

context['partner'] = request.site.partner
context['request'] = request

serializer = serializers.ProgramSerializer(data=data, context=context)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

def update(self, request, *args, **kwargs):
"""
Update exsisting program.
"""
data = request.data
context = {}
instance = Program.objects.get(uuid=kwargs.get('uuid'))

self.prepare_and_set_read_only_data(data, context)

context['request'] = request

serializer = serializers.ProgramSerializer(instance, data=data, context=context)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)

def list(self, request, *args, **kwargs):
""" List all programs.
---
Expand Down
20 changes: 19 additions & 1 deletion course_discovery/apps/api/v1/views/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
LOOKUP_FILTER_RANGE, LOOKUP_FILTER_TERM, LOOKUP_FILTER_TERMS, LOOKUP_QUERY_EXCLUDE, LOOKUP_QUERY_GT,
LOOKUP_QUERY_GTE, LOOKUP_QUERY_IN, LOOKUP_QUERY_LT, LOOKUP_QUERY_LTE
)
from django_elasticsearch_dsl_drf.filter_backends import DefaultOrderingFilterBackend, OrderingFilterBackend
from django_elasticsearch_dsl_drf.filter_backends import DefaultOrderingFilterBackend, FilteringFilterBackend, MultiMatchSearchFilterBackend, OrderingFilterBackend, SearchFilterBackend
from elasticsearch_dsl.query import Q as ESDSLQ
from rest_framework import status, viewsets
from rest_framework.exceptions import ValidationError
Expand Down Expand Up @@ -146,6 +146,24 @@ class ProgramSearchViewSet(BaseElasticsearchDocumentViewSet):
'status': {'field': 'status', 'enabled': True},
'seat_types': {'field': 'seat_types', 'enabled': True},
}

filter_backends = [
FilteringFilterBackend,
SearchFilterBackend,
MultiMatchSearchFilterBackend,
DefaultOrderingFilterBackend,
]

search_fields = ('title', )

filter_fields = {
'published': 'published',
'status': 'status',
'partner': 'partner',
'title': 'title',
'type': {'field': 'type.raw'},
'uuid': 'uuid',
}


class BaseAggregateSearchViewSet(FacetQueryFieldsMixin, BaseElasticsearchDocumentViewSet):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class ProgramDocument(BaseDocument, OrganizationsMixin):
is_2u_degree_program = fields.BooleanField()
excluded_from_seo = fields.BooleanField()
excluded_from_search = fields.BooleanField()
no_of_courses = fields.IntegerField(fields={'raw': fields.KeywordField()})

def prepare_aggregation_key(self, obj):
return 'program:{}'.format(obj.uuid)
Expand Down Expand Up @@ -120,8 +121,11 @@ def prepare_staff_uuids(self, obj):
def prepare_type(self, obj):
return obj.type.name_t

def prepare_no_of_courses(self, obj):
return len([course_run for course_run in obj.course_runs])

def get_queryset(self):
return super().get_queryset().select_related('type').select_related('partner')
return super().get_queryset().select_related('type').select_related('partner').prefetch_related('courses__course_runs')

class Django:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class Meta:
'is_2u_degree_program',
'excluded_from_search',
'excluded_from_seo',
'no_of_courses'
)
)

Expand Down
16 changes: 16 additions & 0 deletions course_discovery/apps/discovery_dataloader_app/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,19 @@ def run_dataloader(partner, course_id, service):
LOGGER.info('Runing remove_unused indexes command ...')
with subprocess.Popen(remove_unused_index_cmd, stdout=subprocess.PIPE, shell=True) as proc:
LOGGER.info(proc.stdout.read())


@shared_task
def update_indexes(model_names=[]):
models = " ".join(model_names)
update_index_cmd = "python manage.py update_index --disable-change-limit "
if models:
update_index_cmd += '--models ' + models
remove_unused_index_cmd = "python manage.py remove_unused_indexes"
LOGGER.info('Runing update_index command ...')
with subprocess.Popen(update_index_cmd, stdout=subprocess.PIPE, shell=True) as proc:
LOGGER.info(proc.stdout.read())

LOGGER.info('Runing remove_unused indexes command ...')
with subprocess.Popen(remove_unused_index_cmd, stdout=subprocess.PIPE, shell=True) as proc:
LOGGER.info(proc.stdout.read())

0 comments on commit c7ddacf

Please sign in to comment.