Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
course-discovery
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
course-discovery
Commits
c4f757cd
Commit
c4f757cd
authored
Apr 19, 2018
by
Business Sandbox
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/uman/full-detail-search' into business/hornets
parents
c2abe452
b70e5fd4
Hide whitespace changes
Inline
Side-by-side
Showing
44 changed files
with
775 additions
and
565 deletions
+775
-565
.gitignore
+1
-0
course_discovery/apps/api/mixins.py
+56
-0
course_discovery/apps/api/serializers.py
+145
-94
course_discovery/apps/api/tests/test_serializers.py
+186
-112
course_discovery/apps/api/utils.py
+1
-0
course_discovery/apps/api/v1/tests/test_views/mixins.py
+88
-18
course_discovery/apps/api/v1/tests/test_views/test_courses.py
+0
-1
course_discovery/apps/api/v1/tests/test_views/test_programs.py
+2
-2
course_discovery/apps/api/v1/tests/test_views/test_search.py
+170
-240
course_discovery/apps/api/v1/views/__init__.py
+0
-3
course_discovery/apps/api/v1/views/search.py
+9
-5
course_discovery/apps/core/migrations/0007_auto_20171004_1133.py
+0
-1
course_discovery/apps/core/tests/mixins.py
+0
-1
course_discovery/apps/course_metadata/models.py
+1
-3
course_discovery/apps/course_metadata/publishers.py
+1
-6
course_discovery/apps/course_metadata/search_indexes.py
+20
-0
course_discovery/apps/course_metadata/tests/factories.py
+1
-0
course_discovery/apps/course_metadata/tests/test_publishers.py
+2
-9
course_discovery/apps/edx_catalog_extensions/api/v1/tests/test_views.py
+5
-5
course_discovery/apps/edx_haystack_extensions/distinct_counts/backends.py
+0
-1
course_discovery/apps/edx_haystack_extensions/distinct_counts/query.py
+1
-0
course_discovery/apps/edx_haystack_extensions/management/commands/remove_unused_indexes.py
+0
-1
course_discovery/apps/publisher/admin.py
+8
-6
course_discovery/apps/publisher/api/serializers.py
+4
-3
course_discovery/apps/publisher/api/tests/test_serializers.py
+8
-6
course_discovery/apps/publisher/api/tests/test_utils.py
+3
-2
course_discovery/apps/publisher/api/tests/test_views.py
+3
-2
course_discovery/apps/publisher/api/urls.py
+5
-5
course_discovery/apps/publisher/api/v1/tests/test_views.py
+3
-2
course_discovery/apps/publisher/api/views.py
+10
-7
course_discovery/apps/publisher/assign_permissions.py
+4
-3
course_discovery/apps/publisher/migrations/0019_create_user_groups.py
+3
-2
course_discovery/apps/publisher/migrations/0061_add_people_permission.py
+4
-2
course_discovery/apps/publisher/models.py
+0
-1
course_discovery/apps/publisher/tests/factories.py
+4
-3
course_discovery/apps/publisher/tests/test_admin.py
+3
-2
course_discovery/apps/publisher/tests/test_models.py
+3
-2
course_discovery/apps/publisher/tests/test_utils.py
+10
-8
course_discovery/apps/publisher/tests/test_wrapper.py
+3
-2
course_discovery/apps/publisher/utils.py
+4
-2
course_discovery/apps/publisher/views.py
+2
-2
course_discovery/apps/publisher_comments/models.py
+1
-0
course_discovery/apps/publisher_comments/urls.py
+0
-1
course_discovery/settings/devstack_test.py
+1
-0
No files found.
.gitignore
View file @
c4f757cd
...
...
@@ -33,6 +33,7 @@ assets/
# Unit test / coverage reports
.coverage
.coverage.*
htmlcov
.tox
nosetests.xml
...
...
course_discovery/apps/api/mixins.py
0 → 100644
View file @
c4f757cd
"""
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
"""
Returns fully detailed aggregated search 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
course_discovery/apps/api/serializers.py
View file @
c4f757cd
...
...
@@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model
from
django.db.models.query
import
Prefetch
from
django.utils.text
import
slugify
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.fields
import
DictField
from
taggit_serializer.serializers
import
TaggitSerializer
,
TagListSerializerField
...
...
@@ -18,68 +18,18 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie
from
course_discovery.apps.api.fields
import
ImageField
,
StdImageSerializerField
from
course_discovery.apps.catalogs.models
import
Catalog
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.models
import
(
FAQ
,
CorporateEndorsement
,
Course
,
CourseEntitlement
,
CourseRun
,
Endorsement
,
Image
,
Organization
,
Person
,
PersonSocialNetwork
,
PersonWork
,
Position
,
Prerequisite
,
Program
,
ProgramType
,
Seat
,
SeatType
,
Subject
,
Topic
,
Video
)
from
course_discovery.apps.course_metadata.search_indexes
import
CourseIndex
,
CourseRunIndex
,
ProgramIndex
User
=
get_user_model
()
COMMON_IGNORED_FIELDS
=
(
'text'
,)
COMMON_SEARCH_FIELD_ALIASES
=
{
'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'
,)
COMMON_SEARCH_FIELD_ALIASES
=
{
'q'
:
'text'
}
PREFETCH_FIELDS
=
{
'course_run'
:
[
'course__level_type'
,
...
...
@@ -178,6 +128,17 @@ class TimestampModelSerializer(serializers.ModelSerializer):
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
):
"""Serializer for models inheriting from ``AbstractNamedModel``."""
name
=
serializers
.
CharField
()
...
...
@@ -904,7 +865,7 @@ class ProgramSerializer(MinimalProgramSerializer):
"""
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 P
r
efetch 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
the querysets that we're prefetching).
"""
...
...
@@ -1155,6 +1116,7 @@ class QueryFacetFieldSerializer(serializers.Serializer):
class
BaseHaystackFacetSerializer
(
HaystackFacetSerializer
):
_abstract
=
True
serialize_objects
=
True
def
get_fields
(
self
):
query_facet_counts
=
self
.
instance
.
pop
(
'queries'
,
{})
...
...
@@ -1186,27 +1148,28 @@ class BaseHaystackFacetSerializer(HaystackFacetSerializer):
class
CourseSearchSerializer
(
HaystackSerializer
):
content_type
=
serializers
.
CharField
(
source
=
'model_name'
)
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
(
'key'
,
'title'
,
'short_description'
,
'full_description'
,
'text'
,
'aggregation_key'
,)
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
):
serialize_objects
=
True
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
ignore_fields
=
COMMON_IGNORED_FIELDS
field_options
=
{
'level_type'
:
{},
'organizations'
:
{},
'prerequisites'
:
{},
'subjects'
:
{},
}
ignore_fields
=
COMMON_IGNORED_FIELDS
class
CourseRunSearchSerializer
(
HaystackSerializer
):
...
...
@@ -1217,19 +1180,68 @@ class CourseRunSearchSerializer(HaystackSerializer):
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
COURSE_RUN_SEARCH_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
):
serialize_objects
=
True
class
Meta
:
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
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
):
...
...
@@ -1241,32 +1253,86 @@ class ProgramSearchSerializer(HaystackSerializer):
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
field_options
=
PROGRAM_FACET_FIELD_OPTIONS
fields
=
PROGRAM_SEARCH_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
):
serialize_objects
=
True
class
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
field_options
=
PROGRAM_FACET_FIELD_OPTIONS
fields
=
PROGRAM_FACET_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
Meta
:
field_aliases
=
COMMON_SEARCH_FIELD_ALIASES
fields
=
COURSE_RUN_SEARCH_FIELDS
+
PROGRAM_SEARCH_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
=
{
CourseRunIndex
:
CourseRunSearch
Serializer
,
CourseIndex
:
CourseSearch
Serializer
,
ProgramIndex
:
ProgramSearch
Serializer
,
search_indexes
.
CourseRunIndex
:
CourseRunSearchModel
Serializer
,
search_indexes
.
CourseIndex
:
CourseSearchModel
Serializer
,
search_indexes
.
ProgramIndex
:
ProgramSearchModel
Serializer
,
}
...
...
@@ -1294,21 +1360,6 @@ class TypeaheadSearchSerializer(serializers.Serializer):
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
):
"""Serializer for the ``Topic`` model."""
...
...
course_discovery/apps/api/tests/test_serializers.py
View file @
c4f757cd
# pylint: disable=no-member,
test-inherits-tests
# pylint: disable=no-member,test-inherits-tests
import
datetime
import
itertools
from
urllib.parse
import
urlencode
...
...
@@ -19,14 +19,15 @@ from waffle.testutils import override_switch
from
course_discovery.apps.api.fields
import
ImageField
,
StdImageSerializerField
from
course_discovery.apps.api.serializers
import
(
AffiliateWindowSerializer
,
CatalogSerializer
,
ContainedCourseRunsSerializer
,
ContainedCoursesSerializer
,
CorporateEndorsementSerializer
,
CourseEntitlementSerializer
,
CourseRunSearchSerializer
,
CourseRunSerializer
,
CourseRunWithProgramsSerializer
,
CourseSearchSerializer
,
CourseSerializer
,
CourseWithProgramsSerializer
,
EndorsementSerializer
,
FAQSerializer
,
FlattenedCourseRunWithCourseSerializer
,
ImageSerializer
,
MinimalCourseRunSerializer
,
MinimalCourseSerializer
,
MinimalOrganizationSerializer
,
MinimalProgramCourseSerializer
,
MinimalProgramSerializer
,
NestedProgramSerializer
,
OrganizationSerializer
,
PersonSerializer
,
PositionSerializer
,
PrerequisiteSerializer
,
ProgramSearchSerializer
,
ProgramSerializer
,
ProgramTypeSerializer
,
SeatSerializer
,
SubjectSerializer
,
TopicSerializer
,
TypeaheadCourseRunSearchSerializer
,
TypeaheadProgramSearchSerializer
,
VideoSerializer
,
get_utm_source_for_user
ContentTypeSerializer
,
CorporateEndorsementSerializer
,
CourseEntitlementSerializer
,
CourseRunSearchModelSerializer
,
CourseRunSearchSerializer
,
CourseRunSerializer
,
CourseRunWithProgramsSerializer
,
CourseSearchModelSerializer
,
CourseSearchSerializer
,
CourseSerializer
,
CourseWithProgramsSerializer
,
EndorsementSerializer
,
FAQSerializer
,
FlattenedCourseRunWithCourseSerializer
,
ImageSerializer
,
MinimalCourseRunSerializer
,
MinimalCourseSerializer
,
MinimalOrganizationSerializer
,
MinimalProgramCourseSerializer
,
MinimalProgramSerializer
,
NestedProgramSerializer
,
OrganizationSerializer
,
PersonSerializer
,
PositionSerializer
,
PrerequisiteSerializer
,
ProgramSearchModelSerializer
,
ProgramSearchSerializer
,
ProgramSerializer
,
ProgramTypeSerializer
,
SeatSerializer
,
SubjectSerializer
,
TopicSerializer
,
TypeaheadCourseRunSearchSerializer
,
TypeaheadProgramSearchSerializer
,
VideoSerializer
,
get_utm_source_for_user
)
from
course_discovery.apps.api.tests.mixins
import
SiteMixin
from
course_discovery.apps.catalogs.tests.factories
import
CatalogFactory
...
...
@@ -109,7 +110,8 @@ class CatalogSerializerTests(ElasticsearchTestMixin, TestCase):
class
MinimalCourseSerializerTests
(
SiteMixin
,
TestCase
):
serializer_class
=
MinimalCourseSerializer
def
get_expected_data
(
self
,
course
,
request
):
@classmethod
def
get_expected_data
(
cls
,
course
,
request
):
context
=
{
'request'
:
request
}
return
{
...
...
@@ -136,7 +138,8 @@ class MinimalCourseSerializerTests(SiteMixin, TestCase):
class
CourseSerializerTests
(
MinimalCourseSerializerTests
):
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
.
update
({
'short_description'
:
course
.
short_description
,
...
...
@@ -179,7 +182,8 @@ class CourseSerializerTests(MinimalCourseSerializerTests):
class
CourseWithProgramsSerializerTests
(
CourseSerializerTests
):
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
.
update
({
'programs'
:
NestedProgramSerializer
(
...
...
@@ -224,7 +228,8 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
class
MinimalCourseRunSerializerTests
(
TestCase
):
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
{
'key'
:
course_run
.
key
,
'uuid'
:
str
(
course_run
.
uuid
),
...
...
@@ -259,7 +264,8 @@ class MinimalCourseRunSerializerTests(TestCase):
class
CourseRunSerializerTests
(
MinimalCourseRunSerializerTests
):
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
.
update
({
'course'
:
course_run
.
course
.
key
,
...
...
@@ -308,17 +314,7 @@ class CourseRunWithProgramsSerializerTests(TestCase):
def
test_data
(
self
):
serializer
=
CourseRunWithProgramsSerializer
(
self
.
course_run
,
context
=
self
.
serializer_context
)
ProgramFactory
(
courses
=
[
self
.
course_run
.
course
])
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
)
self
.
assertDictEqual
(
serializer
.
data
,
self
.
get_expected_data
(
self
.
course_run
,
self
.
request
))
def
test_data_excluded_course_run
(
self
):
"""
...
...
@@ -328,11 +324,8 @@ class CourseRunWithProgramsSerializerTests(TestCase):
serializer
=
CourseRunWithProgramsSerializer
(
self
.
course_run
,
context
=
self
.
serializer_context
)
ProgramFactory
(
courses
=
[
self
.
course_run
.
course
],
excluded_course_runs
=
[
self
.
course_run
])
expected
=
CourseRunSerializer
(
self
.
course_run
,
context
=
self
.
serializer_context
)
.
data
expected
.
update
({
'programs'
:
[],
})
self
.
assertDictEqual
(
serializer
.
data
,
expected
)
expected
.
update
({
'programs'
:
[]})
assert
serializer
.
data
==
expected
def
test_exclude_deleted_programs
(
self
):
"""
...
...
@@ -398,9 +391,22 @@ class CourseRunWithProgramsSerializerTests(TestCase):
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
def
serialize_seats
(
self
,
course_run
):
@classmethod
def
serialize_seats
(
cls
,
course_run
):
seats
=
{
'audit'
:
{
'type'
:
''
...
...
@@ -442,21 +448,23 @@ class FlattenedCourseRunWithCourseSerializerTests(TestCase): # pragma: no cover
return
seats
def
serialize_items
(
self
,
organizations
,
attr
):
@classmethod
def
serialize_items
(
cls
,
organizations
,
attr
):
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
serializer_context
=
{
'request'
:
request
}
expected
=
dict
(
CourseRunSerializer
(
course_run
,
context
=
serializer_context
)
.
data
)
expected
.
update
({
'subjects'
:
self
.
serialize_items
(
course
.
subjects
.
all
(),
'name'
),
'seats'
:
self
.
serialize_seats
(
course_run
),
'owners'
:
self
.
serialize_items
(
course
.
authoring_organizations
.
all
(),
'key'
),
'sponsors'
:
self
.
serialize_items
(
course
.
sponsoring_organizations
.
all
(),
'key'
),
'prerequisites'
:
self
.
serialize_items
(
course
.
prerequisites
.
all
(),
'name'
),
'subjects'
:
cls
.
serialize_items
(
course
.
subjects
.
all
(),
'name'
),
'seats'
:
cls
.
serialize_seats
(
course_run
),
'owners'
:
cls
.
serialize_items
(
course
.
authoring_organizations
.
all
(),
'key'
),
'sponsors'
:
cls
.
serialize_items
(
course
.
sponsoring_organizations
.
all
(),
'key'
),
'prerequisites'
:
cls
.
serialize_items
(
course
.
prerequisites
.
all
(),
'name'
),
'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
,
'image'
:
ImageField
()
.
to_representation
(
course_run
.
card_image_url
),
})
...
...
@@ -623,7 +631,8 @@ class MinimalProgramSerializerTests(TestCase):
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
.
_context
=
{
'request'
:
request
}
# pylint: disable=protected-access
...
...
@@ -661,9 +670,9 @@ class MinimalProgramSerializerTests(TestCase):
class
ProgramSerializerTests
(
MinimalProgramSerializerTests
):
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
.
update
({
'authoring_organizations'
:
OrganizationSerializer
(
program
.
authoring_organizations
,
many
=
True
)
.
data
,
'video'
:
VideoSerializer
(
program
.
video
)
.
data
,
...
...
@@ -698,7 +707,6 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
'subjects'
:
SubjectSerializer
(
program
.
subjects
,
many
=
True
)
.
data
,
'transcript_languages'
:
[
serialize_language_to_code
(
l
)
for
l
in
program
.
transcript_languages
],
})
return
expected
def
test_data_with_exclusions
(
self
):
...
...
@@ -900,7 +908,8 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
class
ProgramTypeSerializerTests
(
TestCase
):
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
.
_context
=
{
'request'
:
request
}
# pylint: disable=protected-access
...
...
@@ -945,6 +954,23 @@ class ContainedCoursesSerializerTests(TestCase):
@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
):
@ddt.data
(
(
PrerequisiteFactory
,
PrerequisiteSerializer
),
...
...
@@ -1067,7 +1093,8 @@ class MinimalOrganizationSerializerTests(TestCase):
def
create_organization
(
self
):
return
OrganizationFactory
()
def
get_expected_data
(
self
,
organization
):
@classmethod
def
get_expected_data
(
cls
,
organization
):
return
{
'uuid'
:
str
(
organization
.
uuid
),
'key'
:
organization
.
key
,
...
...
@@ -1090,14 +1117,15 @@ class OrganizationSerializerTests(MinimalOrganizationSerializerTests):
organization
.
tags
.
add
(
self
.
TAG
)
return
organization
def
get_expected_data
(
self
,
organization
):
@classmethod
def
get_expected_data
(
cls
,
organization
):
expected
=
super
()
.
get_expected_data
(
organization
)
expected
.
update
({
'certificate_logo_image_url'
:
organization
.
certificate_logo_image_url
,
'description'
:
organization
.
description
,
'homepage_url'
:
organization
.
homepage_url
,
'logo_image_url'
:
organization
.
logo_image_url
,
'tags'
:
[
self
.
TAG
],
'tags'
:
[
cls
.
TAG
],
'marketing_url'
:
organization
.
marketing_url
,
})
...
...
@@ -1215,11 +1243,23 @@ class AffiliateWindowSerializerTests(TestCase):
class
CourseSearchSerializerTests
(
TestCase
):
serializer_class
=
CourseSearchSerializer
def
test_data
(
self
):
request
=
make_request
()
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
,
'title'
:
course
.
title
,
'short_description'
:
course
.
short_description
,
...
...
@@ -1227,26 +1267,47 @@ class CourseSearchSerializerTests(TestCase):
'content_type'
:
'course'
,
'aggregation_key'
:
'course:{}'
.
format
(
course
.
key
),
}
assert
serializer
.
data
==
expected
def
serialize_course
(
self
,
course
):
""" Serializes the given `Course` as a search result. """
result
=
SearchQuerySet
()
.
models
(
Course
)
.
filter
(
key
=
course
.
key
)[
0
]
serializer
=
CourseSearchSerializer
(
result
)
return
serializer
class
CourseSearchModelSerializerTests
(
CourseSearchSerializerTests
):
serializer_class
=
CourseSearchModelSerializer
@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
):
serializer_class
=
CourseRunSearchSerializer
def
test_data
(
self
):
request
=
make_request
()
course_run
=
CourseRunFactory
(
transcript_languages
=
LanguageTag
.
objects
.
filter
(
code__in
=
[
'en-us'
,
'zh-cn'
]),
authoring_organizations
=
[
OrganizationFactory
()])
SeatFactory
.
create
(
course_run
=
course_run
,
type
=
'verified'
,
price
=
10
,
sku
=
'ABCDEF'
)
program
=
ProgramFactory
(
courses
=
[
course_run
.
course
])
self
.
reindex_courses
(
program
)
serializer
=
self
.
serialize_course_run
(
course_run
)
course_run_key
=
CourseKey
.
from_string
(
course_run
.
key
)
orgs
=
course_run
.
authoring_organizations
.
all
()
expected
=
{
serializer
=
self
.
serialize_course_run
(
course_run
,
request
)
assert
serializer
.
data
==
self
.
get_expected_data
(
course_run
,
request
)
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
()],
'min_effort'
:
course_run
.
min_effort
,
'max_effort'
:
course_run
.
max_effort
,
...
...
@@ -1264,8 +1325,8 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
'full_description'
:
course_run
.
full_description
,
'title'
:
course_run
.
title
,
'content_type'
:
'courserun'
,
'org'
:
course_run_key
.
org
,
'number'
:
course_run_key
.
course
,
'org'
:
CourseKey
.
from_string
(
course_run
.
key
)
.
org
,
'number'
:
CourseKey
.
from_string
(
course_run
.
key
)
.
course
,
'seat_types'
:
course_run
.
seat_types
,
'image_url'
:
course_run
.
image_url
,
'type'
:
course_run
.
type
,
...
...
@@ -1274,7 +1335,7 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
'published'
:
course_run
.
status
==
CourseRunStatus
.
Published
,
'partner'
:
course_run
.
course
.
partner
.
short_code
,
'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
()),
'subject_uuids'
:
get_uuids
(
course_run
.
subjects
.
all
()),
'staff_uuids'
:
get_uuids
(
course_run
.
staff
.
all
()),
...
...
@@ -1282,25 +1343,30 @@ class CourseRunSearchSerializerTests(ElasticsearchTestMixin, TestCase):
'has_enrollable_seats'
:
course_run
.
has_enrollable_seats
,
'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
):
""" Verify a null `LevelType` is properly serialized as None. """
course_run
=
CourseRunFactory
(
course__level_type
=
None
)
serializer
=
self
.
serialize_course_run
(
course_run
)
assert
serializer
.
data
[
'level_type'
]
is
None
class
CourseRunSearchModelSerializerTests
(
CourseRunSearchSerializerTests
):
serializer_class
=
CourseRunSearchModelSerializer
@classmethod
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.usefixtures
(
'haystack_default_connection'
)
class
TestProgramSearchSerializer
:
def
_create_expected_data
(
self
,
program
):
class
TestProgramSearchSerializer
(
TestCase
):
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
{
'uuid'
:
str
(
program
.
uuid
),
'title'
:
program
.
title
,
...
...
@@ -1330,81 +1396,89 @@ class TestProgramSearchSerializer:
'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
):
authoring_organization
,
crediting_organization
=
OrganizationFactory
.
create_batch
(
2
)
program
=
ProgramFactory
(
authoring_organizations
=
[
authoring_organization
],
credit_backing_organizations
=
[
crediting_organization
])
# NOTE: This serializer expects SearchQuerySet results, so we run a search on the newly-created object
# to generate such a result.
result
=
SearchQuerySet
()
.
models
(
Program
)
.
filter
(
uuid
=
program
.
uuid
)[
0
]
serializer
=
ProgramSearchSerializer
(
result
)
expected
=
self
.
_create_expected_data
(
program
)
serializer
=
self
.
serialize_program
(
program
,
self
.
request
)
expected
=
self
.
get_expected_data
(
program
,
self
.
request
)
assert
serializer
.
data
==
expected
def
test_data_without_organizations
(
self
):
""" Verify the serializer serialized programs with no associated organizations.
In such cases the organizations value should be an empty array. """
program
=
ProgramFactory
(
authoring_organizations
=
[],
credit_backing_organizations
=
[])
result
=
SearchQuerySet
()
.
models
(
Program
)
.
filter
(
uuid
=
program
.
uuid
)[
0
]
serializer
=
ProgramSearchSerializer
(
result
)
expected
=
self
.
_create_expected_data
(
program
)
serializer
=
self
.
serialize_program
(
program
,
self
.
request
)
expected
=
self
.
get_expected_data
(
program
,
self
.
request
)
assert
serializer
.
data
==
expected
def
test_data_with_languages
(
self
):
"""
Verify that program languages are serialized.
"""
course_run
=
CourseRunFactory
(
language
=
LanguageTag
.
objects
.
get
(
code
=
'en-us'
),
authoring_organizations
=
[
OrganizationFactory
()]
)
CourseRunFactory
(
course
=
course_run
.
course
,
language
=
LanguageTag
.
objects
.
get
(
code
=
'zh-cmn'
)
)
course_run
=
CourseRunFactory
(
language
=
LanguageTag
.
objects
.
get
(
code
=
'en-us'
),
authoring_organizations
=
[
OrganizationFactory
()])
CourseRunFactory
(
course
=
course_run
.
course
,
language
=
LanguageTag
.
objects
.
get
(
code
=
'zh-cmn'
))
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
set
(
expected
[
'languages'
])
==
{
'en-us'
,
'zh-cmn'
}
result
=
SearchQuerySet
()
.
models
(
Program
)
.
filter
(
uuid
=
program
.
uuid
)[
0
]
serializer
=
ProgramSearchSerializer
(
result
)
expected
=
self
.
_create_expected_data
(
program
)
assert
serializer
.
data
==
expected
assert
{
'English'
,
'Chinese - Mandarin'
}
==
{
*
expected
[
'language'
]}
class
ProgramSearchModelSerializerTest
(
TestProgramSearchSerializer
):
serializer_class
=
ProgramSearchModelSerializer
@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.usefixtures
(
'haystack_default_connection'
)
class
TestTypeaheadCourseRunSearchSerializer
:
def
test_data
(
self
):
authoring_organization
=
OrganizationFactory
()
course_run
=
CourseRunFactory
(
authoring_organizations
=
[
authoring_organization
])
serialized_course
=
self
.
serialize_course_run
(
course_run
)
serializer_class
=
TypeaheadCourseRunSearchSerializer
expected
=
{
@classmethod
def
get_expected_data
(
cls
,
course_run
):
return
{
'key'
:
course_run
.
key
,
'title'
:
course_run
.
title
,
'orgs'
:
[
org
.
key
for
org
in
course_run
.
authoring_organizations
.
all
()],
'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
):
""" Serializes the given `CourseRun` as a typeahead result. """
result
=
SearchQuerySet
()
.
models
(
CourseRun
)
.
filter
(
key
=
course_run
.
key
)[
0
]
serializer
=
TypeaheadCourseRunSearchSerializer
(
result
)
serializer
=
self
.
serializer_class
(
result
)
return
serializer
@pytest.mark.django_db
@pytest.mark.usefixtures
(
'haystack_default_connection'
)
class
TestTypeaheadProgramSearchSerializer
:
def
_create_expected_data
(
self
,
program
):
serializer_class
=
TypeaheadProgramSearchSerializer
@classmethod
def
get_expected_data
(
cls
,
program
):
return
{
'uuid'
:
str
(
program
.
uuid
),
'title'
:
program
.
title
,
...
...
@@ -1417,7 +1491,7 @@ class TestTypeaheadProgramSearchSerializer:
authoring_organization
=
OrganizationFactory
()
program
=
ProgramFactory
(
authoring_organizations
=
[
authoring_organization
])
serialized_program
=
self
.
serialize_program
(
program
)
expected
=
self
.
_create
_expected_data
(
program
)
expected
=
self
.
get
_expected_data
(
program
)
assert
serialized_program
.
data
==
expected
def
test_data_multiple_authoring_organizations
(
self
):
...
...
@@ -1430,7 +1504,7 @@ class TestTypeaheadProgramSearchSerializer:
def
serialize_program
(
self
,
program
):
""" Serializes the given `Program` as a typeahead result. """
result
=
SearchQuerySet
()
.
models
(
Program
)
.
filter
(
uuid
=
program
.
uuid
)[
0
]
serializer
=
TypeaheadProgramSearchSerializer
(
result
)
serializer
=
self
.
serializer_class
(
result
)
return
serializer
...
...
course_discovery/apps/api/utils.py
View file @
c4f757cd
import
hashlib
import
logging
import
six
logger
=
logging
.
getLogger
(
__name__
)
...
...
course_discovery/apps/api/v1/tests/test_views/mixins.py
View file @
c4f757cd
...
...
@@ -4,16 +4,15 @@ import json
import
responses
from
django.conf
import
settings
from
haystack.query
import
SearchQuerySet
from
rest_framework.test
import
APITestCase
as
RestAPITestCase
from
rest_framework.test
import
APIRequestFactory
from
course_discovery.apps.api.serializers
import
(
CatalogCourseSerializer
,
CatalogSerializer
,
CourseRunWithProgramsSerializer
,
CourseWithProgramsSerializer
,
FlattenedCourseRunWithCourseSerializer
,
MinimalProgramSerializer
,
OrganizationSerializer
,
PersonSerializer
,
ProgramSerializer
,
ProgramTypeSerializer
,
SubjectSerializer
,
TopicSerializer
)
from
course_discovery.apps.api
import
serializers
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
:
...
...
@@ -32,47 +31,69 @@ class SerializationMixin:
context
=
{
'request'
:
self
.
_get_request
(
format
)}
if
extra_context
:
context
.
update
(
extra_context
)
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
):
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
):
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
):
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
):
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
):
return
self
.
_serialize_object
(
MinimalProgramSerializer
if
many
else
ProgramSerializer
,
serializers
.
MinimalProgramSerializer
if
many
else
serializers
.
ProgramSerializer
,
program
,
many
,
format
,
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
):
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
):
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
):
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
):
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
):
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
):
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
):
...
...
@@ -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
):
pass
course_discovery/apps/api/v1/tests/test_views/test_courses.py
View file @
c4f757cd
...
...
@@ -2,7 +2,6 @@ import datetime
import
ddt
import
pytz
from
django.core.cache
import
cache
from
django.db.models.functions
import
Lower
from
rest_framework.reverse
import
reverse
...
...
course_discovery/apps/api/v1/tests/test_views/test_programs.py
View file @
c4f757cd
...
...
@@ -115,7 +115,7 @@ class TestProgramViewSet(SerializationMixin):
partner
=
self
.
partner
)
# property does not have the right values while being indexed
del
program
.
_course_run_weeks_to_complete
with
django_assert_num_queries
(
3
8
):
with
django_assert_num_queries
(
3
9
):
response
=
self
.
assert_retrieve_success
(
program
)
assert
response
.
data
==
self
.
serialize_program
(
program
)
assert
course_list
==
list
(
program
.
courses
.
all
())
# pylint: disable=no-member
...
...
@@ -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. """
course
=
CourseFactory
(
partner
=
self
.
partner
)
program
=
ProgramFactory
(
courses
=
[
course
],
partner
=
self
.
partner
)
with
django_assert_num_queries
(
2
5
):
with
django_assert_num_queries
(
2
6
):
response
=
self
.
assert_retrieve_success
(
program
)
assert
response
.
data
==
self
.
serialize_program
(
program
)
...
...
course_discovery/apps/api/v1/tests/test_views/test_search.py
View file @
c4f757cd
import
datetime
import
json
import
urllib.parse
import
ddt
import
pytz
from
django.urls
import
reverse
from
haystack.query
import
SearchQuerySet
from
course_discovery.apps.api.serializers
import
(
CourseRunSearchSerializer
,
ProgramSearchSerializer
,
TypeaheadCourseRunSearchSerializer
,
TypeaheadProgramSearchSerializer
)
from
course_discovery.apps.api.v1.tests.test_views.mixins
import
APITestCase
from
course_discovery.apps.api
import
serializers
from
course_discovery.apps.api.v1.tests.test_views
import
mixins
from
course_discovery.apps.api.v1.views.search
import
TypeaheadSearchView
from
course_discovery.apps.core.tests.factories
import
USER_PASSWORD
,
PartnerFactory
,
Us
erFactory
from
course_discovery.apps.core.tests.factories
import
Partn
erFactory
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.models
import
CourseRun
,
Program
from
course_discovery.apps.course_metadata.tests.factories
import
(
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
)
from
course_discovery.apps.course_metadata.models
import
CourseRun
from
course_discovery.apps.course_metadata.tests.factories
import
(
CourseFactory
,
CourseRunFactory
,
OrganizationFactory
,
ProgramFactory
)
@ddt.ddt
class
CourseRunSearchViewSetTests
(
SerializationMixin
,
LoginMixin
,
ElasticsearchTestMixin
,
APITestCase
):
class
CourseRunSearchViewSetTests
(
mixins
.
SerializationMixin
,
mixins
.
LoginMixin
,
ElasticsearchTestMixin
,
mixins
.
APITestCase
):
""" Tests for CourseRunSearchViewSet. """
detailed_path
=
reverse
(
'api:v1:search-course_runs-details'
)
faceted_path
=
reverse
(
'api:v1:search-course_runs-facets'
)
list_path
=
reverse
(
'api:v1:search-course_runs-list'
)
def
get_response
(
self
,
query
=
None
,
faceted
=
False
):
qs
=
''
if
query
:
qs
=
urllib
.
parse
.
urlencode
({
'q'
:
query
})
path
=
self
.
faceted_path
if
faceted
else
self
.
list_path
def
get_response
(
self
,
query
=
None
,
path
=
None
):
qs
=
urllib
.
parse
.
urlencode
({
'q'
:
query
})
if
query
else
''
path
=
path
or
self
.
list_path
url
=
'{path}?{qs}'
.
format
(
path
=
path
,
qs
=
qs
)
return
self
.
client
.
get
(
url
)
def
process_response
(
self
,
response
):
response
=
self
.
get_response
(
response
)
.
json
()
self
.
assertTrue
(
response
[
'objects'
][
'count'
])
assert
response
[
'objects'
][
'count'
]
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
build_facet_url
(
self
,
params
):
return
'http://testserver.fake{path}?{query}'
.
format
(
path
=
self
.
faceted_path
,
query
=
urllib
.
parse
.
urlencode
(
params
)
)
def
assert_successful_search
(
self
,
faceted
=
Fals
e
):
def
assert_successful_search
(
self
,
path
=
None
,
serializer
=
Non
e
):
""" Asserts the search functionality returns results for a generated query. """
# Generate data that should be indexed and returned by the query
course_run
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
course__title
=
'Software Testing'
,
status
=
CourseRunStatus
.
Published
)
response
=
self
.
get_response
(
'software'
,
faceted
=
faceted
)
response
=
self
.
get_response
(
'software'
,
path
=
path
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
)
)
assert
response
.
status_code
==
200
response_data
=
response
.
json
(
)
# Validate the search results
expected
=
{
'count'
:
1
,
'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
)
return
course_run
,
response_data
def
build_facet_url
(
self
,
params
):
return
'http://testserver.fake{path}?{query}'
.
format
(
path
=
self
.
faceted_path
,
query
=
urllib
.
parse
.
urlencode
(
params
)
)
def
assert_response_includes_availability_facets
(
self
,
response_data
):
""" Verifies the query facet counts/URLs are properly rendered. """
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
):
""" Verify the endpoint returns HTTP 400 if an invalid facet is requested. """
...
...
@@ -166,11 +118,11 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
url
=
'{path}?selected_query_facets={facet}'
.
format
(
path
=
self
.
faceted_path
,
facet
=
facet
)
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
)}
self
.
assertEqual
(
response_data
,
expected
)
assert
response_data
==
expected
def
test_availability_faceting
(
self
):
""" Verify the endpoint returns availability facets with the results. """
...
...
@@ -184,18 +136,18 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
upcoming
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
start
=
now
+
datetime
.
timedelta
(
days
=
61
),
end
=
now
+
datetime
.
timedelta
(
days
=
90
),
status
=
CourseRunStatus
.
Published
)
response
=
self
.
get_response
(
faceted
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
)
)
response
=
self
.
get_response
(
path
=
self
.
faceted_path
)
assert
response
.
status_code
==
200
response_data
=
response
.
json
(
)
# 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
]:
serialized
=
self
.
serialize_course_run
(
run
)
serialized
=
self
.
serialize_course_run
_search
(
run
)
# Force execution of lazy function.
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
)
...
...
@@ -204,39 +156,48 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
path
=
self
.
faceted_path
,
facet
=
'availability_archived'
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
)
)
self
.
assertEqual
(
response_data
[
'objects'
][
'results'
],
[
self
.
serialize_course_run
(
archived
)])
assert
response
.
status_code
==
200
response_data
=
response
.
json
(
)
assert
response_data
[
'objects'
][
'results'
]
==
[
self
.
serialize_course_run_search
(
archived
)]
def
assert_response_includes_availability_facets
(
self
,
response_data
):
""" Verifies the query facet counts/URLs are properly rendered. """
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
(
(
list_path
,
serializers
.
CourseRunSearchSerializer
,
[
'results'
,
0
,
'program_types'
,
0
],
ProgramStatus
.
Deleted
,
6
),
(
list_path
,
serializers
.
CourseRunSearchSerializer
,
[
'results'
,
0
,
'program_types'
,
0
],
ProgramStatus
.
Unpublished
,
6
),
(
detailed_path
,
serializers
.
CourseRunSearchModelSerializer
,
[
'results'
,
0
,
'programs'
,
0
,
'type'
],
ProgramStatus
.
Deleted
,
35
),
(
detailed_path
,
serializers
.
CourseRunSearchModelSerializer
,
[
'results'
,
0
,
'programs'
,
0
,
'type'
],
ProgramStatus
.
Unpublished
,
36
),
)
@ddt.unpack
def
test_exclude_unavailable_program_types
(
self
,
path
,
serializer
,
result_location_keys
,
program_status
,
expected_queries
):
""" Verify that unavailable programs 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
(
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
):
""" Verify the deleted programs do not show in the program_types representation. """
self
.
_test_exclude_program_types
(
ProgramStatus
.
Deleted
)
# Validate the search results
expected
=
{
'count'
:
1
,
'results'
:
[
self
.
serialize_course_run_search
(
course_run
,
serializer
=
serializer
)
]
}
self
.
assertDictContainsSubset
(
expected
,
response_data
)
def
test_exclude_unpublished_program_types
(
self
):
""" Verify the unpublished programs do not show in the program_types representation. """
self
.
_test_exclude_program_types
(
ProgramStatus
.
Unpublished
)
# Check that the program is indeed the active one.
for
key
in
result_location_keys
:
response_data
=
response_data
[
key
]
assert
response_data
==
active_program
.
type
.
name
@ddt.data
(
[{
'title'
:
'Software Testing'
,
'excluded'
:
True
}],
...
...
@@ -268,49 +229,25 @@ class CourseRunSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
self
.
reindex_courses
(
program
)
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
()
self
.
assertEqual
(
response_data
[
'count'
],
len
(
course_run_list
)
)
assert
response_data
[
'count'
]
==
len
(
course_run_list
)
for
result
in
response_data
[
'results'
]:
for
course_run
in
excluded_course_run_list
:
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
:
if
result
.
get
(
'title'
)
==
course_run
.
title
:
self
.
assertEqual
(
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
])
assert
result
.
get
(
'program_types'
)
==
course_run
.
program_types
@ddt.ddt
class
AggregateSearchViewSetTests
(
SerializationMixin
,
LoginMixin
,
ElasticsearchTestMixin
,
SynonymTestMixin
,
APITestCase
):
class
AggregateSearchViewSetTests
(
mixins
.
SerializationMixin
,
mixins
.
LoginMixin
,
ElasticsearchTestMixin
,
mixins
.
SynonymTestMixin
,
mixins
.
APITestCase
):
path
=
reverse
(
'api:v1:search-all-facets'
)
def
get_response
(
self
,
query
=
None
):
...
...
@@ -338,26 +275,21 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
program
=
ProgramFactory
(
partner
=
self
.
partner
,
status
=
ProgramStatus
.
Active
)
response
=
self
.
get_response
()
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertListEqual
(
response_data
[
'objects'
][
'results'
],
[
self
.
serialize_program
(
program
),
self
.
serialize_course_run
(
course_run
)]
)
assert
response
.
status_code
==
200
response_data
=
response
.
json
()
assert
response_data
[
'objects'
][
'results'
]
==
\
[
self
.
serialize_program_search
(
program
),
self
.
serialize_course_run_search
(
course_run
)]
def
test_hidden_runs_excluded
(
self
):
"""Search results should not include hidden runs."""
visible_run
=
CourseRunFactory
(
course__partner
=
self
.
partner
)
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
()
data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertEqual
(
data
[
'objects'
][
'results'
],
[
self
.
serialize_course_run
(
visible_run
)]
)
data
=
response
.
json
()
assert
data
[
'objects'
][
'results'
]
==
[
self
.
serialize_course_run_search
(
visible_run
)]
def
test_results_filtered_by_default_partner
(
self
):
""" Verify the search results only include items related to the default partner if no partner is
...
...
@@ -369,23 +301,21 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
other_partner
=
PartnerFactory
()
other_course_run
=
CourseRunFactory
(
course__partner
=
other_partner
,
status
=
CourseRunStatus
.
Published
)
other_program
=
ProgramFactory
(
partner
=
other_partner
,
status
=
ProgramStatus
.
Active
)
self
.
assertNotEqual
(
other_program
.
partner
.
short_code
,
self
.
partner
.
short_code
)
self
.
assertNotEqual
(
other_course_run
.
course
.
partner
.
short_code
,
self
.
partner
.
short_code
)
assert
other_program
.
partner
.
short_code
!=
self
.
partner
.
short_code
assert
other_course_run
.
course
.
partner
.
short_code
!=
self
.
partner
.
short_code
response
=
self
.
get_response
()
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertListEqual
(
response_data
[
'objects'
][
'results'
],
[
self
.
serialize_program
(
program
),
self
.
serialize_course_run
(
course_run
)]
)
assert
response
.
status_code
==
200
response_data
=
response
.
json
()
assert
response_data
[
'objects'
][
'results'
]
==
\
[
self
.
serialize_program_search
(
program
),
self
.
serialize_course_run_search
(
course_run
)]
# Filter results by partner
response
=
self
.
get_response
({
'partner'
:
other_partner
.
short_code
})
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
)
)
self
.
assertListEqual
(
response_data
[
'objects'
][
'results'
],
[
self
.
serialize_program
(
other_program
),
self
.
serialize_course_run
(
other_course_run
)])
assert
response
.
status_code
==
200
response_data
=
response
.
json
(
)
assert
response_data
[
'objects'
][
'results'
]
==
\
[
self
.
serialize_program_search
(
other_program
),
self
.
serialize_course_run_search
(
other_course_run
)]
def
test_empty_query
(
self
):
""" Verify, when the query (q) parameter is empty, the endpoint behaves as if the parameter
...
...
@@ -394,10 +324,10 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
program
=
ProgramFactory
(
partner
=
self
.
partner
,
status
=
ProgramStatus
.
Active
)
response
=
self
.
get_response
({
'q'
:
''
,
'content_type'
:
[
'courserun'
,
'program'
]})
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
)
)
self
.
assertListEqual
(
response_data
[
'objects'
][
'results'
],
[
self
.
serialize_program
(
program
),
self
.
serialize_course_run
(
course_run
)])
assert
response
.
status_code
==
200
response_data
=
response
.
json
(
)
assert
response_data
[
'objects'
][
'results'
]
==
\
[
self
.
serialize_program_search
(
program
),
self
.
serialize_course_run_search
(
course_run
)]
@ddt.data
(
'start'
,
'-start'
)
def
test_results_ordered_by_start_date
(
self
,
ordering
):
...
...
@@ -410,12 +340,12 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
course_run_keys
=
[
course_run
.
key
for
course_run
in
[
archived
,
current
,
starting_soon
,
upcoming
]]
response
=
self
.
get_response
({
"ordering"
:
ordering
})
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
data
[
'objects'
][
'count'
],
4
)
assert
response
.
status_code
==
200
assert
response
.
data
[
'objects'
][
'count'
]
==
4
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
]
self
.
assertEqual
(
response
.
data
[
'objects'
][
'results'
],
expected
)
expected
=
[
self
.
serialize_course_run
_search
(
course_run
)
for
course_run
in
course_runs
]
assert
response
.
data
[
'objects'
][
'results'
]
==
expected
def
test_results_include_aggregation_key
(
self
):
""" Verify the search results only include the aggregation_key for each document. """
...
...
@@ -424,7 +354,7 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
response
=
self
.
get_response
()
assert
response
.
status_code
==
200
response_data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
)
)
response_data
=
response
.
json
(
)
expected
=
sorted
(
[
'courserun:{}'
.
format
(
course_run
.
course
.
key
),
'program:{}'
.
format
(
program
.
uuid
)]
...
...
@@ -435,8 +365,8 @@ class AggregateSearchViewSetTests(SerializationMixin, LoginMixin, ElasticsearchT
assert
expected
==
actual
class
TypeaheadSearchViewTests
(
TypeaheadSerializationMixin
,
LoginMixin
,
ElasticsearchTestMixin
,
SynonymTestMixin
,
APITestCase
):
class
TypeaheadSearchViewTests
(
mixins
.
TypeaheadSerializationMixin
,
mixins
.
LoginMixin
,
ElasticsearchTestMixin
,
mixins
.
SynonymTestMixin
,
mixins
.
APITestCase
):
path
=
reverse
(
'api:v1:search-typeahead'
)
def
get_response
(
self
,
query
=
None
,
partner
=
None
):
...
...
@@ -460,8 +390,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response
=
self
.
get_response
({
'q'
:
title
})
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
self
.
assertDictEqual
(
response_data
,
{
'course_runs'
:
[
self
.
serialize_course_run
(
course_run
)],
'programs'
:
[
self
.
serialize_program
(
program
)]})
self
.
assertDictEqual
(
response_data
,
{
'course_runs'
:
[
self
.
serialize_course_run
_search
(
course_run
)],
'programs'
:
[
self
.
serialize_program
_search
(
program
)]})
def
test_typeahead_multiple_results
(
self
):
""" Verify the typeahead responses always returns a limited number of results, even if there are more hits. """
...
...
@@ -512,8 +442,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response
=
self
.
get_response
({
'q'
:
title
})
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
self
.
assertDictEqual
(
response_data
,
{
'course_runs'
:
[
self
.
serialize_course_run
(
course_run
)],
'programs'
:
[
self
.
serialize_program
(
program
)]})
self
.
assertDictEqual
(
response_data
,
{
'course_runs'
:
[
self
.
serialize_course_run
_search
(
course_run
)],
'programs'
:
[
self
.
serialize_program
_search
(
program
)]})
def
test_partial_term_search
(
self
):
""" Test typeahead response with partial term search. """
...
...
@@ -525,8 +455,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
expected_response_data
=
{
'course_runs'
:
[
self
.
serialize_course_run
(
course_run
)],
'programs'
:
[
self
.
serialize_program
(
program
)]
'course_runs'
:
[
self
.
serialize_course_run
_search
(
course_run
)],
'programs'
:
[
self
.
serialize_program
_search
(
program
)]
}
self
.
assertDictEqual
(
response_data
,
expected_response_data
)
...
...
@@ -544,8 +474,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
expected_response_data
=
{
'course_runs'
:
[
self
.
serialize_course_run
(
course_run
)],
'programs'
:
[
self
.
serialize_program
(
program
)]
'course_runs'
:
[
self
.
serialize_course_run
_search
(
course_run
)],
'programs'
:
[
self
.
serialize_program
_search
(
program
)]
}
self
.
assertDictEqual
(
response_data
,
expected_response_data
)
...
...
@@ -559,7 +489,7 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response_data
=
response
.
json
()
expected_response_data
=
{
'course_runs'
:
[],
'programs'
:
[
self
.
serialize_program
(
program
)]
'programs'
:
[
self
.
serialize_program
_search
(
program
)]
}
self
.
assertDictEqual
(
response_data
,
expected_response_data
)
...
...
@@ -579,8 +509,8 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response
=
self
.
get_response
({
'q'
:
partial_key
})
self
.
assertEqual
(
response
.
status_code
,
200
)
expected
=
{
'course_runs'
:
[
self
.
serialize_course_run
(
course_run
)],
'programs'
:
[
self
.
serialize_program
(
program
)]
'course_runs'
:
[
self
.
serialize_course_run
_search
(
course_run
)],
'programs'
:
[
self
.
serialize_program
_search
(
program
)]
}
self
.
assertDictEqual
(
response
.
data
,
expected
)
...
...
@@ -611,9 +541,9 @@ class TypeaheadSearchViewTests(TypeaheadSerializationMixin, LoginMixin, Elastics
response
=
self
.
get_response
({
'q'
:
'mit'
})
self
.
assertEqual
(
response
.
status_code
,
200
)
expected
=
{
'course_runs'
:
[
self
.
serialize_course_run
(
mit_run
),
self
.
serialize_course_run
(
harvard_run
)],
'programs'
:
[
self
.
serialize_program
(
mit_program
),
self
.
serialize_program
(
harvard_program
)]
'course_runs'
:
[
self
.
serialize_course_run
_search
(
mit_run
),
self
.
serialize_course_run
_search
(
harvard_run
)],
'programs'
:
[
self
.
serialize_program
_search
(
mit_program
),
self
.
serialize_program
_search
(
harvard_program
)]
}
self
.
assertDictEqual
(
response
.
data
,
expected
)
course_discovery/apps/api/v1/views/__init__.py
View file @
c4f757cd
from
django.conf
import
settings
from
django.contrib.auth
import
get_user_model
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
()
...
...
course_discovery/apps/api/v1/views/search.py
View file @
c4f757cd
...
...
@@ -11,12 +11,12 @@ from rest_framework.permissions import IsAuthenticated
from
rest_framework.response
import
Response
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.models
import
Course
,
CourseRun
,
Program
class
BaseHaystackViewSet
(
FacetMixin
,
HaystackViewSet
):
class
BaseHaystackViewSet
(
mixins
.
DetailMixin
,
FacetMixin
,
HaystackViewSet
):
document_uid_field
=
'key'
facet_filter_backends
=
[
filters
.
HaystackFacetFilterWithQueries
,
filters
.
HaystackFilter
,
OrderingFilter
]
ordering_fields
=
(
'start'
,)
...
...
@@ -93,27 +93,31 @@ class BaseHaystackViewSet(FacetMixin, HaystackViewSet):
class
CourseSearchViewSet
(
BaseHaystackViewSet
):
facet_serializer_class
=
serializers
.
CourseFacetSerializer
index_models
=
(
Course
,)
detail_serializer_class
=
serializers
.
CourseSearchModelSerializer
facet_serializer_class
=
serializers
.
CourseFacetSerializer
serializer_class
=
serializers
.
CourseSearchSerializer
class
CourseRunSearchViewSet
(
BaseHaystackViewSet
):
facet_serializer_class
=
serializers
.
CourseRunFacetSerializer
index_models
=
(
CourseRun
,)
detail_serializer_class
=
serializers
.
CourseRunSearchModelSerializer
facet_serializer_class
=
serializers
.
CourseRunFacetSerializer
serializer_class
=
serializers
.
CourseRunSearchSerializer
class
ProgramSearchViewSet
(
BaseHaystackViewSet
):
document_uid_field
=
'uuid'
lookup_field
=
'uuid'
facet_serializer_class
=
serializers
.
ProgramFacetSerializer
index_models
=
(
Program
,)
detail_serializer_class
=
serializers
.
ProgramSearchModelSerializer
facet_serializer_class
=
serializers
.
ProgramFacetSerializer
serializer_class
=
serializers
.
ProgramSearchSerializer
class
AggregateSearchViewSet
(
BaseHaystackViewSet
):
""" Search all content types. """
detail_serializer_class
=
serializers
.
AggregateSearchModelSerializer
facet_serializer_class
=
serializers
.
AggregateFacetSearchSerializer
serializer_class
=
serializers
.
AggregateSearchSerializer
...
...
course_discovery/apps/core/migrations/0007_auto_20171004_1133.py
View file @
c4f757cd
...
...
@@ -4,7 +4,6 @@ from __future__ import unicode_literals
from
django.db
import
migrations
SWITCH
=
'use_company_name_as_utm_source_value'
...
...
course_discovery/apps/core/tests/mixins.py
View file @
c4f757cd
...
...
@@ -3,7 +3,6 @@ import logging
import
pytest
import
responses
from
django.conf
import
settings
from
haystack
import
connections
as
haystack_connections
...
...
course_discovery/apps/course_metadata/models.py
View file @
c4f757cd
...
...
@@ -26,15 +26,13 @@ from taggit_autosuggest.managers import TaggableManager
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.publishers
import
(
CourseRunMarketingSitePublisher
,
ProgramMarketingSitePublisher
CourseRunMarketingSitePublisher
,
ProgramMarketingSitePublisher
)
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.ietf_language_tags.models
import
LanguageTag
from
course_discovery.apps.publisher.utils
import
VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY
logger
=
logging
.
getLogger
(
__name__
)
...
...
course_discovery/apps/course_metadata/publishers.py
View file @
c4f757cd
...
...
@@ -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.exceptions
import
(
AliasCreateError
,
AliasDeleteError
,
FormRetrievalError
,
NodeCreateError
,
NodeDeleteError
,
NodeEditError
,
AliasCreateError
,
AliasDeleteError
,
FormRetrievalError
,
NodeCreateError
,
NodeDeleteError
,
NodeEditError
,
NodeLookupError
)
from
course_discovery.apps.course_metadata.utils
import
MarketingSiteAPIClient
...
...
course_discovery/apps/course_metadata/search_indexes.py
View file @
c4f757cd
...
...
@@ -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.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
# Boost title over all other parameters (multiplicative)
# The max boost received from our boosting functions is ~6.
...
...
course_discovery/apps/course_metadata/tests/factories.py
View file @
c4f757cd
...
...
@@ -270,6 +270,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
banner_image_url
=
FuzzyText
(
prefix
=
'https://example.com/program/banner'
)
card_image_url
=
FuzzyText
(
prefix
=
'https://example.com/program/card'
)
partner
=
factory
.
SubFactory
(
PartnerFactory
)
video
=
factory
.
SubFactory
(
VideoFactory
)
overview
=
FuzzyText
()
total_hours_of_effort
=
FuzzyInteger
(
2
)
weeks_to_complete
=
FuzzyInteger
(
1
)
...
...
course_discovery/apps/course_metadata/tests/test_publishers.py
View file @
c4f757cd
...
...
@@ -7,18 +7,11 @@ import responses
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.exceptions
import
(
AliasCreateError
,
AliasDeleteError
,
FormRetrievalError
,
NodeCreateError
,
NodeDeleteError
,
NodeEditError
,
AliasCreateError
,
AliasDeleteError
,
FormRetrievalError
,
NodeCreateError
,
NodeDeleteError
,
NodeEditError
,
NodeLookupError
)
from
course_discovery.apps.course_metadata.publishers
import
(
BaseMarketingSitePublisher
,
CourseRunMarketingSitePublisher
,
ProgramMarketingSitePublisher
BaseMarketingSitePublisher
,
CourseRunMarketingSitePublisher
,
ProgramMarketingSitePublisher
)
from
course_discovery.apps.course_metadata.tests
import
toggle_switch
from
course_discovery.apps.course_metadata.tests.factories
import
CourseRunFactory
,
ProgramFactory
...
...
course_discovery/apps/edx_catalog_extensions/api/v1/tests/test_views.py
View file @
c4f757cd
...
...
@@ -4,10 +4,10 @@ import urllib.parse
import
pytz
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.test_search
import
(
ElasticsearchTestMixin
,
LoginMixin
,
SerializationMixin
,
SynonymTestMixin
from
course_discovery.apps.api.v1.tests.test_views.mixins
import
(
APITestCase
,
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.tests.factories
import
CourseFactory
,
CourseRunFactory
,
ProgramFactory
from
course_discovery.apps.edx_catalog_extensions.api.serializers
import
DistinctCountsAggregateFacetSearchSerializer
...
...
@@ -125,9 +125,9 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin,
for
record
in
objects
[
'results'
]:
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
:
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
):
""" Verify that the response is accurate when a search query is passed."""
...
...
course_discovery/apps/edx_haystack_extensions/distinct_counts/backends.py
View file @
c4f757cd
import
elasticsearch
from
django.conf
import
settings
from
haystack.backends.elasticsearch_backend
import
ElasticsearchSearchQuery
from
haystack.models
import
SearchResult
...
...
course_discovery/apps/edx_haystack_extensions/distinct_counts/query.py
View file @
c4f757cd
from
django.conf
import
settings
from
haystack.query
import
SearchQuerySet
from
course_discovery.apps.edx_haystack_extensions.distinct_counts.backends
import
DistinctCountsSearchQuery
...
...
course_discovery/apps/edx_haystack_extensions/management/commands/remove_unused_indexes.py
View file @
c4f757cd
...
...
@@ -4,7 +4,6 @@ from django.conf import settings
from
django.core.management.base
import
BaseCommand
from
haystack
import
connections
as
haystack_connections
logger
=
logging
.
getLogger
(
__name__
)
...
...
course_discovery/apps/publisher/admin.py
View file @
c4f757cd
...
...
@@ -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.choices
import
InternalUserRole
from
course_discovery.apps.publisher.constants
import
(
INTERNAL_USER_GROUP_NAME
,
PARTNER_MANAGER_GROUP_NAME
,
PROJECT_COORDINATOR_GROUP_NAME
,
PUBLISHER_GROUP_NAME
,
REVIEWER_GROUP_NAME
)
from
course_discovery.apps.publisher.constants
import
(
INTERNAL_USER_GROUP_NAME
,
PARTNER_MANAGER_GROUP_NAME
,
PROJECT_COORDINATOR_GROUP_NAME
,
PUBLISHER_GROUP_NAME
,
REVIEWER_GROUP_NAME
)
from
course_discovery.apps.publisher.forms
import
(
CourseRunAdminForm
,
CourseRunStateAdminForm
,
CourseStateAdminForm
,
OrganizationExtensionForm
,
PublisherUserCreationForm
,
UserAttributesAdminForm
)
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseEntitlement
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
OrganizationUserRole
,
PublisherUser
,
Seat
,
UserAttributes
)
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseEntitlement
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
OrganizationUserRole
,
PublisherUser
,
Seat
,
UserAttributes
)
@admin.register
(
CourseUserRole
)
...
...
course_discovery/apps/publisher/api/serializers.py
View file @
c4f757cd
...
...
@@ -13,9 +13,10 @@ from rest_framework import serializers
from
course_discovery.apps.core.models
import
User
from
course_discovery.apps.publisher.choices
import
PublisherUserRole
from
course_discovery.apps.publisher.emails
import
(
send_change_role_assignment_email
,
send_email_for_studio_instance_created
,
send_email_preview_accepted
,
send_email_preview_page_is_available
)
from
course_discovery.apps.publisher.emails
import
(
send_change_role_assignment_email
,
send_email_for_studio_instance_created
,
send_email_preview_accepted
,
send_email_preview_page_is_available
)
from
course_discovery.apps.publisher.models
import
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
...
...
course_discovery/apps/publisher/api/tests/test_serializers.py
View file @
c4f757cd
...
...
@@ -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.factories
import
OrganizationFactory
,
PersonFactory
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
from
course_discovery.apps.publisher.api.serializers
import
(
CourseRevisionSerializer
,
CourseRunSerializer
,
CourseRunStateSerializer
,
CourseStateSerializer
,
CourseUserRoleSerializer
,
GroupUserSerializer
)
from
course_discovery.apps.publisher.api.serializers
import
(
CourseRevisionSerializer
,
CourseRunSerializer
,
CourseRunStateSerializer
,
CourseStateSerializer
,
CourseUserRoleSerializer
,
GroupUserSerializer
)
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.tests.factories
import
(
CourseFactory
,
CourseRunFactory
,
CourseRunStateFactory
,
CourseStateFactory
,
CourseUserRoleFactory
,
OrganizationExtensionFactory
,
SeatFactory
)
from
course_discovery.apps.publisher.tests.factories
import
(
CourseFactory
,
CourseRunFactory
,
CourseRunStateFactory
,
CourseStateFactory
,
CourseUserRoleFactory
,
OrganizationExtensionFactory
,
SeatFactory
)
class
CourseUserRoleSerializerTests
(
SiteMixin
,
TestCase
):
...
...
course_discovery/apps/publisher/api/tests/test_utils.py
View file @
c4f757cd
import
pytest
from
course_discovery.apps.core.utils
import
serialize_datetime
from
course_discovery.apps.publisher.api.utils
import
(
serialize_entitlement_for_ecommerce_api
,
serialize_seat_for_ecommerce_api
)
from
course_discovery.apps.publisher.api.utils
import
(
serialize_entitlement_for_ecommerce_api
,
serialize_seat_for_ecommerce_api
)
from
course_discovery.apps.publisher.models
import
Seat
from
course_discovery.apps.publisher.tests.factories
import
CourseEntitlementFactory
,
SeatFactory
...
...
course_discovery/apps/publisher/api/tests/test_views.py
View file @
c4f757cd
...
...
@@ -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.choices
import
CourseRunStateChoices
,
CourseStateChoices
,
PublisherUserRole
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
,
OrganizationExtension
,
Seat
)
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseRun
,
CourseRunState
,
CourseState
,
OrganizationExtension
,
Seat
)
from
course_discovery.apps.publisher.tests
import
JSON_CONTENT_TYPE
,
factories
...
...
course_discovery/apps/publisher/api/urls.py
View file @
c4f757cd
""" Publisher API URLs. """
from
django.conf.urls
import
include
,
url
from
course_discovery.apps.publisher.api.views
import
(
AcceptAllRevisionView
,
ChangeCourseRunStateView
,
ChangeCourseStateView
,
CourseRevisionDetailView
,
CourseRoleAssignmentView
,
CoursesAutoComplete
,
OrganizationGroupUserView
,
RevertCourseRevisionView
,
UpdateCourseRunView
)
from
course_discovery.apps.publisher.api.views
import
(
AcceptAllRevisionView
,
ChangeCourseRunStateView
,
ChangeCourseStateView
,
CourseRevisionDetailView
,
CourseRoleAssignmentView
,
CoursesAutoComplete
,
OrganizationGroupUserView
,
RevertCourseRevisionView
,
UpdateCourseRunView
)
urlpatterns
=
[
url
(
r'^course_role_assignments/(?P<pk>\d+)/$'
,
CourseRoleAssignmentView
.
as_view
(),
name
=
'course_role_assignments'
),
...
...
course_discovery/apps/publisher/api/v1/tests/test_views.py
View file @
c4f757cd
...
...
@@ -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.tests.factories
import
OrganizationFactory
,
PersonFactory
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
from
course_discovery.apps.publisher.api.utils
import
(
serialize_entitlement_for_ecommerce_api
,
serialize_seat_for_ecommerce_api
)
from
course_discovery.apps.publisher.api.utils
import
(
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.models
import
CourseEntitlement
,
Seat
from
course_discovery.apps.publisher.tests.factories
import
CourseEntitlementFactory
,
CourseRunFactory
,
SeatFactory
...
...
course_discovery/apps/publisher/api/views.py
View file @
c4f757cd
...
...
@@ -11,14 +11,17 @@ from rest_framework.views import APIView
from
course_discovery.apps.core.models
import
User
from
course_discovery.apps.publisher.api.paginations
import
LargeResultsSetPagination
from
course_discovery.apps.publisher.api.permissions
import
(
CanViewAssociatedCourse
,
InternalUserPermission
,
PublisherUserPermission
)
from
course_discovery.apps.publisher.api.serializers
import
(
CourseRevisionSerializer
,
CourseRunSerializer
,
CourseRunStateSerializer
,
CourseStateSerializer
,
CourseUserRoleSerializer
,
GroupUserSerializer
)
from
course_discovery.apps.publisher.api.permissions
import
(
CanViewAssociatedCourse
,
InternalUserPermission
,
PublisherUserPermission
)
from
course_discovery.apps.publisher.api.serializers
import
(
CourseRevisionSerializer
,
CourseRunSerializer
,
CourseRunStateSerializer
,
CourseStateSerializer
,
CourseUserRoleSerializer
,
GroupUserSerializer
)
from
course_discovery.apps.publisher.forms
import
CourseForm
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
PublisherUser
)
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
PublisherUser
)
logger
=
logging
.
getLogger
(
__name__
)
...
...
course_discovery/apps/publisher/assign_permissions.py
View file @
c4f757cd
from
django.contrib.auth.models
import
Group
from
guardian.shortcuts
import
assign_perm
from
course_discovery.apps.publisher.constants
import
(
GENERAL_STAFF_GROUP_NAME
,
LEGAL_TEAM_GROUP_NAME
,
PARTNER_SUPPORT_GROUP_NAME
,
PROJECT_COORDINATOR_GROUP_NAME
,
REVIEWER_GROUP_NAME
)
from
course_discovery.apps.publisher.constants
import
(
GENERAL_STAFF_GROUP_NAME
,
LEGAL_TEAM_GROUP_NAME
,
PARTNER_SUPPORT_GROUP_NAME
,
PROJECT_COORDINATOR_GROUP_NAME
,
REVIEWER_GROUP_NAME
)
from
course_discovery.apps.publisher.models
import
OrganizationExtension
...
...
course_discovery/apps/publisher/migrations/0019_create_user_groups.py
View file @
c4f757cd
...
...
@@ -3,8 +3,9 @@ from __future__ import unicode_literals
from
django.db
import
migrations
from
course_discovery.apps.publisher.constants
import
(
PARTNER_COORDINATOR_GROUP_NAME
,
PUBLISHER_GROUP_NAME
,
REVIEWER_GROUP_NAME
)
from
course_discovery.apps.publisher.constants
import
(
PARTNER_COORDINATOR_GROUP_NAME
,
PUBLISHER_GROUP_NAME
,
REVIEWER_GROUP_NAME
)
GROUPS
=
[
PARTNER_COORDINATOR_GROUP_NAME
,
REVIEWER_GROUP_NAME
,
PUBLISHER_GROUP_NAME
]
...
...
course_discovery/apps/publisher/migrations/0061_add_people_permission.py
View file @
c4f757cd
...
...
@@ -4,8 +4,10 @@ from __future__ import unicode_literals
from
django.db
import
migrations
from
course_discovery.apps.publisher.constants
import
(
INTERNAL_USER_GROUP_NAME
,
PARTNER_COORDINATOR_GROUP_NAME
,
PUBLISHER_GROUP_NAME
,
REVIEWER_GROUP_NAME
)
from
course_discovery.apps.publisher.constants
import
(
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
]
...
...
course_discovery/apps/publisher/models.py
View file @
c4f757cd
...
...
@@ -29,7 +29,6 @@ from course_discovery.apps.publisher.choices import (
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.validators
import
ImageMultiSizeValidator
logger
=
logging
.
getLogger
(
__name__
)
...
...
course_discovery/apps/publisher/tests/factories.py
View file @
c4f757cd
...
...
@@ -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.ietf_language_tags.models
import
LanguageTag
from
course_discovery.apps.publisher.choices
import
PublisherUserRole
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseEntitlement
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
OrganizationUserRole
,
Seat
,
UserAttributes
)
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseEntitlement
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
OrganizationUserRole
,
Seat
,
UserAttributes
)
class
CourseFactory
(
factory
.
DjangoModelFactory
):
...
...
course_discovery/apps/publisher/tests/test_admin.py
View file @
c4f757cd
...
...
@@ -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.course_metadata.tests.factories
import
OrganizationFactory
from
course_discovery.apps.publisher.choices
import
PublisherUserRole
from
course_discovery.apps.publisher.constants
import
(
PARTNER_MANAGER_GROUP_NAME
,
PROJECT_COORDINATOR_GROUP_NAME
,
PUBLISHER_GROUP_NAME
,
REVIEWER_GROUP_NAME
)
from
course_discovery.apps.publisher.constants
import
(
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.models
import
CourseRun
,
OrganizationExtension
from
course_discovery.apps.publisher.tests
import
factories
...
...
course_discovery/apps/publisher/tests/test_models.py
View file @
c4f757cd
...
...
@@ -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.publisher.choices
import
CourseRunStateChoices
,
CourseStateChoices
,
PublisherUserRole
from
course_discovery.apps.publisher.mixins
import
check_course_organization_permission
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseUserRole
,
OrganizationExtension
,
OrganizationUserRole
,
Seat
)
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseUserRole
,
OrganizationExtension
,
OrganizationUserRole
,
Seat
)
from
course_discovery.apps.publisher.tests
import
factories
...
...
course_discovery/apps/publisher/tests/test_utils.py
View file @
c4f757cd
...
...
@@ -9,16 +9,18 @@ from guardian.shortcuts import assign_perm
from
mock
import
Mock
from
course_discovery.apps.core.tests.factories
import
UserFactory
from
course_discovery.apps.publisher.constants
import
(
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.constants
import
(
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.models
import
OrganizationExtension
from
course_discovery.apps.publisher.tests
import
factories
from
course_discovery.apps.publisher.utils
import
(
get_internal_users
,
has_role_for_course
,
is_email_notification_enabled
,
is_internal_user
,
is_project_coordinator_user
,
is_publisher_admin
,
is_publisher_user
,
make_bread_crumbs
,
parse_datetime_field
)
from
course_discovery.apps.publisher.utils
import
(
get_internal_users
,
has_role_for_course
,
is_email_notification_enabled
,
is_internal_user
,
is_project_coordinator_user
,
is_publisher_admin
,
is_publisher_user
,
make_bread_crumbs
,
parse_datetime_field
)
@ddt.ddt
...
...
course_discovery/apps/publisher/tests/test_wrapper.py
View file @
c4f757cd
...
...
@@ -6,8 +6,9 @@ import ddt
from
django.test
import
TestCase
from
course_discovery.apps.course_metadata.choices
import
CourseRunPacing
from
course_discovery.apps.course_metadata.tests.factories
import
(
OrganizationFactory
,
PersonFactory
,
PersonSocialNetworkFactory
,
PositionFactory
)
from
course_discovery.apps.course_metadata.tests.factories
import
(
OrganizationFactory
,
PersonFactory
,
PersonSocialNetworkFactory
,
PositionFactory
)
from
course_discovery.apps.publisher.choices
import
CourseRunStateChoices
,
PublisherUserRole
from
course_discovery.apps.publisher.models
import
Seat
from
course_discovery.apps.publisher.tests
import
factories
...
...
course_discovery/apps/publisher/utils.py
View file @
c4f757cd
""" Publisher Utils."""
import
re
from
dateutil
import
parser
from
course_discovery.apps.core.models
import
User
from
course_discovery.apps.publisher.constants
import
(
ADMIN_GROUP_NAME
,
INTERNAL_USER_GROUP_NAME
,
PROJECT_COORDINATOR_GROUP_NAME
)
from
course_discovery.apps.publisher.constants
import
(
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._-]*$'
)
...
...
course_discovery/apps/publisher/views.py
View file @
c4f757cd
...
...
@@ -32,8 +32,8 @@ from course_discovery.apps.publisher.forms import (
AdminImportCourseForm
,
CourseEntitlementForm
,
CourseForm
,
CourseRunForm
,
CourseSearchForm
,
SeatForm
)
from
course_discovery.apps.publisher.models
import
(
Course
,
CourseEntitlement
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
PublisherUser
,
Seat
,
UserAttributes
Course
,
CourseEntitlement
,
CourseRun
,
CourseRunState
,
CourseState
,
CourseUserRole
,
OrganizationExtension
,
PublisherUser
,
Seat
,
UserAttributes
)
from
course_discovery.apps.publisher.utils
import
(
get_internal_users
,
has_role_for_course
,
is_internal_user
,
is_project_coordinator_user
,
is_publisher_admin
,
...
...
course_discovery/apps/publisher_comments/models.py
View file @
c4f757cd
import
logging
import
waffle
from
django.db
import
models
,
transaction
from
django.utils.translation
import
ugettext_lazy
as
_
...
...
course_discovery/apps/publisher_comments/urls.py
View file @
c4f757cd
...
...
@@ -3,7 +3,6 @@ URLs for the course publisher comments views.
"""
from
django.conf.urls
import
include
,
url
urlpatterns
=
[
url
(
r'^api/'
,
include
(
'course_discovery.apps.publisher_comments.api.urls'
,
namespace
=
'api'
)),
]
course_discovery/settings/devstack_test.py
View file @
c4f757cd
from
course_discovery.settings.devstack
import
*
# noinspection PyUnresolvedReferences
from
course_discovery.settings.shared.test
import
*
# isort:skip
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment