Commit 9a33d0bf by Uman Shahzad Committed by Uman Shahzad

Add full detail search endpoints.

* Ignore all types of coverage files.

  I was getting things like `.coverage.$user`.

* Refactor existing serializers.

  Some serializers had their metadata all the way at the top
  of the file in separate variables, even though no other
  serializer were using those variables.

* Put things into proper places to make it easier to read
  and change, without having to jump up and down a 1000+
  line file.

* Add new serializers and use them in viewsets.

  These are the serializers for all 4 new endpoints,
  for courses, course runs, programs, and their aggregation.

* Add new mixin for detail endpoint.

  The mixin will add `/detail` to the end of whatever viewset
  that uses it.

* Call isort on the codebase.

* Refactor serializer test code.

  This will make it x10 easier to add tests for the
  serializers we introduced in this PR.

* Add test code for serializers.

* Refactor and add tests for viewsets.
parent 31ac8f5c
...@@ -33,6 +33,7 @@ assets/ ...@@ -33,6 +33,7 @@ assets/
# Unit test / coverage reports # Unit test / coverage reports
.coverage .coverage
.coverage.*
htmlcov htmlcov
.tox .tox
nosetests.xml nosetests.xml
......
from rest_framework import status
from rest_framework.exceptions import APIException
class InvalidPartnerError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
"""
Mixins for the API application.
"""
# pylint: disable=not-callable
from rest_framework.decorators import list_route
from rest_framework.response import Response
class DetailMixin(object):
"""Mixin for adding in a detail endpoint using a special detail serializer."""
detail_serializer_class = None
@list_route(methods=['get'])
def details(self, request): # pylint: disable=unused-argument
"""
List detailed results.
---
parameters:
- name: q
description: Search text
paramType: query
type: string
required: false
"""
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_detail_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_detail_serializer(queryset, many=True)
return Response(serializer.data)
def get_detail_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_detail_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
def get_detail_serializer_class(self):
"""
Return the class to use for the serializer.
Defaults to using `self.detail_serializer_class`.
"""
assert self.detail_serializer_class is not None, (
"'%s' should either include a `detail_serializer_class` attribute, "
"or override the `get_detail_serializer_class()` method."
% self.__class__.__name__
)
return self.detail_serializer_class
...@@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model ...@@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model
from django.db.models.query import Prefetch from django.db.models.query import Prefetch
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from drf_haystack.serializers import HaystackFacetSerializer, HaystackSerializer from drf_haystack.serializers import HaystackFacetSerializer, HaystackSerializer, HaystackSerializerMixin
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import DictField from rest_framework.fields import DictField
from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
...@@ -18,68 +18,18 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie ...@@ -18,68 +18,18 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie
from course_discovery.apps.api.fields import ImageField, StdImageSerializerField from course_discovery.apps.api.fields import ImageField, StdImageSerializerField
from course_discovery.apps.catalogs.models import Catalog from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.core.api_client.lms import LMSAPIClient from course_discovery.apps.core.api_client.lms import LMSAPIClient
from course_discovery.apps.course_metadata import search_indexes
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.models import ( from course_discovery.apps.course_metadata.models import (
FAQ, CorporateEndorsement, Course, CourseEntitlement, CourseRun, Endorsement, Image, Organization, Person, FAQ, CorporateEndorsement, Course, CourseEntitlement, CourseRun, Endorsement, Image, Organization, Person,
PersonSocialNetwork, PersonWork, Position, Prerequisite, Program, ProgramType, Seat, SeatType, Subject, Topic, PersonSocialNetwork, PersonWork, Position, Prerequisite, Program, ProgramType, Seat, SeatType, Subject, Topic,
Video Video
) )
from course_discovery.apps.course_metadata.search_indexes import CourseIndex, CourseRunIndex, ProgramIndex
User = get_user_model() User = get_user_model()
COMMON_IGNORED_FIELDS = ('text',) COMMON_IGNORED_FIELDS = ('text',)
COMMON_SEARCH_FIELD_ALIASES = { COMMON_SEARCH_FIELD_ALIASES = {'q': 'text'}
'q': 'text',
}
COURSE_RUN_FACET_FIELD_OPTIONS = {
'level_type': {},
'organizations': {'size': settings.SEARCH_FACET_LIMIT},
'prerequisites': {},
'subjects': {},
'language': {},
'transcript_languages': {},
'pacing_type': {},
'content_type': {},
'type': {},
'seat_types': {},
'mobile_available': {},
}
COURSE_RUN_FACET_FIELD_QUERIES = {
'availability_current': {'query': 'start:<now AND end:>now'},
'availability_starting_soon': {'query': 'start:[now TO now+60d]'},
'availability_upcoming': {'query': 'start:[now+60d TO *]'},
'availability_archived': {'query': 'end:<=now'},
}
COURSE_RUN_SEARCH_FIELDS = (
'text', 'key', 'title', 'short_description', 'full_description', 'start', 'end', 'enrollment_start',
'enrollment_end', 'pacing_type', 'language', 'transcript_languages', 'marketing_url', 'content_type', 'org',
'number', 'seat_types', 'image_url', 'type', 'level_type', 'availability', 'published', 'partner', 'program_types',
'authoring_organization_uuids', 'subject_uuids', 'staff_uuids', 'mobile_available', 'logo_image_urls',
'aggregation_key', 'min_effort', 'max_effort', 'weeks_to_complete', 'has_enrollable_seats',
'first_enrollable_paid_seat_sku'
)
PROGRAM_FACET_FIELD_OPTIONS = {
'status': {},
'type': {},
'seat_types': {},
}
BASE_PROGRAM_FIELDS = (
'text', 'uuid', 'title', 'subtitle', 'type', 'marketing_url', 'content_type', 'status', 'card_image_url',
'published', 'partner', 'language',
)
PROGRAM_SEARCH_FIELDS = BASE_PROGRAM_FIELDS + (
'aggregation_key', 'authoring_organizations', 'authoring_organization_uuids', 'subject_uuids', 'staff_uuids',
'weeks_to_complete_min', 'weeks_to_complete_max', 'min_hours_effort_per_week', 'max_hours_effort_per_week',
'hidden', 'is_program_eligible_for_one_click_purchase',
)
PROGRAM_FACET_FIELDS = BASE_PROGRAM_FIELDS + ('organizations',)
PREFETCH_FIELDS = { PREFETCH_FIELDS = {
'course_run': [ 'course_run': [
'course__level_type', 'course__level_type',
...@@ -178,6 +128,17 @@ class TimestampModelSerializer(serializers.ModelSerializer): ...@@ -178,6 +128,17 @@ class TimestampModelSerializer(serializers.ModelSerializer):
modified = serializers.DateTimeField() modified = serializers.DateTimeField()
class ContentTypeSerializer(serializers.Serializer):
"""Serializer for retrieving the type of content. Useful in views returning multiple serialized models."""
content_type = serializers.SerializerMethodField()
def get_content_type(self, obj):
return obj._meta.model_name
class Meta(object):
fields = ('content_type',)
class NamedModelSerializer(serializers.ModelSerializer): class NamedModelSerializer(serializers.ModelSerializer):
"""Serializer for models inheriting from ``AbstractNamedModel``.""" """Serializer for models inheriting from ``AbstractNamedModel``."""
name = serializers.CharField() name = serializers.CharField()
...@@ -904,7 +865,7 @@ class ProgramSerializer(MinimalProgramSerializer): ...@@ -904,7 +865,7 @@ class ProgramSerializer(MinimalProgramSerializer):
""" """
Prefetch the related objects that will be serialized with a `Program`. Prefetch the related objects that will be serialized with a `Program`.
We use Pefetch objects so that we can prefetch and select all the way down the We use Prefetch objects so that we can prefetch and select all the way down the
chain of related fields from programs to course runs (i.e., we want control over chain of related fields from programs to course runs (i.e., we want control over
the querysets that we're prefetching). the querysets that we're prefetching).
""" """
...@@ -1155,6 +1116,7 @@ class QueryFacetFieldSerializer(serializers.Serializer): ...@@ -1155,6 +1116,7 @@ class QueryFacetFieldSerializer(serializers.Serializer):
class BaseHaystackFacetSerializer(HaystackFacetSerializer): class BaseHaystackFacetSerializer(HaystackFacetSerializer):
_abstract = True _abstract = True
serialize_objects = True
def get_fields(self): def get_fields(self):
query_facet_counts = self.instance.pop('queries', {}) query_facet_counts = self.instance.pop('queries', {})
...@@ -1186,27 +1148,28 @@ class BaseHaystackFacetSerializer(HaystackFacetSerializer): ...@@ -1186,27 +1148,28 @@ class BaseHaystackFacetSerializer(HaystackFacetSerializer):
class CourseSearchSerializer(HaystackSerializer): class CourseSearchSerializer(HaystackSerializer):
content_type = serializers.CharField(source='model_name')
class Meta: class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES field_aliases = COMMON_SEARCH_FIELD_ALIASES
fields = ('key', 'title', 'short_description', 'full_description', 'text', 'aggregation_key',)
ignore_fields = COMMON_IGNORED_FIELDS ignore_fields = COMMON_IGNORED_FIELDS
index_classes = [CourseIndex] index_classes = [search_indexes.CourseIndex]
fields = search_indexes.BASE_SEARCH_INDEX_FIELDS + (
'full_description',
'key',
'short_description',
'title',
)
class CourseFacetSerializer(BaseHaystackFacetSerializer): class CourseFacetSerializer(BaseHaystackFacetSerializer):
serialize_objects = True
class Meta: class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES field_aliases = COMMON_SEARCH_FIELD_ALIASES
ignore_fields = COMMON_IGNORED_FIELDS
field_options = { field_options = {
'level_type': {}, 'level_type': {},
'organizations': {}, 'organizations': {},
'prerequisites': {}, 'prerequisites': {},
'subjects': {}, 'subjects': {},
} }
ignore_fields = COMMON_IGNORED_FIELDS
class CourseRunSearchSerializer(HaystackSerializer): class CourseRunSearchSerializer(HaystackSerializer):
...@@ -1217,19 +1180,68 @@ class CourseRunSearchSerializer(HaystackSerializer): ...@@ -1217,19 +1180,68 @@ class CourseRunSearchSerializer(HaystackSerializer):
class Meta: class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES field_aliases = COMMON_SEARCH_FIELD_ALIASES
fields = COURSE_RUN_SEARCH_FIELDS
ignore_fields = COMMON_IGNORED_FIELDS ignore_fields = COMMON_IGNORED_FIELDS
index_classes = [CourseRunIndex] index_classes = [search_indexes.CourseRunIndex]
fields = search_indexes.BASE_SEARCH_INDEX_FIELDS + (
'authoring_organization_uuids',
'availability',
'end',
'enrollment_end',
'enrollment_start',
'first_enrollable_paid_seat_sku',
'full_description',
'has_enrollable_seats',
'image_url',
'key',
'language',
'level_type',
'logo_image_urls',
'marketing_url',
'max_effort',
'min_effort',
'mobile_available',
'number',
'org',
'pacing_type',
'partner',
'program_types',
'published',
'seat_types',
'short_description',
'staff_uuids',
'start',
'subject_uuids',
'text',
'title',
'transcript_languages',
'type',
'weeks_to_complete'
)
class CourseRunFacetSerializer(BaseHaystackFacetSerializer): class CourseRunFacetSerializer(BaseHaystackFacetSerializer):
serialize_objects = True
class Meta: class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES field_aliases = COMMON_SEARCH_FIELD_ALIASES
field_options = COURSE_RUN_FACET_FIELD_OPTIONS
field_queries = COURSE_RUN_FACET_FIELD_QUERIES
ignore_fields = COMMON_IGNORED_FIELDS ignore_fields = COMMON_IGNORED_FIELDS
field_options = {
'content_type': {},
'language': {},
'level_type': {},
'mobile_available': {},
'organizations': {'size': settings.SEARCH_FACET_LIMIT},
'pacing_type': {},
'prerequisites': {},
'seat_types': {},
'subjects': {},
'transcript_languages': {},
'type': {},
}
field_queries = {
'availability_current': {'query': 'start:<now AND end:>now'},
'availability_starting_soon': {'query': 'start:[now TO now+60d]'},
'availability_upcoming': {'query': 'start:[now+60d TO *]'},
'availability_archived': {'query': 'end:<=now'},
}
class ProgramSearchSerializer(HaystackSerializer): class ProgramSearchSerializer(HaystackSerializer):
...@@ -1241,32 +1253,86 @@ class ProgramSearchSerializer(HaystackSerializer): ...@@ -1241,32 +1253,86 @@ class ProgramSearchSerializer(HaystackSerializer):
class Meta: class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES field_aliases = COMMON_SEARCH_FIELD_ALIASES
field_options = PROGRAM_FACET_FIELD_OPTIONS
fields = PROGRAM_SEARCH_FIELDS
ignore_fields = COMMON_IGNORED_FIELDS ignore_fields = COMMON_IGNORED_FIELDS
index_classes = [ProgramIndex] index_classes = [search_indexes.ProgramIndex]
fields = search_indexes.BASE_SEARCH_INDEX_FIELDS + search_indexes.BASE_PROGRAM_FIELDS + (
'authoring_organization_uuids',
'authoring_organizations',
'hidden',
'is_program_eligible_for_one_click_purchase',
'max_hours_effort_per_week',
'min_hours_effort_per_week',
'staff_uuids',
'subject_uuids',
'weeks_to_complete_max',
'weeks_to_complete_min',
)
class ProgramFacetSerializer(BaseHaystackFacetSerializer): class ProgramFacetSerializer(BaseHaystackFacetSerializer):
serialize_objects = True
class Meta: class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES field_aliases = COMMON_SEARCH_FIELD_ALIASES
field_options = PROGRAM_FACET_FIELD_OPTIONS
fields = PROGRAM_FACET_FIELDS
ignore_fields = COMMON_IGNORED_FIELDS ignore_fields = COMMON_IGNORED_FIELDS
index_classes = [ProgramIndex] index_classes = [search_indexes.ProgramIndex]
field_options = {
'status': {},
'type': {},
'seat_types': {},
}
fields = search_indexes.BASE_PROGRAM_FIELDS + (
'organizations',
)
class AggregateSearchSerializer(HaystackSerializer): class AggregateSearchSerializer(HaystackSerializer):
class Meta: class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES field_aliases = COMMON_SEARCH_FIELD_ALIASES
fields = COURSE_RUN_SEARCH_FIELDS + PROGRAM_SEARCH_FIELDS
ignore_fields = COMMON_IGNORED_FIELDS ignore_fields = COMMON_IGNORED_FIELDS
fields = CourseRunSearchSerializer.Meta.fields + ProgramSearchSerializer.Meta.fields
serializers = {
search_indexes.CourseRunIndex: CourseRunSearchSerializer,
search_indexes.CourseIndex: CourseSearchSerializer,
search_indexes.ProgramIndex: ProgramSearchSerializer,
}
class AggregateFacetSearchSerializer(BaseHaystackFacetSerializer):
class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES
ignore_fields = COMMON_IGNORED_FIELDS
field_queries = CourseRunFacetSerializer.Meta.field_queries
field_options = {
**CourseRunFacetSerializer.Meta.field_options,
**ProgramFacetSerializer.Meta.field_options
}
serializers = {
search_indexes.CourseRunIndex: CourseRunFacetSerializer,
search_indexes.CourseIndex: CourseFacetSerializer,
search_indexes.ProgramIndex: ProgramFacetSerializer,
}
class CourseSearchModelSerializer(HaystackSerializerMixin, ContentTypeSerializer, CourseWithProgramsSerializer):
class Meta(CourseWithProgramsSerializer.Meta):
fields = ContentTypeSerializer.Meta.fields + CourseWithProgramsSerializer.Meta.fields
class CourseRunSearchModelSerializer(HaystackSerializerMixin, ContentTypeSerializer, CourseRunWithProgramsSerializer):
class Meta(CourseRunWithProgramsSerializer.Meta):
fields = ContentTypeSerializer.Meta.fields + CourseRunWithProgramsSerializer.Meta.fields
class ProgramSearchModelSerializer(HaystackSerializerMixin, ContentTypeSerializer, ProgramSerializer):
class Meta(ProgramSerializer.Meta):
fields = ContentTypeSerializer.Meta.fields + ProgramSerializer.Meta.fields
class AggregateSearchModelSerializer(HaystackSerializer):
class Meta:
serializers = { serializers = {
CourseRunIndex: CourseRunSearchSerializer, search_indexes.CourseRunIndex: CourseRunSearchModelSerializer,
CourseIndex: CourseSearchSerializer, search_indexes.CourseIndex: CourseSearchModelSerializer,
ProgramIndex: ProgramSearchSerializer, search_indexes.ProgramIndex: ProgramSearchModelSerializer,
} }
...@@ -1294,21 +1360,6 @@ class TypeaheadSearchSerializer(serializers.Serializer): ...@@ -1294,21 +1360,6 @@ class TypeaheadSearchSerializer(serializers.Serializer):
programs = TypeaheadProgramSearchSerializer(many=True) programs = TypeaheadProgramSearchSerializer(many=True)
class AggregateFacetSearchSerializer(BaseHaystackFacetSerializer):
serialize_objects = True
class Meta:
field_aliases = COMMON_SEARCH_FIELD_ALIASES
field_options = {**COURSE_RUN_FACET_FIELD_OPTIONS, **PROGRAM_FACET_FIELD_OPTIONS}
field_queries = COURSE_RUN_FACET_FIELD_QUERIES
ignore_fields = COMMON_IGNORED_FIELDS
serializers = {
CourseRunIndex: CourseRunFacetSerializer,
CourseIndex: CourseFacetSerializer,
ProgramIndex: ProgramFacetSerializer,
}
class TopicSerializer(serializers.ModelSerializer): class TopicSerializer(serializers.ModelSerializer):
"""Serializer for the ``Topic`` model.""" """Serializer for the ``Topic`` model."""
......
# pylint: disable=no-member, test-inherits-tests # pylint: disable=no-member,test-inherits-tests
import datetime import datetime
import itertools import itertools
from urllib.parse import urlencode from urllib.parse import urlencode
...@@ -19,14 +19,15 @@ from waffle.testutils import override_switch ...@@ -19,14 +19,15 @@ from waffle.testutils import override_switch
from course_discovery.apps.api.fields import ImageField, StdImageSerializerField from course_discovery.apps.api.fields import ImageField, StdImageSerializerField
from course_discovery.apps.api.serializers import ( from course_discovery.apps.api.serializers import (
AffiliateWindowSerializer, CatalogSerializer, ContainedCourseRunsSerializer, ContainedCoursesSerializer, AffiliateWindowSerializer, CatalogSerializer, ContainedCourseRunsSerializer, ContainedCoursesSerializer,
CorporateEndorsementSerializer, CourseEntitlementSerializer, CourseRunSearchSerializer, CourseRunSerializer, ContentTypeSerializer, CorporateEndorsementSerializer, CourseEntitlementSerializer, CourseRunSearchModelSerializer,
CourseRunWithProgramsSerializer, CourseSearchSerializer, CourseSerializer, CourseWithProgramsSerializer, CourseRunSearchSerializer, CourseRunSerializer, CourseRunWithProgramsSerializer, CourseSearchModelSerializer,
EndorsementSerializer, FAQSerializer, FlattenedCourseRunWithCourseSerializer, ImageSerializer, CourseSearchSerializer, CourseSerializer, CourseWithProgramsSerializer, EndorsementSerializer, FAQSerializer,
MinimalCourseRunSerializer, MinimalCourseSerializer, MinimalOrganizationSerializer, MinimalProgramCourseSerializer, FlattenedCourseRunWithCourseSerializer, ImageSerializer, MinimalCourseRunSerializer, MinimalCourseSerializer,
MinimalProgramSerializer, NestedProgramSerializer, OrganizationSerializer, PersonSerializer, PositionSerializer, MinimalOrganizationSerializer, MinimalProgramCourseSerializer, MinimalProgramSerializer, NestedProgramSerializer,
PrerequisiteSerializer, ProgramSearchSerializer, ProgramSerializer, ProgramTypeSerializer, SeatSerializer, OrganizationSerializer, PersonSerializer, PositionSerializer, PrerequisiteSerializer, ProgramSearchModelSerializer,
SubjectSerializer, TopicSerializer, TypeaheadCourseRunSearchSerializer, TypeaheadProgramSearchSerializer, ProgramSearchSerializer, ProgramSerializer, ProgramTypeSerializer, SeatSerializer, SubjectSerializer,
VideoSerializer, get_utm_source_for_user TopicSerializer, TypeaheadCourseRunSearchSerializer, TypeaheadProgramSearchSerializer, VideoSerializer,
get_utm_source_for_user
) )
from course_discovery.apps.api.tests.mixins import SiteMixin from course_discovery.apps.api.tests.mixins import SiteMixin
from course_discovery.apps.catalogs.tests.factories import CatalogFactory from course_discovery.apps.catalogs.tests.factories import CatalogFactory
...@@ -109,7 +110,8 @@ class CatalogSerializerTests(ElasticsearchTestMixin, TestCase): ...@@ -109,7 +110,8 @@ class CatalogSerializerTests(ElasticsearchTestMixin, TestCase):
class MinimalCourseSerializerTests(SiteMixin, TestCase): class MinimalCourseSerializerTests(SiteMixin, TestCase):
serializer_class = MinimalCourseSerializer serializer_class = MinimalCourseSerializer
def get_expected_data(self, course, request): @classmethod
def get_expected_data(cls, course, request):
context = {'request': request} context = {'request': request}
return { return {
...@@ -136,7 +138,8 @@ class MinimalCourseSerializerTests(SiteMixin, TestCase): ...@@ -136,7 +138,8 @@ class MinimalCourseSerializerTests(SiteMixin, TestCase):
class CourseSerializerTests(MinimalCourseSerializerTests): class CourseSerializerTests(MinimalCourseSerializerTests):
serializer_class = CourseSerializer serializer_class = CourseSerializer
def get_expected_data(self, course, request): @classmethod
def get_expected_data(cls, course, request):
expected = super().get_expected_data(course, request) expected = super().get_expected_data(course, request)
expected.update({ expected.update({
'short_description': course.short_description, 'short_description': course.short_description,
...@@ -179,7 +182,8 @@ class CourseSerializerTests(MinimalCourseSerializerTests): ...@@ -179,7 +182,8 @@ class CourseSerializerTests(MinimalCourseSerializerTests):
class CourseWithProgramsSerializerTests(CourseSerializerTests): class CourseWithProgramsSerializerTests(CourseSerializerTests):
serializer_class = CourseWithProgramsSerializer serializer_class = CourseWithProgramsSerializer
def get_expected_data(self, course, request): @classmethod
def get_expected_data(cls, course, request):
expected = super().get_expected_data(course, request) expected = super().get_expected_data(course, request)
expected.update({ expected.update({
'programs': NestedProgramSerializer( 'programs': NestedProgramSerializer(
...@@ -224,7 +228,8 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests): ...@@ -224,7 +228,8 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
class MinimalCourseRunSerializerTests(TestCase): class MinimalCourseRunSerializerTests(TestCase):
serializer_class = MinimalCourseRunSerializer serializer_class = MinimalCourseRunSerializer
def get_expected_data(self, course_run, request): # pylint: disable=unused-argument @classmethod
def get_expected_data(cls, course_run, request): # pylint: disable=unused-argument
return { return {
'key': course_run.key, 'key': course_run.key,
'uuid': str(course_run.uuid), 'uuid': str(course_run.uuid),
...@@ -259,7 +264,8 @@ class MinimalCourseRunSerializerTests(TestCase): ...@@ -259,7 +264,8 @@ class MinimalCourseRunSerializerTests(TestCase):
class CourseRunSerializerTests(MinimalCourseRunSerializerTests): class CourseRunSerializerTests(MinimalCourseRunSerializerTests):
serializer_class = CourseRunSerializer serializer_class = CourseRunSerializer
def get_expected_data(self, course_run, request): @classmethod
def get_expected_data(cls, course_run, request):
expected = super().get_expected_data(course_run, request) expected = super().get_expected_data(course_run, request)
expected.update({ expected.update({
'course': course_run.course.key, 'course': course_run.course.key,
...@@ -308,17 +314,7 @@ class CourseRunWithProgramsSerializerTests(TestCase): ...@@ -308,17 +314,7 @@ class CourseRunWithProgramsSerializerTests(TestCase):
def test_data(self): def test_data(self):
serializer = CourseRunWithProgramsSerializer(self.course_run, context=self.serializer_context) serializer = CourseRunWithProgramsSerializer(self.course_run, context=self.serializer_context)
ProgramFactory(courses=[self.course_run.course]) ProgramFactory(courses=[self.course_run.course])
self.assertDictEqual(serializer.data, self.get_expected_data(self.course_run, self.request))
expected = CourseRunSerializer(self.course_run, context=self.serializer_context).data
expected.update({
'programs': NestedProgramSerializer(
self.course_run.course.programs,
many=True,
context=self.serializer_context
).data,
})
self.assertDictEqual(serializer.data, expected)
def test_data_excluded_course_run(self): def test_data_excluded_course_run(self):
""" """
...@@ -328,11 +324,8 @@ class CourseRunWithProgramsSerializerTests(TestCase): ...@@ -328,11 +324,8 @@ class CourseRunWithProgramsSerializerTests(TestCase):
serializer = CourseRunWithProgramsSerializer(self.course_run, context=self.serializer_context) serializer = CourseRunWithProgramsSerializer(self.course_run, context=self.serializer_context)
ProgramFactory(courses=[self.course_run.course], excluded_course_runs=[self.course_run]) ProgramFactory(courses=[self.course_run.course], excluded_course_runs=[self.course_run])
expected = CourseRunSerializer(self.course_run, context=self.serializer_context).data expected = CourseRunSerializer(self.course_run, context=self.serializer_context).data
expected.update({ expected.update({'programs': []})
'programs': [], assert serializer.data == expected
})
self.assertDictEqual(serializer.data, expected)
def test_exclude_deleted_programs(self): def test_exclude_deleted_programs(self):
""" """
...@@ -398,9 +391,22 @@ class CourseRunWithProgramsSerializerTests(TestCase): ...@@ -398,9 +391,22 @@ class CourseRunWithProgramsSerializerTests(TestCase):
NestedProgramSerializer([retired_program], many=True, context=self.serializer_context).data NestedProgramSerializer([retired_program], many=True, context=self.serializer_context).data
) )
@classmethod
def get_expected_data(cls, course_run, request):
expected = CourseRunSerializer(course_run, context={'request': request}).data
expected.update({
'programs': NestedProgramSerializer(
course_run.course.programs,
many=True,
context={'request': request},
).data,
})
return expected
class FlattenedCourseRunWithCourseSerializerTests(TestCase): # pragma: no cover class FlattenedCourseRunWithCourseSerializerTests(TestCase): # pragma: no cover
def serialize_seats(self, course_run): @classmethod
def serialize_seats(cls, course_run):
seats = { seats = {
'audit': { 'audit': {
'type': '' 'type': ''
...@@ -442,21 +448,23 @@ class FlattenedCourseRunWithCourseSerializerTests(TestCase): # pragma: no cover ...@@ -442,21 +448,23 @@ class FlattenedCourseRunWithCourseSerializerTests(TestCase): # pragma: no cover
return seats return seats
def serialize_items(self, organizations, attr): @classmethod
def serialize_items(cls, organizations, attr):
return ','.join([getattr(organization, attr) for organization in organizations]) return ','.join([getattr(organization, attr) for organization in organizations])
def get_expected_data(self, request, course_run): @classmethod
def get_expected_data(cls, request, course_run):
course = course_run.course course = course_run.course
serializer_context = {'request': request} serializer_context = {'request': request}
expected = dict(CourseRunSerializer(course_run, context=serializer_context).data) expected = dict(CourseRunSerializer(course_run, context=serializer_context).data)
expected.update({ expected.update({
'subjects': self.serialize_items(course.subjects.all(), 'name'), 'subjects': cls.serialize_items(course.subjects.all(), 'name'),
'seats': self.serialize_seats(course_run), 'seats': cls.serialize_seats(course_run),
'owners': self.serialize_items(course.authoring_organizations.all(), 'key'), 'owners': cls.serialize_items(course.authoring_organizations.all(), 'key'),
'sponsors': self.serialize_items(course.sponsoring_organizations.all(), 'key'), 'sponsors': cls.serialize_items(course.sponsoring_organizations.all(), 'key'),
'prerequisites': self.serialize_items(course.prerequisites.all(), 'name'), 'prerequisites': cls.serialize_items(course.prerequisites.all(), 'name'),
'level_type': course_run.level_type.name if course_run.level_type else None, 'level_type': course_run.level_type.name if course_run.level_type else None,
'expected_learning_items': self.serialize_items(course.expected_learning_items.all(), 'value'), 'expected_learning_items': cls.serialize_items(course.expected_learning_items.all(), 'value'),
'course_key': course.key, 'course_key': course.key,
'image': ImageField().to_representation(course_run.card_image_url), 'image': ImageField().to_representation(course_run.card_image_url),
}) })
...@@ -623,7 +631,8 @@ class MinimalProgramSerializerTests(TestCase): ...@@ -623,7 +631,8 @@ class MinimalProgramSerializerTests(TestCase):
order_courses_by_start_date=False, order_courses_by_start_date=False,
) )
def get_expected_data(self, program, request): @classmethod
def get_expected_data(cls, program, request):
image_field = StdImageSerializerField() image_field = StdImageSerializerField()
image_field._context = {'request': request} # pylint: disable=protected-access image_field._context = {'request': request} # pylint: disable=protected-access
...@@ -661,9 +670,9 @@ class MinimalProgramSerializerTests(TestCase): ...@@ -661,9 +670,9 @@ class MinimalProgramSerializerTests(TestCase):
class ProgramSerializerTests(MinimalProgramSerializerTests): class ProgramSerializerTests(MinimalProgramSerializerTests):
serializer_class = ProgramSerializer serializer_class = ProgramSerializer
def get_expected_data(self, program, request): @classmethod
def get_expected_data(cls, program, request):
expected = super().get_expected_data(program, request) expected = super().get_expected_data(program, request)
expected.update({ expected.update({
'authoring_organizations': OrganizationSerializer(program.authoring_organizations, many=True).data, 'authoring_organizations': OrganizationSerializer(program.authoring_organizations, many=True).data,
'video': VideoSerializer(program.video).data, 'video': VideoSerializer(program.video).data,
...@@ -698,7 +707,6 @@ class ProgramSerializerTests(MinimalProgramSerializerTests): ...@@ -698,7 +707,6 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
'subjects': SubjectSerializer(program.subjects, many=True).data, 'subjects': SubjectSerializer(program.subjects, many=True).data,
'transcript_languages': [serialize_language_to_code(l) for l in program.transcript_languages], 'transcript_languages': [serialize_language_to_code(l) for l in program.transcript_languages],
}) })
return expected return expected
def test_data_with_exclusions(self): def test_data_with_exclusions(self):
...@@ -900,7 +908,8 @@ class ProgramSerializerTests(MinimalProgramSerializerTests): ...@@ -900,7 +908,8 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
class ProgramTypeSerializerTests(TestCase): class ProgramTypeSerializerTests(TestCase):
serializer_class = ProgramTypeSerializer serializer_class = ProgramTypeSerializer
def get_expected_data(self, program_type, request): @classmethod
def get_expected_data(cls, program_type, request):
image_field = StdImageSerializerField() image_field = StdImageSerializerField()
image_field._context = {'request': request} # pylint: disable=protected-access image_field._context = {'request': request} # pylint: disable=protected-access
...@@ -945,6 +954,23 @@ class ContainedCoursesSerializerTests(TestCase): ...@@ -945,6 +954,23 @@ class ContainedCoursesSerializerTests(TestCase):
@ddt.ddt @ddt.ddt
class ContentTypeSerializerTests(TestCase):
@ddt.data(
(CourseFactory, 'course'),
(CourseRunFactory, 'courserun'),
(ProgramFactory, 'program'),
)
@ddt.unpack
def test_data(self, factory_class, expected_content_type):
obj = factory_class()
serializer = ContentTypeSerializer(obj)
expected = {
'content_type': expected_content_type
}
assert serializer.data == expected
@ddt.ddt
class NamedModelSerializerTests(TestCase): class NamedModelSerializerTests(TestCase):
@ddt.data( @ddt.data(
(PrerequisiteFactory, PrerequisiteSerializer), (PrerequisiteFactory, PrerequisiteSerializer),
...@@ -1067,7 +1093,8 @@ class MinimalOrganizationSerializerTests(TestCase): ...@@ -1067,7 +1093,8 @@ class MinimalOrganizationSerializerTests(TestCase):
def create_organization(self): def create_organization(self):
return OrganizationFactory() return OrganizationFactory()
def get_expected_data(self, organization): @classmethod
def get_expected_data(cls, organization):
return { return {
'uuid': str(organization.uuid), 'uuid': str(organization.uuid),
'key': organization.key, 'key': organization.key,
...@@ -1090,14 +1117,15 @@ class OrganizationSerializerTests(MinimalOrganizationSerializerTests): ...@@ -1090,14 +1117,15 @@ class OrganizationSerializerTests(MinimalOrganizationSerializerTests):
organization.tags.add(self.TAG) organization.tags.add(self.TAG)
return organization return organization
def get_expected_data(self, organization): @classmethod
def get_expected_data(cls, organization):
expected = super().get_expected_data(organization) expected = super().get_expected_data(organization)
expected.update({ expected.update({
'certificate_logo_image_url': organization.certificate_logo_image_url, 'certificate_logo_image_url': organization.certificate_logo_image_url,
'description': organization.description, 'description': organization.description,
'homepage_url': organization.homepage_url, 'homepage_url': organization.homepage_url,
'logo_image_url': organization.logo_image_url, 'logo_image_url': organization.logo_image_url,
'tags': [self.TAG], 'tags': [cls.TAG],
'marketing_url': organization.marketing_url, 'marketing_url': organization.marketing_url,
}) })
...@@ -1215,11 +1243,23 @@ class AffiliateWindowSerializerTests(TestCase): ...@@ -1215,11 +1243,23 @@ class AffiliateWindowSerializerTests(TestCase):
class CourseSearchSerializerTests(TestCase): class CourseSearchSerializerTests(TestCase):
serializer_class = CourseSearchSerializer
def test_data(self): def test_data(self):
request = make_request()
course = CourseFactory() course = CourseFactory()
serializer = self.serialize_course(course) serializer = self.serialize_course(course, request)
assert serializer.data == self.get_expected_data(course, request)
expected = { def serialize_course(self, course, request):
""" Serializes the given `Course` as a search result. """
result = SearchQuerySet().models(Course).filter(key=course.key)[0]
serializer = self.serializer_class(result, context={'request': request})
return serializer
@classmethod
def get_expected_data(cls, course, request): # pylint: disable=unused-argument
return {
'key': course.key, 'key': course.key,
'title': course.title, 'title': course.title,
'short_description': course.short_description, 'short_description': course.short_description,
...@@ -1227,26 +1267,47 @@ class CourseSearchSerializerTests(TestCase): ...@@ -1227,26 +1267,47 @@ class CourseSearchSerializerTests(TestCase):
'content_type': 'course', 'content_type': 'course',
'aggregation_key': 'course:{}'.format(course.key), 'aggregation_key': 'course:{}'.format(course.key),
} }
assert serializer.data == expected
def serialize_course(self, course):
""" Serializes the given `Course` as a search result. """ class CourseSearchModelSerializerTests(CourseSearchSerializerTests):
result = SearchQuerySet().models(Course).filter(key=course.key)[0] serializer_class = CourseSearchModelSerializer
serializer = CourseSearchSerializer(result)
return serializer @classmethod
def get_expected_data(cls, course, request):
expected_data = CourseWithProgramsSerializerTests.get_expected_data(course, request)
expected_data.update({'content_type': 'course'})
return expected_data
class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase): class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
serializer_class = CourseRunSearchSerializer
def test_data(self): def test_data(self):
request = make_request()
course_run = CourseRunFactory(transcript_languages=LanguageTag.objects.filter(code__in=['en-us', 'zh-cn']), course_run = CourseRunFactory(transcript_languages=LanguageTag.objects.filter(code__in=['en-us', 'zh-cn']),
authoring_organizations=[OrganizationFactory()]) authoring_organizations=[OrganizationFactory()])
SeatFactory.create(course_run=course_run, type='verified', price=10, sku='ABCDEF') SeatFactory.create(course_run=course_run, type='verified', price=10, sku='ABCDEF')
program = ProgramFactory(courses=[course_run.course]) program = ProgramFactory(courses=[course_run.course])
self.reindex_courses(program) self.reindex_courses(program)
serializer = self.serialize_course_run(course_run) serializer = self.serialize_course_run(course_run, request)
course_run_key = CourseKey.from_string(course_run.key) assert serializer.data == self.get_expected_data(course_run, request)
orgs = course_run.authoring_organizations.all()
expected = { def test_data_without_serializers(self):
""" Verify a null `LevelType` is properly serialized as None. """
request = make_request()
course_run = CourseRunFactory(course__level_type=None)
serializer = self.serialize_course_run(course_run, request)
assert serializer.data['level_type'] is None
def serialize_course_run(self, course_run, request):
""" Serializes the given `CourseRun` as a search result. """
result = SearchQuerySet().models(CourseRun).filter(key=course_run.key)[0]
serializer = self.serializer_class(result, context={'request': request})
return serializer
@classmethod
def get_expected_data(cls, course_run, request): # pylint: disable=unused-argument
return {
'transcript_languages': [serialize_language(l) for l in course_run.transcript_languages.all()], 'transcript_languages': [serialize_language(l) for l in course_run.transcript_languages.all()],
'min_effort': course_run.min_effort, 'min_effort': course_run.min_effort,
'max_effort': course_run.max_effort, 'max_effort': course_run.max_effort,
...@@ -1264,8 +1325,8 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase): ...@@ -1264,8 +1325,8 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
'full_description': course_run.full_description, 'full_description': course_run.full_description,
'title': course_run.title, 'title': course_run.title,
'content_type': 'courserun', 'content_type': 'courserun',
'org': course_run_key.org, 'org': CourseKey.from_string(course_run.key).org,
'number': course_run_key.course, 'number': CourseKey.from_string(course_run.key).course,
'seat_types': course_run.seat_types, 'seat_types': course_run.seat_types,
'image_url': course_run.image_url, 'image_url': course_run.image_url,
'type': course_run.type, 'type': course_run.type,
...@@ -1274,7 +1335,7 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase): ...@@ -1274,7 +1335,7 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
'published': course_run.status == CourseRunStatus.Published, 'published': course_run.status == CourseRunStatus.Published,
'partner': course_run.course.partner.short_code, 'partner': course_run.course.partner.short_code,
'program_types': course_run.program_types, 'program_types': course_run.program_types,
'logo_image_urls': [org.logo_image_url for org in orgs], 'logo_image_urls': [org.logo_image_url for org in course_run.authoring_organizations.all()],
'authoring_organization_uuids': get_uuids(course_run.authoring_organizations.all()), 'authoring_organization_uuids': get_uuids(course_run.authoring_organizations.all()),
'subject_uuids': get_uuids(course_run.subjects.all()), 'subject_uuids': get_uuids(course_run.subjects.all()),
'staff_uuids': get_uuids(course_run.staff.all()), 'staff_uuids': get_uuids(course_run.staff.all()),
...@@ -1282,25 +1343,30 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase): ...@@ -1282,25 +1343,30 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
'has_enrollable_seats': course_run.has_enrollable_seats, 'has_enrollable_seats': course_run.has_enrollable_seats,
'first_enrollable_paid_seat_sku': course_run.first_enrollable_paid_seat_sku(), 'first_enrollable_paid_seat_sku': course_run.first_enrollable_paid_seat_sku(),
} }
assert serializer.data == expected
def serialize_course_run(self, course_run):
""" Serializes the given `CourseRun` as a search result. """
result = SearchQuerySet().models(CourseRun).filter(key=course_run.key)[0]
serializer = CourseRunSearchSerializer(result)
return serializer
def test_data_without_serializers(self): class CourseRunSearchModelSerializerTests(CourseRunSearchSerializerTests):
""" Verify a null `LevelType` is properly serialized as None. """ serializer_class = CourseRunSearchModelSerializer
course_run = CourseRunFactory(course__level_type=None)
serializer = self.serialize_course_run(course_run) @classmethod
assert serializer.data['level_type'] is None def get_expected_data(cls, course_run, request):
expected_data = CourseRunWithProgramsSerializerTests.get_expected_data(course_run, request)
expected_data.update({'content_type': 'courserun'})
# This explicit conversion needs to happen, apparently because the real type is DRF's 'ReturnDict'. It's weird.
return dict(expected_data)
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.usefixtures('haystack_default_connection') @pytest.mark.usefixtures('haystack_default_connection')
class TestProgramSearchSerializer: class TestProgramSearchSerializer(TestCase):
def _create_expected_data(self, program): serializer_class = ProgramSearchSerializer
def setUp(self):
super().setUp()
self.request = make_request()
@classmethod
def get_expected_data(cls, program, request): # pylint: disable=unused-argument
return { return {
'uuid': str(program.uuid), 'uuid': str(program.uuid),
'title': program.title, 'title': program.title,
...@@ -1330,81 +1396,89 @@ class TestProgramSearchSerializer: ...@@ -1330,81 +1396,89 @@ class TestProgramSearchSerializer:
'is_program_eligible_for_one_click_purchase': program.is_program_eligible_for_one_click_purchase 'is_program_eligible_for_one_click_purchase': program.is_program_eligible_for_one_click_purchase
} }
def serialize_program(self, program, request):
""" Serializes the given `Program` as a search result. """
result = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0]
serializer = self.serializer_class(result, context={'request': request})
return serializer
def test_data(self): def test_data(self):
authoring_organization, crediting_organization = OrganizationFactory.create_batch(2) authoring_organization, crediting_organization = OrganizationFactory.create_batch(2)
program = ProgramFactory(authoring_organizations=[authoring_organization], program = ProgramFactory(authoring_organizations=[authoring_organization],
credit_backing_organizations=[crediting_organization]) credit_backing_organizations=[crediting_organization])
serializer = self.serialize_program(program, self.request)
# NOTE: This serializer expects SearchQuerySet results, so we run a search on the newly-created object expected = self.get_expected_data(program, self.request)
# to generate such a result.
result = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0]
serializer = ProgramSearchSerializer(result)
expected = self._create_expected_data(program)
assert serializer.data == expected assert serializer.data == expected
def test_data_without_organizations(self): def test_data_without_organizations(self):
""" Verify the serializer serialized programs with no associated organizations. """ Verify the serializer serialized programs with no associated organizations.
In such cases the organizations value should be an empty array. """ In such cases the organizations value should be an empty array. """
program = ProgramFactory(authoring_organizations=[], credit_backing_organizations=[]) program = ProgramFactory(authoring_organizations=[], credit_backing_organizations=[])
serializer = self.serialize_program(program, self.request)
result = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0] expected = self.get_expected_data(program, self.request)
serializer = ProgramSearchSerializer(result)
expected = self._create_expected_data(program)
assert serializer.data == expected assert serializer.data == expected
def test_data_with_languages(self): def test_data_with_languages(self):
""" """
Verify that program languages are serialized. Verify that program languages are serialized.
""" """
course_run = CourseRunFactory( course_run = CourseRunFactory(language=LanguageTag.objects.get(code='en-us'),
language=LanguageTag.objects.get(code='en-us'), authoring_organizations=[OrganizationFactory()])
authoring_organizations=[OrganizationFactory()] CourseRunFactory(course=course_run.course, language=LanguageTag.objects.get(code='zh-cmn'))
)
CourseRunFactory(
course=course_run.course,
language=LanguageTag.objects.get(code='zh-cmn')
)
program = ProgramFactory(courses=[course_run.course]) program = ProgramFactory(courses=[course_run.course])
serializer = self.serialize_program(program, self.request)
expected = self.get_expected_data(program, self.request)
assert serializer.data == expected
if 'language' in expected:
assert {'English', 'Chinese - Mandarin'} == {*expected['language']}
else:
assert {'en-us', 'zh-cmn'} == {*expected['languages']}
result = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0]
serializer = ProgramSearchSerializer(result)
expected = self._create_expected_data(program) class ProgramSearchModelSerializerTest(TestProgramSearchSerializer):
assert serializer.data == expected serializer_class = ProgramSearchModelSerializer
assert {'English', 'Chinese - Mandarin'} == {*expected['language']}
@classmethod
def get_expected_data(cls, program, request):
expected = ProgramSerializerTests.get_expected_data(program, request)
expected.update({'content_type': 'program'})
return expected
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.usefixtures('haystack_default_connection') @pytest.mark.usefixtures('haystack_default_connection')
class TestTypeaheadCourseRunSearchSerializer: class TestTypeaheadCourseRunSearchSerializer:
def test_data(self): serializer_class = TypeaheadCourseRunSearchSerializer
authoring_organization = OrganizationFactory()
course_run = CourseRunFactory(authoring_organizations=[authoring_organization])
serialized_course = self.serialize_course_run(course_run)
expected = { @classmethod
def get_expected_data(cls, course_run):
return {
'key': course_run.key, 'key': course_run.key,
'title': course_run.title, 'title': course_run.title,
'orgs': [org.key for org in course_run.authoring_organizations.all()], 'orgs': [org.key for org in course_run.authoring_organizations.all()],
'marketing_url': course_run.marketing_url, 'marketing_url': course_run.marketing_url,
} }
assert serialized_course.data == expected
def test_data(self):
authoring_organization = OrganizationFactory()
course_run = CourseRunFactory(authoring_organizations=[authoring_organization])
serialized_course = self.serialize_course_run(course_run)
assert serialized_course.data == self.get_expected_data(course_run)
def serialize_course_run(self, course_run): def serialize_course_run(self, course_run):
""" Serializes the given `CourseRun` as a typeahead result. """ """ Serializes the given `CourseRun` as a typeahead result. """
result = SearchQuerySet().models(CourseRun).filter(key=course_run.key)[0] result = SearchQuerySet().models(CourseRun).filter(key=course_run.key)[0]
serializer = TypeaheadCourseRunSearchSerializer(result) serializer = self.serializer_class(result)
return serializer return serializer
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.usefixtures('haystack_default_connection') @pytest.mark.usefixtures('haystack_default_connection')
class TestTypeaheadProgramSearchSerializer: class TestTypeaheadProgramSearchSerializer:
def _create_expected_data(self, program): serializer_class = TypeaheadProgramSearchSerializer
@classmethod
def get_expected_data(cls, program):
return { return {
'uuid': str(program.uuid), 'uuid': str(program.uuid),
'title': program.title, 'title': program.title,
...@@ -1417,7 +1491,7 @@ class TestTypeaheadProgramSearchSerializer: ...@@ -1417,7 +1491,7 @@ class TestTypeaheadProgramSearchSerializer:
authoring_organization = OrganizationFactory() authoring_organization = OrganizationFactory()
program = ProgramFactory(authoring_organizations=[authoring_organization]) program = ProgramFactory(authoring_organizations=[authoring_organization])
serialized_program = self.serialize_program(program) serialized_program = self.serialize_program(program)
expected = self._create_expected_data(program) expected = self.get_expected_data(program)
assert serialized_program.data == expected assert serialized_program.data == expected
def test_data_multiple_authoring_organizations(self): def test_data_multiple_authoring_organizations(self):
...@@ -1430,7 +1504,7 @@ class TestTypeaheadProgramSearchSerializer: ...@@ -1430,7 +1504,7 @@ class TestTypeaheadProgramSearchSerializer:
def serialize_program(self, program): def serialize_program(self, program):
""" Serializes the given `Program` as a typeahead result. """ """ Serializes the given `Program` as a typeahead result. """
result = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0] result = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0]
serializer = TypeaheadProgramSearchSerializer(result) serializer = self.serializer_class(result)
return serializer return serializer
......
import hashlib import hashlib
import logging import logging
import six import six
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -4,16 +4,15 @@ import json ...@@ -4,16 +4,15 @@ import json
import responses import responses
from django.conf import settings from django.conf import settings
from haystack.query import SearchQuerySet
from rest_framework.test import APITestCase as RestAPITestCase from rest_framework.test import APITestCase as RestAPITestCase
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.serializers import ( from course_discovery.apps.api import serializers
CatalogCourseSerializer, CatalogSerializer, CourseRunWithProgramsSerializer,
CourseWithProgramsSerializer, FlattenedCourseRunWithCourseSerializer, MinimalProgramSerializer,
OrganizationSerializer, PersonSerializer, ProgramSerializer, ProgramTypeSerializer, SubjectSerializer,
TopicSerializer
)
from course_discovery.apps.api.tests.mixins import SiteMixin from course_discovery.apps.api.tests.mixins import SiteMixin
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.course_metadata.models import CourseRun, Program
from course_discovery.apps.course_metadata.tests import factories
class SerializationMixin: class SerializationMixin:
...@@ -32,47 +31,69 @@ class SerializationMixin: ...@@ -32,47 +31,69 @@ class SerializationMixin:
context = {'request': self._get_request(format)} context = {'request': self._get_request(format)}
if extra_context: if extra_context:
context.update(extra_context) context.update(extra_context)
return serializer(obj, many=many, context=context).data return serializer(obj, many=many, context=context).data
def _get_search_result(self, model, **kwargs):
return SearchQuerySet().models(model).filter(**kwargs)[0]
def serialize_catalog(self, catalog, many=False, format=None, extra_context=None): def serialize_catalog(self, catalog, many=False, format=None, extra_context=None):
return self._serialize_object(CatalogSerializer, catalog, many, format, extra_context) return self._serialize_object(serializers.CatalogSerializer, catalog, many, format, extra_context)
def serialize_course(self, course, many=False, format=None, extra_context=None): def serialize_course(self, course, many=False, format=None, extra_context=None):
return self._serialize_object(CourseWithProgramsSerializer, course, many, format, extra_context) return self._serialize_object(serializers.CourseWithProgramsSerializer, course, many, format, extra_context)
def serialize_course_run(self, run, many=False, format=None, extra_context=None): def serialize_course_run(self, run, many=False, format=None, extra_context=None):
return self._serialize_object(CourseRunWithProgramsSerializer, run, many, format, extra_context) return self._serialize_object(serializers.CourseRunWithProgramsSerializer, run, many, format, extra_context)
def serialize_course_run_search(self, run, serializer=None):
obj = self._get_search_result(CourseRun, key=run.key)
return self._serialize_object(serializer or serializers.CourseRunSearchSerializer, obj)
def serialize_person(self, person, many=False, format=None, extra_context=None): def serialize_person(self, person, many=False, format=None, extra_context=None):
return self._serialize_object(PersonSerializer, person, many, format, extra_context) return self._serialize_object(serializers.PersonSerializer, person, many, format, extra_context)
def serialize_program(self, program, many=False, format=None, extra_context=None): def serialize_program(self, program, many=False, format=None, extra_context=None):
return self._serialize_object( return self._serialize_object(
MinimalProgramSerializer if many else ProgramSerializer, serializers.MinimalProgramSerializer if many else serializers.ProgramSerializer,
program, program,
many, many,
format, format,
extra_context extra_context
) )
def serialize_program_search(self, program, serializer=None):
obj = self._get_search_result(Program, uuid=program.uuid)
return self._serialize_object(serializer or serializers.ProgramSearchSerializer, obj)
def serialize_program_type(self, program_type, many=False, format=None, extra_context=None): def serialize_program_type(self, program_type, many=False, format=None, extra_context=None):
return self._serialize_object(ProgramTypeSerializer, program_type, many, format, extra_context) return self._serialize_object(serializers.ProgramTypeSerializer, program_type, many, format, extra_context)
def serialize_catalog_course(self, course, many=False, format=None, extra_context=None): def serialize_catalog_course(self, course, many=False, format=None, extra_context=None):
return self._serialize_object(CatalogCourseSerializer, course, many, format, extra_context) return self._serialize_object(serializers.CatalogCourseSerializer, course, many, format, extra_context)
def serialize_catalog_flat_course_run(self, course_run, many=False, format=None, extra_context=None): def serialize_catalog_flat_course_run(self, course_run, many=False, format=None, extra_context=None):
return self._serialize_object(FlattenedCourseRunWithCourseSerializer, course_run, many, format, extra_context) return self._serialize_object(
serializers.FlattenedCourseRunWithCourseSerializer, course_run, many, format, extra_context
)
def serialize_organization(self, organization, many=False, format=None, extra_context=None): def serialize_organization(self, organization, many=False, format=None, extra_context=None):
return self._serialize_object(OrganizationSerializer, organization, many, format, extra_context) return self._serialize_object(serializers.OrganizationSerializer, organization, many, format, extra_context)
def serialize_subject(self, subject, many=False, format=None, extra_context=None): def serialize_subject(self, subject, many=False, format=None, extra_context=None):
return self._serialize_object(SubjectSerializer, subject, many, format, extra_context) return self._serialize_object(serializers.SubjectSerializer, subject, many, format, extra_context)
def serialize_topic(self, topic, many=False, format=None, extra_context=None): def serialize_topic(self, topic, many=False, format=None, extra_context=None):
return self._serialize_object(TopicSerializer, topic, many, format, extra_context) return self._serialize_object(serializers.TopicSerializer, topic, many, format, extra_context)
class TypeaheadSerializationMixin:
def serialize_course_run_search(self, run):
obj = SearchQuerySet().models(CourseRun).filter(key=run.key)[0]
return serializers.TypeaheadCourseRunSearchSerializer(obj).data
def serialize_program_search(self, program):
obj = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0]
return serializers.TypeaheadProgramSearchSerializer(obj).data
class OAuth2Mixin(object): class OAuth2Mixin(object):
...@@ -99,5 +120,54 @@ class OAuth2Mixin(object): ...@@ -99,5 +120,54 @@ class OAuth2Mixin(object):
) )
class SynonymTestMixin:
def test_org_synonyms(self):
""" Test that synonyms work for organization names """
title = 'UniversityX'
authoring_organizations = [factories.OrganizationFactory(name='University')]
factories.CourseRunFactory(
title=title,
course__partner=self.partner,
authoring_organizations=authoring_organizations
)
factories.ProgramFactory(title=title, partner=self.partner, authoring_organizations=authoring_organizations)
response1 = self.process_response({'q': title})
response2 = self.process_response({'q': 'University'})
assert response1 == response2
def test_title_synonyms(self):
""" Test that synonyms work for terms in the title """
factories.CourseRunFactory(title='HTML', course__partner=self.partner)
factories.ProgramFactory(title='HTML', partner=self.partner)
response1 = self.process_response({'q': 'HTML5'})
response2 = self.process_response({'q': 'HTML'})
assert response1 == response2
def test_special_character_synonyms(self):
""" Test that synonyms work with special characters (non ascii) """
factories.ProgramFactory(title='spanish', partner=self.partner)
response1 = self.process_response({'q': 'spanish'})
response2 = self.process_response({'q': 'español'})
assert response1 == response2
def test_stemmed_synonyms(self):
""" Test that synonyms work with stemming from the snowball analyzer """
title = 'Running'
factories.ProgramFactory(title=title, partner=self.partner)
response1 = self.process_response({'q': 'running'})
response2 = self.process_response({'q': 'jogging'})
assert response1 == response2
class LoginMixin:
def setUp(self):
super(LoginMixin, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD)
if getattr(self, 'request'):
self.request.user = self.user
class APITestCase(SiteMixin, RestAPITestCase): class APITestCase(SiteMixin, RestAPITestCase):
pass pass
...@@ -2,7 +2,6 @@ import datetime ...@@ -2,7 +2,6 @@ import datetime
import ddt import ddt
import pytz import pytz
from django.core.cache import cache from django.core.cache import cache
from django.db.models.functions import Lower from django.db.models.functions import Lower
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
......
...@@ -115,7 +115,7 @@ class TestProgramViewSet(SerializationMixin): ...@@ -115,7 +115,7 @@ class TestProgramViewSet(SerializationMixin):
partner=self.partner) partner=self.partner)
# property does not have the right values while being indexed # property does not have the right values while being indexed
del program._course_run_weeks_to_complete del program._course_run_weeks_to_complete
with django_assert_num_queries(38): with django_assert_num_queries(39):
response = self.assert_retrieve_success(program) response = self.assert_retrieve_success(program)
assert response.data == self.serialize_program(program) assert response.data == self.serialize_program(program)
assert course_list == list(program.courses.all()) # pylint: disable=no-member assert course_list == list(program.courses.all()) # pylint: disable=no-member
...@@ -124,7 +124,7 @@ class TestProgramViewSet(SerializationMixin): ...@@ -124,7 +124,7 @@ class TestProgramViewSet(SerializationMixin):
""" Verify the endpoint returns data for a program even if the program's courses have no course runs. """ """ Verify the endpoint returns data for a program even if the program's courses have no course runs. """
course = CourseFactory(partner=self.partner) course = CourseFactory(partner=self.partner)
program = ProgramFactory(courses=[course], partner=self.partner) program = ProgramFactory(courses=[course], partner=self.partner)
with django_assert_num_queries(25): with django_assert_num_queries(26):
response = self.assert_retrieve_success(program) response = self.assert_retrieve_success(program)
assert response.data == self.serialize_program(program) assert response.data == self.serialize_program(program)
......
import datetime import datetime
import json
import urllib.parse import urllib.parse
import ddt import ddt
import pytz import pytz
from django.urls import reverse from django.urls import reverse
from haystack.query import SearchQuerySet
from course_discovery.apps.api.serializers import (CourseRunSearchSerializer, ProgramSearchSerializer, from course_discovery.apps.api import serializers
TypeaheadCourseRunSearchSerializer, TypeaheadProgramSearchSerializer) from course_discovery.apps.api.v1.tests.test_views import mixins
from course_discovery.apps.api.v1.tests.test_views.mixins import APITestCase
from course_discovery.apps.api.v1.views.search import TypeaheadSearchView from course_discovery.apps.api.v1.views.search import TypeaheadSearchView
from course_discovery.apps.core.tests.factories import USER_PASSWORD, PartnerFactory, UserFactory from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.models import CourseRun, Program from course_discovery.apps.course_metadata.models import CourseRun
from course_discovery.apps.course_metadata.tests.factories import (CourseFactory, CourseRunFactory, OrganizationFactory, from course_discovery.apps.course_metadata.tests.factories import (
ProgramFactory) CourseFactory, CourseRunFactory, OrganizationFactory, ProgramFactory
)
class SerializationMixin:
def serialize_course_run(self, course_run):
result = SearchQuerySet().models(CourseRun).filter(key=course_run.key)[0]
return CourseRunSearchSerializer(result).data
def serialize_program(self, program):
result = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0]
return ProgramSearchSerializer(result).data
class TypeaheadSerializationMixin:
def serialize_course_run(self, course_run):
result = SearchQuerySet().models(CourseRun).filter(key=course_run.key)[0]
data = TypeaheadCourseRunSearchSerializer(result).data
return data
def serialize_program(self, program):
result = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0]
data = TypeaheadProgramSearchSerializer(result).data
return data
class LoginMixin:
def setUp(self):
super(LoginMixin, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD)
class SynonymTestMixin:
def test_org_synonyms(self):
""" Test that synonyms work for organization names """
title = 'UniversityX'
authoring_organizations = [OrganizationFactory(name='University')]
CourseRunFactory(
title=title,
course__partner=self.partner,
authoring_organizations=authoring_organizations
)
ProgramFactory(title=title, partner=self.partner, authoring_organizations=authoring_organizations)
response1 = self.process_response({'q': title})
response2 = self.process_response({'q': 'University'})
self.assertDictEqual(response1, response2)
def test_title_synonyms(self):
""" Test that synonyms work for terms in the title """
CourseRunFactory(title='HTML', course__partner=self.partner)
ProgramFactory(title='HTML', partner=self.partner)
response1 = self.process_response({'q': 'HTML5'})
response2 = self.process_response({'q': 'HTML'})
self.assertDictEqual(response1, response2)
def test_special_character_synonyms(self):
""" Test that synonyms work with special characters (non ascii) """
ProgramFactory(title='spanish', partner=self.partner)
response1 = self.process_response({'q': 'spanish'})
response2 = self.process_response({'q': 'español'})
self.assertDictEqual(response1, response2)
def test_stemmed_synonyms(self):
""" Test that synonyms work with stemming from the snowball analyzer """
title = 'Running'
ProgramFactory(title=title, partner=self.partner)
response1 = self.process_response({'q': 'running'})
response2 = self.process_response({'q': 'jogging'})
self.assertDictEqual(response1, response2)
@ddt.ddt @ddt.ddt
class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchTestMixin, class CourseRunSearchViewSetTests(mixins.SerializationMixin, mixins.LoginMixin, ElasticsearchTestMixin,
APITestCase): mixins.APITestCase):
""" Tests for CourseRunSearchViewSet. """ """ Tests for CourseRunSearchViewSet. """
detailed_path = reverse('api:v1:search-course_runs-details')
faceted_path = reverse('api:v1:search-course_runs-facets') faceted_path = reverse('api:v1:search-course_runs-facets')
list_path = reverse('api:v1:search-course_runs-list') list_path = reverse('api:v1:search-course_runs-list')
def get_response(self, query=None, faceted=False): def get_response(self, query=None, path=None):
qs = '' qs = urllib.parse.urlencode({'q': query}) if query else ''
path = path or self.list_path
if query:
qs = urllib.parse.urlencode({'q': query})
path = self.faceted_path if faceted else self.list_path
url = '{path}?{qs}'.format(path=path, qs=qs) url = '{path}?{qs}'.format(path=path, qs=qs)
return self.client.get(url) return self.client.get(url)
def process_response(self, response): def build_facet_url(self, params):
response = self.get_response(response).json() return 'http://testserver.fake{path}?{query}'.format(
self.assertTrue(response['objects']['count']) path=self.faceted_path, query=urllib.parse.urlencode(params)
return response['objects'] )
@ddt.data(True, False)
def test_authentication(self, faceted):
""" Verify the endpoint requires authentication. """
self.client.logout()
response = self.get_response(faceted=faceted)
self.assertEqual(response.status_code, 403)
def test_search(self):
""" Verify the view returns search results. """
self.assert_successful_search(faceted=False)
def test_faceted_search(self):
""" Verify the view returns results and facets. """
course_run, response_data = self.assert_successful_search(faceted=True)
# Validate the pacing facet
expected = {
'text': course_run.pacing_type,
'count': 1,
}
self.assertDictContainsSubset(expected, response_data['fields']['pacing_type'][0])
def assert_successful_search(self, faceted=False): def assert_successful_search(self, path=None, serializer=None):
""" Asserts the search functionality returns results for a generated query. """ """ Asserts the search functionality returns results for a generated query. """
# Generate data that should be indexed and returned by the query # Generate data that should be indexed and returned by the query
course_run = CourseRunFactory(course__partner=self.partner, course__title='Software Testing', course_run = CourseRunFactory(course__partner=self.partner, course__title='Software Testing',
status=CourseRunStatus.Published) status=CourseRunStatus.Published)
response = self.get_response('software', faceted=faceted) response = self.get_response('software', path=path)
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
# Validate the search results # Validate the search results
expected = { expected = {
'count': 1, 'count': 1,
'results': [ 'results': [
self.serialize_course_run(course_run) self.serialize_course_run_search(course_run, serializer=serializer)
] ]
} }
actual = response_data['objects'] if faceted else response_data actual = response_data['objects'] if path == self.faceted_path else response_data
self.assertDictContainsSubset(expected, actual) self.assertDictContainsSubset(expected, actual)
return course_run, response_data return course_run, response_data
def build_facet_url(self, params): def assert_response_includes_availability_facets(self, response_data):
return 'http://testserver.fake{path}?{query}'.format( """ Verifies the query facet counts/URLs are properly rendered. """
path=self.faceted_path, query=urllib.parse.urlencode(params) expected = {
) 'availability_archived': {
'count': 1,
'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_archived'})
},
'availability_current': {
'count': 1,
'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_current'})
},
'availability_starting_soon': {
'count': 1,
'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_starting_soon'})
},
'availability_upcoming': {
'count': 1,
'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_upcoming'})
},
}
self.assertDictContainsSubset(expected, response_data['queries'])
@ddt.data(faceted_path, list_path, detailed_path)
def test_authentication(self, path):
""" Verify the endpoint requires authentication. """
self.client.logout()
response = self.get_response(path=path)
assert response.status_code == 403
@ddt.data(
(list_path, serializers.CourseRunSearchSerializer,),
(detailed_path, serializers.CourseRunSearchModelSerializer,),
)
@ddt.unpack
def test_search(self, path, serializer):
""" Verify the view returns search results. """
self.assert_successful_search(path=path, serializer=serializer)
def test_faceted_search(self):
""" Verify the view returns results and facets. """
course_run, response_data = self.assert_successful_search(path=self.faceted_path)
# Validate the pacing facet
expected = {
'text': course_run.pacing_type,
'count': 1,
}
self.assertDictContainsSubset(expected, response_data['fields']['pacing_type'][0])
def test_invalid_query_facet(self): def test_invalid_query_facet(self):
""" Verify the endpoint returns HTTP 400 if an invalid facet is requested. """ """ Verify the endpoint returns HTTP 400 if an invalid facet is requested. """
...@@ -166,11 +113,11 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -166,11 +113,11 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
url = '{path}?selected_query_facets={facet}'.format(path=self.faceted_path, facet=facet) url = '{path}?selected_query_facets={facet}'.format(path=self.faceted_path, facet=facet)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 400) assert response.status_code == 400
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
expected = {'detail': 'The selected query facet [{facet}] is not valid.'.format(facet=facet)} expected = {'detail': 'The selected query facet [{facet}] is not valid.'.format(facet=facet)}
self.assertEqual(response_data, expected) assert response_data == expected
def test_availability_faceting(self): def test_availability_faceting(self):
""" Verify the endpoint returns availability facets with the results. """ """ Verify the endpoint returns availability facets with the results. """
...@@ -184,18 +131,18 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -184,18 +131,18 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
upcoming = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(days=61), upcoming = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(days=61),
end=now + datetime.timedelta(days=90), status=CourseRunStatus.Published) end=now + datetime.timedelta(days=90), status=CourseRunStatus.Published)
response = self.get_response(faceted=True) response = self.get_response(path=self.faceted_path)
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
# Verify all course runs are returned # Verify all course runs are returned
self.assertEqual(response_data['objects']['count'], 4) assert response_data['objects']['count'] == 4
for run in [archived, current, starting_soon, upcoming]: for run in [archived, current, starting_soon, upcoming]:
serialized = self.serialize_course_run(run) serialized = self.serialize_course_run_search(run)
# Force execution of lazy function. # Force execution of lazy function.
serialized['availability'] = serialized['availability'].strip() serialized['availability'] = serialized['availability'].strip()
self.assertIn(serialized, response_data['objects']['results']) assert serialized in response_data['objects']['results']
self.assert_response_includes_availability_facets(response_data) self.assert_response_includes_availability_facets(response_data)
...@@ -204,39 +151,48 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -204,39 +151,48 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
path=self.faceted_path, facet='availability_archived' path=self.faceted_path, facet='availability_archived'
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
self.assertEqual(response_data['objects']['results'], [self.serialize_course_run(archived)]) assert response_data['objects']['results'] == [self.serialize_course_run_search(archived)]
def assert_response_includes_availability_facets(self, response_data): @ddt.data(
""" Verifies the query facet counts/URLs are properly rendered. """ (list_path, serializers.CourseRunSearchSerializer,
expected = { ['results', 0, 'program_types', 0], ProgramStatus.Deleted, 6),
'availability_archived': { (list_path, serializers.CourseRunSearchSerializer,
'count': 1, ['results', 0, 'program_types', 0], ProgramStatus.Unpublished, 6),
'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_archived'}) (detailed_path, serializers.CourseRunSearchModelSerializer,
}, ['results', 0, 'programs', 0, 'type'], ProgramStatus.Deleted, 35),
'availability_current': { (detailed_path, serializers.CourseRunSearchModelSerializer,
'count': 1, ['results', 0, 'programs', 0, 'type'], ProgramStatus.Unpublished, 36),
'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_current'}) )
}, @ddt.unpack
'availability_starting_soon': { def test_exclude_unavailable_program_types(self, path, serializer, result_location_keys, program_status,
'count': 1, expected_queries):
'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_starting_soon'}) """ Verify that unavailable programs do not show in the program_types representation. """
}, course_run = CourseRunFactory(course__partner=self.partner, course__title='Software Testing',
'availability_upcoming': { status=CourseRunStatus.Published)
'count': 1, active_program = ProgramFactory(courses=[course_run.course], status=ProgramStatus.Active)
'narrow_url': self.build_facet_url({'selected_query_facets': 'availability_upcoming'}) ProgramFactory(courses=[course_run.course], status=program_status)
}, self.reindex_courses(active_program)
}
self.assertDictContainsSubset(expected, response_data['queries']) with self.assertNumQueries(expected_queries):
response = self.get_response('software', path=path)
assert response.status_code == 200
response_data = response.json()
def test_exclude_deleted_program_types(self): # Validate the search results
""" Verify the deleted programs do not show in the program_types representation. """ expected = {
self._test_exclude_program_types(ProgramStatus.Deleted) 'count': 1,
'results': [
self.serialize_course_run_search(course_run, serializer=serializer)
]
}
self.assertDictContainsSubset(expected, response_data)
def test_exclude_unpublished_program_types(self): # Check that the program is indeed the active one.
""" Verify the unpublished programs do not show in the program_types representation. """ for key in result_location_keys:
self._test_exclude_program_types(ProgramStatus.Unpublished) response_data = response_data[key]
assert response_data == active_program.type.name
@ddt.data( @ddt.data(
[{'title': 'Software Testing', 'excluded': True}], [{'title': 'Software Testing', 'excluded': True}],
...@@ -268,49 +224,25 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -268,49 +224,25 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
self.reindex_courses(program) self.reindex_courses(program)
with self.assertNumQueries(5): with self.assertNumQueries(5):
response = self.get_response('software', faceted=False) response = self.get_response('software', path=self.list_path)
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response_data = response.json() response_data = response.json()
self.assertEqual(response_data['count'], len(course_run_list)) assert response_data['count'] == len(course_run_list)
for result in response_data['results']: for result in response_data['results']:
for course_run in excluded_course_run_list: for course_run in excluded_course_run_list:
if result.get('title') == course_run.title: if result.get('title') == course_run.title:
self.assertEqual(result.get('program_types'), []) assert result.get('program_types') == []
for course_run in non_excluded_course_run_list: for course_run in non_excluded_course_run_list:
if result.get('title') == course_run.title: if result.get('title') == course_run.title:
self.assertEqual(result.get('program_types'), course_run.program_types) assert result.get('program_types') == course_run.program_types
def _test_exclude_program_types(self, program_status):
""" Verify that programs with the provided type do not show in the program_types representation. """
course_run = CourseRunFactory(course__partner=self.partner, course__title='Software Testing',
status=CourseRunStatus.Published)
active_program = ProgramFactory(courses=[course_run.course], status=ProgramStatus.Active)
ProgramFactory(courses=[course_run.course], status=program_status)
self.reindex_courses(active_program)
with self.assertNumQueries(6):
response = self.get_response('software', faceted=False)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content.decode('utf-8'))
# Validate the search results
expected = {
'count': 1,
'results': [
self.serialize_course_run(course_run)
]
}
self.assertDictContainsSubset(expected, response_data)
self.assertEqual(response_data['results'][0].get('program_types'), [active_program.type.name])
@ddt.ddt @ddt.ddt
class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchTestMixin, class AggregateSearchViewSetTests(mixins.SerializationMixin, mixins.LoginMixin, ElasticsearchTestMixin,
SynonymTestMixin, APITestCase): mixins.SynonymTestMixin, mixins.APITestCase):
path = reverse('api:v1:search-all-facets') path = reverse('api:v1:search-all-facets')
def get_response(self, query=None): def get_response(self, query=None):
...@@ -338,26 +270,21 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -338,26 +270,21 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active) program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active)
response = self.get_response() response = self.get_response()
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
self.assertListEqual( assert response_data['objects']['results'] == \
response_data['objects']['results'], [self.serialize_program_search(program), self.serialize_course_run_search(course_run)]
[self.serialize_program(program), self.serialize_course_run(course_run)]
)
def test_hidden_runs_excluded(self): def test_hidden_runs_excluded(self):
"""Search results should not include hidden runs.""" """Search results should not include hidden runs."""
visible_run = CourseRunFactory(course__partner=self.partner) visible_run = CourseRunFactory(course__partner=self.partner)
hidden_run = CourseRunFactory(course__partner=self.partner, hidden=True) hidden_run = CourseRunFactory(course__partner=self.partner, hidden=True)
self.assertEqual(CourseRun.objects.get(hidden=True), hidden_run) assert CourseRun.objects.get(hidden=True) == hidden_run
response = self.get_response() response = self.get_response()
data = json.loads(response.content.decode('utf-8')) data = response.json()
self.assertEqual( assert data['objects']['results'] == [self.serialize_course_run_search(visible_run)]
data['objects']['results'],
[self.serialize_course_run(visible_run)]
)
def test_results_filtered_by_default_partner(self): def test_results_filtered_by_default_partner(self):
""" Verify the search results only include items related to the default partner if no partner is """ Verify the search results only include items related to the default partner if no partner is
...@@ -369,23 +296,21 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -369,23 +296,21 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
other_partner = PartnerFactory() other_partner = PartnerFactory()
other_course_run = CourseRunFactory(course__partner=other_partner, status=CourseRunStatus.Published) other_course_run = CourseRunFactory(course__partner=other_partner, status=CourseRunStatus.Published)
other_program = ProgramFactory(partner=other_partner, status=ProgramStatus.Active) other_program = ProgramFactory(partner=other_partner, status=ProgramStatus.Active)
self.assertNotEqual(other_program.partner.short_code, self.partner.short_code) assert other_program.partner.short_code != self.partner.short_code
self.assertNotEqual(other_course_run.course.partner.short_code, self.partner.short_code) assert other_course_run.course.partner.short_code != self.partner.short_code
response = self.get_response() response = self.get_response()
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
self.assertListEqual( assert response_data['objects']['results'] == \
response_data['objects']['results'], [self.serialize_program_search(program), self.serialize_course_run_search(course_run)]
[self.serialize_program(program), self.serialize_course_run(course_run)]
)
# Filter results by partner # Filter results by partner
response = self.get_response({'partner': other_partner.short_code}) response = self.get_response({'partner': other_partner.short_code})
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
self.assertListEqual(response_data['objects']['results'], assert response_data['objects']['results'] == \
[self.serialize_program(other_program), self.serialize_course_run(other_course_run)]) [self.serialize_program_search(other_program), self.serialize_course_run_search(other_course_run)]
def test_empty_query(self): def test_empty_query(self):
""" Verify, when the query (q) parameter is empty, the endpoint behaves as if the parameter """ Verify, when the query (q) parameter is empty, the endpoint behaves as if the parameter
...@@ -394,10 +319,10 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -394,10 +319,10 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active) program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active)
response = self.get_response({'q': '', 'content_type': ['courserun', 'program']}) response = self.get_response({'q': '', 'content_type': ['courserun', 'program']})
self.assertEqual(response.status_code, 200) assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
self.assertListEqual(response_data['objects']['results'], assert response_data['objects']['results'] == \
[self.serialize_program(program), self.serialize_course_run(course_run)]) [self.serialize_program_search(program), self.serialize_course_run_search(course_run)]
@ddt.data('start', '-start') @ddt.data('start', '-start')
def test_results_ordered_by_start_date(self, ordering): def test_results_ordered_by_start_date(self, ordering):
...@@ -410,12 +335,12 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -410,12 +335,12 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
course_run_keys = [course_run.key for course_run in [archived, current, starting_soon, upcoming]] course_run_keys = [course_run.key for course_run in [archived, current, starting_soon, upcoming]]
response = self.get_response({"ordering": ordering}) response = self.get_response({"ordering": ordering})
self.assertEqual(response.status_code, 200) assert response.status_code == 200
self.assertEqual(response.data['objects']['count'], 4) assert response.data['objects']['count'] == 4
course_runs = CourseRun.objects.filter(key__in=course_run_keys).order_by(ordering) course_runs = CourseRun.objects.filter(key__in=course_run_keys).order_by(ordering)
expected = [self.serialize_course_run(course_run) for course_run in course_runs] expected = [self.serialize_course_run_search(course_run) for course_run in course_runs]
self.assertEqual(response.data['objects']['results'], expected) assert response.data['objects']['results'] == expected
def test_results_include_aggregation_key(self): def test_results_include_aggregation_key(self):
""" Verify the search results only include the aggregation_key for each document. """ """ Verify the search results only include the aggregation_key for each document. """
...@@ -424,7 +349,7 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -424,7 +349,7 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
response = self.get_response() response = self.get_response()
assert response.status_code == 200 assert response.status_code == 200
response_data = json.loads(response.content.decode('utf-8')) response_data = response.json()
expected = sorted( expected = sorted(
['courserun:{}'.format(course_run.course.key), 'program:{}'.format(program.uuid)] ['courserun:{}'.format(course_run.course.key), 'program:{}'.format(program.uuid)]
...@@ -435,8 +360,8 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT ...@@ -435,8 +360,8 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
assert expected == actual assert expected == actual
class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, ElasticsearchTestMixin, class TypeaheadSearchViewTests(mixins.TypeaheadSerializationMixin, mixins.LoginMixin, ElasticsearchTestMixin,
SynonymTestMixin, APITestCase): mixins.SynonymTestMixin, mixins.APITestCase):
path = reverse('api:v1:search-typeahead') path = reverse('api:v1:search-typeahead')
def get_response(self, query=None, partner=None): def get_response(self, query=None, partner=None):
...@@ -460,8 +385,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics ...@@ -460,8 +385,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response = self.get_response({'q': title}) response = self.get_response({'q': title})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response_data = response.json() response_data = response.json()
self.assertDictEqual(response_data, {'course_runs': [self.serialize_course_run(course_run)], self.assertDictEqual(response_data, {'course_runs': [self.serialize_course_run_search(course_run)],
'programs': [self.serialize_program(program)]}) 'programs': [self.serialize_program_search(program)]})
def test_typeahead_multiple_results(self): def test_typeahead_multiple_results(self):
""" Verify the typeahead responses always returns a limited number of results, even if there are more hits. """ """ Verify the typeahead responses always returns a limited number of results, even if there are more hits. """
...@@ -512,8 +437,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics ...@@ -512,8 +437,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response = self.get_response({'q': title}) response = self.get_response({'q': title})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response_data = response.json() response_data = response.json()
self.assertDictEqual(response_data, {'course_runs': [self.serialize_course_run(course_run)], self.assertDictEqual(response_data, {'course_runs': [self.serialize_course_run_search(course_run)],
'programs': [self.serialize_program(program)]}) 'programs': [self.serialize_program_search(program)]})
def test_partial_term_search(self): def test_partial_term_search(self):
""" Test typeahead response with partial term search. """ """ Test typeahead response with partial term search. """
...@@ -525,8 +450,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics ...@@ -525,8 +450,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response_data = response.json() response_data = response.json()
expected_response_data = { expected_response_data = {
'course_runs': [self.serialize_course_run(course_run)], 'course_runs': [self.serialize_course_run_search(course_run)],
'programs': [self.serialize_program(program)] 'programs': [self.serialize_program_search(program)]
} }
self.assertDictEqual(response_data, expected_response_data) self.assertDictEqual(response_data, expected_response_data)
...@@ -544,8 +469,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics ...@@ -544,8 +469,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response_data = response.json() response_data = response.json()
expected_response_data = { expected_response_data = {
'course_runs': [self.serialize_course_run(course_run)], 'course_runs': [self.serialize_course_run_search(course_run)],
'programs': [self.serialize_program(program)] 'programs': [self.serialize_program_search(program)]
} }
self.assertDictEqual(response_data, expected_response_data) self.assertDictEqual(response_data, expected_response_data)
...@@ -559,7 +484,7 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics ...@@ -559,7 +484,7 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response_data = response.json() response_data = response.json()
expected_response_data = { expected_response_data = {
'course_runs': [], 'course_runs': [],
'programs': [self.serialize_program(program)] 'programs': [self.serialize_program_search(program)]
} }
self.assertDictEqual(response_data, expected_response_data) self.assertDictEqual(response_data, expected_response_data)
...@@ -579,8 +504,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics ...@@ -579,8 +504,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response = self.get_response({'q': partial_key}) response = self.get_response({'q': partial_key})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected = { expected = {
'course_runs': [self.serialize_course_run(course_run)], 'course_runs': [self.serialize_course_run_search(course_run)],
'programs': [self.serialize_program(program)] 'programs': [self.serialize_program_search(program)]
} }
self.assertDictEqual(response.data, expected) self.assertDictEqual(response.data, expected)
...@@ -611,9 +536,9 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics ...@@ -611,9 +536,9 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response = self.get_response({'q': 'mit'}) response = self.get_response({'q': 'mit'})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected = { expected = {
'course_runs': [self.serialize_course_run(mit_run), 'course_runs': [self.serialize_course_run_search(mit_run),
self.serialize_course_run(harvard_run)], self.serialize_course_run_search(harvard_run)],
'programs': [self.serialize_program(mit_program), 'programs': [self.serialize_program_search(mit_program),
self.serialize_program(harvard_program)] self.serialize_program_search(harvard_program)]
} }
self.assertDictEqual(response.data, expected) self.assertDictEqual(response.data, expected)
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from course_discovery.apps.api import serializers from course_discovery.apps.api import serializers
from course_discovery.apps.api.exceptions import InvalidPartnerError
from course_discovery.apps.core.models import Partner
User = get_user_model() User = get_user_model()
......
...@@ -11,12 +11,12 @@ from rest_framework.permissions import IsAuthenticated ...@@ -11,12 +11,12 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from course_discovery.apps.api import filters, serializers from course_discovery.apps.api import filters, mixins, serializers
from course_discovery.apps.course_metadata.choices import ProgramStatus from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Course, CourseRun, Program from course_discovery.apps.course_metadata.models import Course, CourseRun, Program
class BaseHaystackViewSet(FacetMixin, HaystackViewSet): class BaseHaystackViewSet(mixins.DetailMixin, FacetMixin, HaystackViewSet):
document_uid_field = 'key' document_uid_field = 'key'
facet_filter_backends = [filters.HaystackFacetFilterWithQueries, filters.HaystackFilter, OrderingFilter] facet_filter_backends = [filters.HaystackFacetFilterWithQueries, filters.HaystackFilter, OrderingFilter]
ordering_fields = ('start',) ordering_fields = ('start',)
...@@ -93,27 +93,31 @@ class BaseHaystackViewSet(FacetMixin, HaystackViewSet): ...@@ -93,27 +93,31 @@ class BaseHaystackViewSet(FacetMixin, HaystackViewSet):
class CourseSearchViewSet(BaseHaystackViewSet): class CourseSearchViewSet(BaseHaystackViewSet):
facet_serializer_class = serializers.CourseFacetSerializer
index_models = (Course,) index_models = (Course,)
detail_serializer_class = serializers.CourseSearchModelSerializer
facet_serializer_class = serializers.CourseFacetSerializer
serializer_class = serializers.CourseSearchSerializer serializer_class = serializers.CourseSearchSerializer
class CourseRunSearchViewSet(BaseHaystackViewSet): class CourseRunSearchViewSet(BaseHaystackViewSet):
facet_serializer_class = serializers.CourseRunFacetSerializer
index_models = (CourseRun,) index_models = (CourseRun,)
detail_serializer_class = serializers.CourseRunSearchModelSerializer
facet_serializer_class = serializers.CourseRunFacetSerializer
serializer_class = serializers.CourseRunSearchSerializer serializer_class = serializers.CourseRunSearchSerializer
class ProgramSearchViewSet(BaseHaystackViewSet): class ProgramSearchViewSet(BaseHaystackViewSet):
document_uid_field = 'uuid' document_uid_field = 'uuid'
lookup_field = 'uuid' lookup_field = 'uuid'
facet_serializer_class = serializers.ProgramFacetSerializer
index_models = (Program,) index_models = (Program,)
detail_serializer_class = serializers.ProgramSearchModelSerializer
facet_serializer_class = serializers.ProgramFacetSerializer
serializer_class = serializers.ProgramSearchSerializer serializer_class = serializers.ProgramSearchSerializer
class AggregateSearchViewSet(BaseHaystackViewSet): class AggregateSearchViewSet(BaseHaystackViewSet):
""" Search all content types. """ """ Search all content types. """
detail_serializer_class = serializers.AggregateSearchModelSerializer
facet_serializer_class = serializers.AggregateFacetSearchSerializer facet_serializer_class = serializers.AggregateFacetSearchSerializer
serializer_class = serializers.AggregateSearchSerializer serializer_class = serializers.AggregateSearchSerializer
......
...@@ -4,7 +4,6 @@ from __future__ import unicode_literals ...@@ -4,7 +4,6 @@ from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
SWITCH = 'use_company_name_as_utm_source_value' SWITCH = 'use_company_name_as_utm_source_value'
......
...@@ -3,7 +3,6 @@ import logging ...@@ -3,7 +3,6 @@ import logging
import pytest import pytest
import responses import responses
from django.conf import settings from django.conf import settings
from haystack import connections as haystack_connections from haystack import connections as haystack_connections
......
...@@ -26,15 +26,13 @@ from taggit_autosuggest.managers import TaggableManager ...@@ -26,15 +26,13 @@ from taggit_autosuggest.managers import TaggableManager
from course_discovery.apps.core.models import Currency, Partner from course_discovery.apps.core.models import Currency, Partner
from course_discovery.apps.course_metadata.choices import CourseRunPacing, CourseRunStatus, ProgramStatus, ReportingType from course_discovery.apps.course_metadata.choices import CourseRunPacing, CourseRunStatus, ProgramStatus, ReportingType
from course_discovery.apps.course_metadata.publishers import ( from course_discovery.apps.course_metadata.publishers import (
CourseRunMarketingSitePublisher, CourseRunMarketingSitePublisher, ProgramMarketingSitePublisher
ProgramMarketingSitePublisher
) )
from course_discovery.apps.course_metadata.query import CourseQuerySet, CourseRunQuerySet, ProgramQuerySet from course_discovery.apps.course_metadata.query import CourseQuerySet, CourseRunQuerySet, ProgramQuerySet
from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath, clean_query, custom_render_variations from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath, clean_query, custom_render_variations
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.utils import VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY from course_discovery.apps.publisher.utils import VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -8,12 +8,7 @@ from django.utils.text import slugify ...@@ -8,12 +8,7 @@ from django.utils.text import slugify
from course_discovery.apps.course_metadata.choices import CourseRunStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus
from course_discovery.apps.course_metadata.exceptions import ( from course_discovery.apps.course_metadata.exceptions import (
AliasCreateError, AliasCreateError, AliasDeleteError, FormRetrievalError, NodeCreateError, NodeDeleteError, NodeEditError,
AliasDeleteError,
FormRetrievalError,
NodeCreateError,
NodeDeleteError,
NodeEditError,
NodeLookupError NodeLookupError
) )
from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClient from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClient
......
...@@ -6,6 +6,26 @@ from opaque_keys.edx.keys import CourseKey ...@@ -6,6 +6,26 @@ from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.models import Course, CourseRun, Program from course_discovery.apps.course_metadata.models import Course, CourseRun, Program
BASE_SEARCH_INDEX_FIELDS = (
'aggregation_key',
'content_type',
'text',
)
BASE_PROGRAM_FIELDS = (
'card_image_url',
'language',
'marketing_url',
'partner',
'published',
'status',
'subtitle',
'text',
'title',
'type',
'uuid'
)
# http://django-haystack.readthedocs.io/en/v2.5.0/boost.html#field-boost # http://django-haystack.readthedocs.io/en/v2.5.0/boost.html#field-boost
# Boost title over all other parameters (multiplicative) # Boost title over all other parameters (multiplicative)
# The max boost received from our boosting functions is ~6. # The max boost received from our boosting functions is ~6.
......
...@@ -270,6 +270,7 @@ class ProgramFactory(factory.django.DjangoModelFactory): ...@@ -270,6 +270,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
banner_image_url = FuzzyText(prefix='https://example.com/program/banner') banner_image_url = FuzzyText(prefix='https://example.com/program/banner')
card_image_url = FuzzyText(prefix='https://example.com/program/card') card_image_url = FuzzyText(prefix='https://example.com/program/card')
partner = factory.SubFactory(PartnerFactory) partner = factory.SubFactory(PartnerFactory)
video = factory.SubFactory(VideoFactory)
overview = FuzzyText() overview = FuzzyText()
total_hours_of_effort = FuzzyInteger(2) total_hours_of_effort = FuzzyInteger(2)
weeks_to_complete = FuzzyInteger(1) weeks_to_complete = FuzzyInteger(1)
......
...@@ -7,18 +7,11 @@ import responses ...@@ -7,18 +7,11 @@ import responses
from course_discovery.apps.core.tests.factories import PartnerFactory from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.exceptions import ( from course_discovery.apps.course_metadata.exceptions import (
AliasCreateError, AliasCreateError, AliasDeleteError, FormRetrievalError, NodeCreateError, NodeDeleteError, NodeEditError,
AliasDeleteError,
FormRetrievalError,
NodeCreateError,
NodeDeleteError,
NodeEditError,
NodeLookupError NodeLookupError
) )
from course_discovery.apps.course_metadata.publishers import ( from course_discovery.apps.course_metadata.publishers import (
BaseMarketingSitePublisher, BaseMarketingSitePublisher, CourseRunMarketingSitePublisher, ProgramMarketingSitePublisher
CourseRunMarketingSitePublisher,
ProgramMarketingSitePublisher
) )
from course_discovery.apps.course_metadata.tests import toggle_switch from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ProgramFactory from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ProgramFactory
......
...@@ -4,10 +4,10 @@ import urllib.parse ...@@ -4,10 +4,10 @@ import urllib.parse
import pytz import pytz
from django.urls import reverse from django.urls import reverse
from course_discovery.apps.api.v1.tests.test_views.mixins import APITestCase from course_discovery.apps.api.v1.tests.test_views.mixins import (
from course_discovery.apps.api.v1.tests.test_views.test_search import ( APITestCase, LoginMixin, SerializationMixin, SynonymTestMixin
ElasticsearchTestMixin, LoginMixin, SerializationMixin, SynonymTestMixin
) )
from course_discovery.apps.api.v1.tests.test_views.test_search import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.tests.factories import CourseFactory, CourseRunFactory, ProgramFactory from course_discovery.apps.course_metadata.tests.factories import CourseFactory, CourseRunFactory, ProgramFactory
from course_discovery.apps.edx_catalog_extensions.api.serializers import DistinctCountsAggregateFacetSearchSerializer from course_discovery.apps.edx_catalog_extensions.api.serializers import DistinctCountsAggregateFacetSearchSerializer
...@@ -125,9 +125,9 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin, ...@@ -125,9 +125,9 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin,
for record in objects['results']: for record in objects['results']:
if record['content_type'] == 'courserun': if record['content_type'] == 'courserun':
assert record == self.serialize_course_run(course_runs[str(record['key'])]) assert record == self.serialize_course_run_search(course_runs[str(record['key'])])
else: else:
assert record == self.serialize_program(programs[str(record['uuid'])]) assert record == self.serialize_program_search(programs[str(record['uuid'])])
def test_response_with_search_query(self): def test_response_with_search_query(self):
""" Verify that the response is accurate when a search query is passed.""" """ Verify that the response is accurate when a search query is passed."""
......
import elasticsearch import elasticsearch
from django.conf import settings from django.conf import settings
from haystack.backends.elasticsearch_backend import ElasticsearchSearchQuery from haystack.backends.elasticsearch_backend import ElasticsearchSearchQuery
from haystack.models import SearchResult from haystack.models import SearchResult
......
from django.conf import settings from django.conf import settings
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from course_discovery.apps.edx_haystack_extensions.distinct_counts.backends import DistinctCountsSearchQuery from course_discovery.apps.edx_haystack_extensions.distinct_counts.backends import DistinctCountsSearchQuery
......
...@@ -4,7 +4,6 @@ from django.conf import settings ...@@ -4,7 +4,6 @@ from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from haystack import connections as haystack_connections from haystack import connections as haystack_connections
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -6,16 +6,18 @@ from simple_history.admin import SimpleHistoryAdmin ...@@ -6,16 +6,18 @@ from simple_history.admin import SimpleHistoryAdmin
from course_discovery.apps.publisher.assign_permissions import assign_permissions from course_discovery.apps.publisher.assign_permissions import assign_permissions
from course_discovery.apps.publisher.choices import InternalUserRole from course_discovery.apps.publisher.choices import InternalUserRole
from course_discovery.apps.publisher.constants import (INTERNAL_USER_GROUP_NAME, PARTNER_MANAGER_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PROJECT_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, INTERNAL_USER_GROUP_NAME, PARTNER_MANAGER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME,
REVIEWER_GROUP_NAME) REVIEWER_GROUP_NAME
)
from course_discovery.apps.publisher.forms import ( from course_discovery.apps.publisher.forms import (
CourseRunAdminForm, CourseRunStateAdminForm, CourseStateAdminForm, OrganizationExtensionForm, CourseRunAdminForm, CourseRunStateAdminForm, CourseStateAdminForm, OrganizationExtensionForm,
PublisherUserCreationForm, UserAttributesAdminForm PublisherUserCreationForm, UserAttributesAdminForm
) )
from course_discovery.apps.publisher.models import (Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, from course_discovery.apps.publisher.models import (
CourseUserRole, OrganizationExtension, OrganizationUserRole, Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension,
PublisherUser, Seat, UserAttributes) OrganizationUserRole, PublisherUser, Seat, UserAttributes
)
@admin.register(CourseUserRole) @admin.register(CourseUserRole)
......
...@@ -13,9 +13,10 @@ from rest_framework import serializers ...@@ -13,9 +13,10 @@ from rest_framework import serializers
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.emails import (send_change_role_assignment_email, from course_discovery.apps.publisher.emails import (
send_email_for_studio_instance_created, send_email_preview_accepted, send_change_role_assignment_email, send_email_for_studio_instance_created, send_email_preview_accepted,
send_email_preview_page_is_available) send_email_preview_page_is_available
)
from course_discovery.apps.publisher.models import CourseRun, CourseRunState, CourseState, CourseUserRole from course_discovery.apps.publisher.models import CourseRun, CourseRunState, CourseState, CourseUserRole
......
...@@ -11,14 +11,16 @@ from course_discovery.apps.core.tests.helpers import make_image_file ...@@ -11,14 +11,16 @@ from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.tests import toggle_switch from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunSerializer, from course_discovery.apps.publisher.api.serializers import (
CourseRunStateSerializer, CourseStateSerializer, CourseRevisionSerializer, CourseRunSerializer, CourseRunStateSerializer, CourseStateSerializer,
CourseUserRoleSerializer, GroupUserSerializer) CourseUserRoleSerializer, GroupUserSerializer
)
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.models import CourseRun, CourseState, Seat from course_discovery.apps.publisher.models import CourseRun, CourseState, Seat
from course_discovery.apps.publisher.tests.factories import (CourseFactory, CourseRunFactory, CourseRunStateFactory, from course_discovery.apps.publisher.tests.factories import (
CourseStateFactory, CourseUserRoleFactory, CourseFactory, CourseRunFactory, CourseRunStateFactory, CourseStateFactory, CourseUserRoleFactory,
OrganizationExtensionFactory, SeatFactory) OrganizationExtensionFactory, SeatFactory
)
class CourseUserRoleSerializerTests(SiteMixin, TestCase): class CourseUserRoleSerializerTests(SiteMixin, TestCase):
......
import pytest import pytest
from course_discovery.apps.core.utils import serialize_datetime from course_discovery.apps.core.utils import serialize_datetime
from course_discovery.apps.publisher.api.utils import (serialize_entitlement_for_ecommerce_api, from course_discovery.apps.publisher.api.utils import (
serialize_seat_for_ecommerce_api) serialize_entitlement_for_ecommerce_api, serialize_seat_for_ecommerce_api
)
from course_discovery.apps.publisher.models import Seat from course_discovery.apps.publisher.models import Seat
from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, SeatFactory from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, SeatFactory
......
...@@ -22,8 +22,9 @@ from course_discovery.apps.ietf_language_tags.models import LanguageTag ...@@ -22,8 +22,9 @@ from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api import views from course_discovery.apps.publisher.api import views
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, from course_discovery.apps.publisher.models import (
OrganizationExtension, Seat) Course, CourseRun, CourseRunState, CourseState, OrganizationExtension, Seat
)
from course_discovery.apps.publisher.tests import JSON_CONTENT_TYPE, factories from course_discovery.apps.publisher.tests import JSON_CONTENT_TYPE, factories
......
""" Publisher API URLs. """ """ Publisher API URLs. """
from django.conf.urls import include, url from django.conf.urls import include, url
from course_discovery.apps.publisher.api.views import (AcceptAllRevisionView, ChangeCourseRunStateView, from course_discovery.apps.publisher.api.views import (
ChangeCourseStateView, CourseRevisionDetailView, AcceptAllRevisionView, ChangeCourseRunStateView, ChangeCourseStateView, CourseRevisionDetailView,
CourseRoleAssignmentView, CoursesAutoComplete, CourseRoleAssignmentView, CoursesAutoComplete, OrganizationGroupUserView, RevertCourseRevisionView,
OrganizationGroupUserView, RevertCourseRevisionView, UpdateCourseRunView
UpdateCourseRunView) )
urlpatterns = [ urlpatterns = [
url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'), url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'),
......
...@@ -18,8 +18,9 @@ from course_discovery.apps.course_metadata.models import Seat as DiscoverySeat ...@@ -18,8 +18,9 @@ from course_discovery.apps.course_metadata.models import Seat as DiscoverySeat
from course_discovery.apps.course_metadata.models import CourseRun, SeatType, Video from course_discovery.apps.course_metadata.models import CourseRun, SeatType, Video
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api.utils import (serialize_entitlement_for_ecommerce_api, from course_discovery.apps.publisher.api.utils import (
serialize_seat_for_ecommerce_api) serialize_entitlement_for_ecommerce_api, serialize_seat_for_ecommerce_api
)
from course_discovery.apps.publisher.api.v1.views import CourseRunViewSet from course_discovery.apps.publisher.api.v1.views import CourseRunViewSet
from course_discovery.apps.publisher.models import CourseEntitlement, Seat from course_discovery.apps.publisher.models import CourseEntitlement, Seat
from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, CourseRunFactory, SeatFactory from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, CourseRunFactory, SeatFactory
......
...@@ -11,14 +11,17 @@ from rest_framework.views import APIView ...@@ -11,14 +11,17 @@ from rest_framework.views import APIView
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.api.paginations import LargeResultsSetPagination from course_discovery.apps.publisher.api.paginations import LargeResultsSetPagination
from course_discovery.apps.publisher.api.permissions import (CanViewAssociatedCourse, InternalUserPermission, from course_discovery.apps.publisher.api.permissions import (
PublisherUserPermission) CanViewAssociatedCourse, InternalUserPermission, PublisherUserPermission
from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunSerializer, )
CourseRunStateSerializer, CourseStateSerializer, from course_discovery.apps.publisher.api.serializers import (
CourseUserRoleSerializer, GroupUserSerializer) CourseRevisionSerializer, CourseRunSerializer, CourseRunStateSerializer, CourseStateSerializer,
CourseUserRoleSerializer, GroupUserSerializer
)
from course_discovery.apps.publisher.forms import CourseForm from course_discovery.apps.publisher.forms import CourseForm
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, CourseUserRole, from course_discovery.apps.publisher.models import (
OrganizationExtension, PublisherUser) Course, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension, PublisherUser
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from course_discovery.apps.publisher.constants import (GENERAL_STAFF_GROUP_NAME, LEGAL_TEAM_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PARTNER_SUPPORT_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, GENERAL_STAFF_GROUP_NAME, LEGAL_TEAM_GROUP_NAME, PARTNER_SUPPORT_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME,
REVIEWER_GROUP_NAME) REVIEWER_GROUP_NAME
)
from course_discovery.apps.publisher.models import OrganizationExtension from course_discovery.apps.publisher.models import OrganizationExtension
......
...@@ -3,8 +3,9 @@ from __future__ import unicode_literals ...@@ -3,8 +3,9 @@ from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
from course_discovery.apps.publisher.constants import (PARTNER_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, from course_discovery.apps.publisher.constants import (
REVIEWER_GROUP_NAME) PARTNER_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME
)
GROUPS = [PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME, PUBLISHER_GROUP_NAME] GROUPS = [PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME, PUBLISHER_GROUP_NAME]
......
...@@ -4,8 +4,10 @@ from __future__ import unicode_literals ...@@ -4,8 +4,10 @@ from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
from course_discovery.apps.publisher.constants import (INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME) INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME
)
GROUPS = [INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME, PUBLISHER_GROUP_NAME] GROUPS = [INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME, PUBLISHER_GROUP_NAME]
......
...@@ -29,7 +29,6 @@ from course_discovery.apps.publisher.choices import ( ...@@ -29,7 +29,6 @@ from course_discovery.apps.publisher.choices import (
CourseRunStateChoices, CourseStateChoices, InternalUserRole, PublisherUserRole CourseRunStateChoices, CourseStateChoices, InternalUserRole, PublisherUserRole
) )
from course_discovery.apps.publisher.utils import is_email_notification_enabled, is_internal_user, is_publisher_admin from course_discovery.apps.publisher.utils import is_email_notification_enabled, is_internal_user, is_publisher_admin
from course_discovery.apps.publisher.validators import ImageMultiSizeValidator from course_discovery.apps.publisher.validators import ImageMultiSizeValidator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -12,9 +12,10 @@ from course_discovery.apps.course_metadata.choices import CourseRunPacing ...@@ -12,9 +12,10 @@ from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.models import (Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, from course_discovery.apps.publisher.models import (
CourseUserRole, OrganizationExtension, OrganizationUserRole, Seat, Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension,
UserAttributes) OrganizationUserRole, Seat, UserAttributes
)
class CourseFactory(factory.DjangoModelFactory): class CourseFactory(factory.DjangoModelFactory):
......
...@@ -8,8 +8,9 @@ from course_discovery.apps.api.tests.mixins import SiteMixin ...@@ -8,8 +8,9 @@ from course_discovery.apps.api.tests.mixins import SiteMixin
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.constants import (PARTNER_MANAGER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME) PARTNER_MANAGER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME
)
from course_discovery.apps.publisher.forms import CourseRunAdminForm from course_discovery.apps.publisher.forms import CourseRunAdminForm
from course_discovery.apps.publisher.models import CourseRun, OrganizationExtension from course_discovery.apps.publisher.models import CourseRun, OrganizationExtension
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
......
...@@ -16,8 +16,9 @@ from course_discovery.apps.course_metadata.tests.factories import OrganizationFa ...@@ -16,8 +16,9 @@ from course_discovery.apps.course_metadata.tests.factories import OrganizationFa
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.mixins import check_course_organization_permission from course_discovery.apps.publisher.mixins import check_course_organization_permission
from course_discovery.apps.publisher.models import (Course, CourseUserRole, OrganizationExtension, from course_discovery.apps.publisher.models import (
OrganizationUserRole, Seat) Course, CourseUserRole, OrganizationExtension, OrganizationUserRole, Seat
)
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
......
...@@ -9,16 +9,18 @@ from guardian.shortcuts import assign_perm ...@@ -9,16 +9,18 @@ from guardian.shortcuts import assign_perm
from mock import Mock from mock import Mock
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.constants import (ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PROJECT_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME) ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME
from course_discovery.apps.publisher.mixins import (check_course_organization_permission, check_roles_access, )
publisher_user_required) from course_discovery.apps.publisher.mixins import (
check_course_organization_permission, check_roles_access, publisher_user_required
)
from course_discovery.apps.publisher.models import OrganizationExtension from course_discovery.apps.publisher.models import OrganizationExtension
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.utils import (get_internal_users, has_role_for_course, from course_discovery.apps.publisher.utils import (
is_email_notification_enabled, is_internal_user, get_internal_users, has_role_for_course, is_email_notification_enabled, is_internal_user,
is_project_coordinator_user, is_publisher_admin, is_publisher_user, is_project_coordinator_user, is_publisher_admin, is_publisher_user, make_bread_crumbs, parse_datetime_field
make_bread_crumbs, parse_datetime_field) )
@ddt.ddt @ddt.ddt
......
...@@ -6,8 +6,9 @@ import ddt ...@@ -6,8 +6,9 @@ import ddt
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.course_metadata.choices import CourseRunPacing from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.tests.factories import (OrganizationFactory, PersonFactory, from course_discovery.apps.course_metadata.tests.factories import (
PersonSocialNetworkFactory, PositionFactory) OrganizationFactory, PersonFactory, PersonSocialNetworkFactory, PositionFactory
)
from course_discovery.apps.publisher.choices import CourseRunStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, PublisherUserRole
from course_discovery.apps.publisher.models import Seat from course_discovery.apps.publisher.models import Seat
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
......
""" Publisher Utils.""" """ Publisher Utils."""
import re import re
from dateutil import parser from dateutil import parser
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.constants import (ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PROJECT_COORDINATOR_GROUP_NAME) ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME
)
VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY = re.compile(r'^[a-zA-Z0-9._-]*$') VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY = re.compile(r'^[a-zA-Z0-9._-]*$')
......
...@@ -32,8 +32,8 @@ from course_discovery.apps.publisher.forms import ( ...@@ -32,8 +32,8 @@ from course_discovery.apps.publisher.forms import (
AdminImportCourseForm, CourseEntitlementForm, CourseForm, CourseRunForm, CourseSearchForm, SeatForm AdminImportCourseForm, CourseEntitlementForm, CourseForm, CourseRunForm, CourseSearchForm, SeatForm
) )
from course_discovery.apps.publisher.models import ( from course_discovery.apps.publisher.models import (
Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, CourseUserRole, Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension,
OrganizationExtension, PublisherUser, Seat, UserAttributes PublisherUser, Seat, UserAttributes
) )
from course_discovery.apps.publisher.utils import ( from course_discovery.apps.publisher.utils import (
get_internal_users, has_role_for_course, is_internal_user, is_project_coordinator_user, is_publisher_admin, get_internal_users, has_role_for_course, is_internal_user, is_project_coordinator_user, is_publisher_admin,
......
import logging import logging
import waffle import waffle
from django.db import models, transaction from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
......
...@@ -3,7 +3,6 @@ URLs for the course publisher comments views. ...@@ -3,7 +3,6 @@ URLs for the course publisher comments views.
""" """
from django.conf.urls import include, url from django.conf.urls import include, url
urlpatterns = [ urlpatterns = [
url(r'^api/', include('course_discovery.apps.publisher_comments.api.urls', namespace='api')), url(r'^api/', include('course_discovery.apps.publisher_comments.api.urls', namespace='api')),
] ]
from course_discovery.settings.devstack import * from course_discovery.settings.devstack import *
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from course_discovery.settings.shared.test import * # isort:skip from course_discovery.settings.shared.test import * # isort:skip
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment