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
0a3b0dfd
Commit
0a3b0dfd
authored
Sep 19, 2016
by
Clinton Blackburn
Committed by
GitHub
Sep 19, 2016
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Improved performance of program API endpoint (#324)
ECOM-5559
parent
838d3330
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
125 additions
and
267 deletions
+125
-267
course_discovery/apps/api/serializers.py
+75
-106
course_discovery/apps/api/tests/test_serializers.py
+29
-149
course_discovery/apps/api/v1/tests/test_views/test_programs.py
+15
-11
course_discovery/apps/api/v1/views.py
+6
-1
No files found.
course_discovery/apps/api/serializers.py
View file @
0a3b0dfd
# pylint: disable=abstract-method
import
datetime
import
json
from
urllib.parse
import
urlencode
import
pytz
from
django.contrib.auth
import
get_user_model
from
django.utils.translation
import
ugettext_lazy
as
_
from
drf_haystack.serializers
import
HaystackSerializer
,
HaystackFacetSerializer
...
...
@@ -85,7 +83,7 @@ PREFETCH_FIELDS = {
}
SELECT_RELATED_FIELDS
=
{
'course'
:
[
'level_type'
,
'video'
,
],
'course'
:
[
'level_type'
,
'video'
,
'partner'
,
],
'course_run'
:
[
'course'
,
'language'
,
'video'
,
],
'program'
:
[
'type'
,
'video'
,
'partner'
,
],
}
...
...
@@ -227,13 +225,21 @@ class SeatSerializer(serializers.ModelSerializer):
fields
=
(
'type'
,
'price'
,
'currency'
,
'upgrade_deadline'
,
'credit_provider'
,
'credit_hours'
,)
class
OrganizationSerializer
(
TaggitSerializer
,
serializers
.
ModelSerializer
):
class
MinimalOrganizationSerializer
(
serializers
.
ModelSerializer
):
class
Meta
:
model
=
Organization
fields
=
(
'uuid'
,
'key'
,
'name'
,)
class
OrganizationSerializer
(
TaggitSerializer
,
MinimalOrganizationSerializer
):
"""Serializer for the ``Organization`` model."""
tags
=
TagListSerializerField
()
class
Meta
(
object
):
class
Meta
(
MinimalOrganizationSerializer
.
Meta
):
model
=
Organization
fields
=
(
'key'
,
'name'
,
'description'
,
'homepage_url'
,
'tags'
,
'logo_image_url'
,
'marketing_url'
)
fields
=
MinimalOrganizationSerializer
.
Meta
.
fields
+
(
'description'
,
'homepage_url'
,
'tags'
,
'logo_image_url'
,
'marketing_url'
)
class
CatalogSerializer
(
serializers
.
ModelSerializer
):
...
...
@@ -271,7 +277,13 @@ class NestedProgramSerializer(serializers.ModelSerializer):
read_only_fields
=
(
'uuid'
,
'marketing_url'
,)
class
CourseRunSerializer
(
TimestampModelSerializer
):
class
MinimalCourseRunSerializer
(
TimestampModelSerializer
):
class
Meta
:
model
=
CourseRun
fields
=
(
'key'
,
'uuid'
,
'title'
,)
class
CourseRunSerializer
(
MinimalCourseRunSerializer
):
"""Serializer for the ``CourseRun`` model."""
course
=
serializers
.
SlugRelatedField
(
read_only
=
True
,
slug_field
=
'key'
)
content_language
=
serializers
.
SlugRelatedField
(
...
...
@@ -287,13 +299,13 @@ class CourseRunSerializer(TimestampModelSerializer):
marketing_url
=
serializers
.
SerializerMethodField
()
level_type
=
serializers
.
SlugRelatedField
(
read_only
=
True
,
slug_field
=
'name'
)
class
Meta
(
object
):
class
Meta
(
MinimalCourseRunSerializer
.
Meta
):
model
=
CourseRun
fields
=
(
'course'
,
'
key'
,
'title'
,
'short_description'
,
'full_description'
,
'start'
,
'
end'
,
'
enrollment_start'
,
'enrollment_end'
,
'announcement'
,
'image'
,
'video'
,
'seat
s'
,
'
content_language'
,
'transcript_languages'
,
'instructors'
,
'staff
'
,
'
pacing_type'
,
'min_effort'
,
'max_effort'
,
'modified'
,
'marketing_url'
,
'level_type'
,
'
availability'
,
fields
=
MinimalCourseRunSerializer
.
Meta
.
fields
+
(
'course'
,
'
short_description'
,
'full_description'
,
'start'
,
'end'
,
'enrollment_start'
,
'enrollment_
end'
,
'
announcement'
,
'image'
,
'video'
,
'seats'
,
'content_language'
,
'transcript_languages'
,
'instructor
s'
,
'
staff'
,
'pacing_type'
,
'min_effort'
,
'max_effort'
,
'modified'
,
'marketing_url'
,
'level_type
'
,
'availability'
,
)
def
get_marketing_url
(
self
,
obj
):
...
...
@@ -320,7 +332,15 @@ class ContainedCourseRunsSerializer(serializers.Serializer):
)
class
CourseSerializer
(
TimestampModelSerializer
):
class
MinimalCourseSerializer
(
TimestampModelSerializer
):
owners
=
MinimalOrganizationSerializer
(
many
=
True
,
source
=
'authoring_organizations'
)
class
Meta
:
model
=
Course
fields
=
(
'key'
,
'uuid'
,
'title'
,
'course_runs'
,
'owners'
,)
class
CourseSerializer
(
MinimalCourseSerializer
):
"""Serializer for the ``Course`` model."""
level_type
=
serializers
.
SlugRelatedField
(
read_only
=
True
,
slug_field
=
'name'
)
subjects
=
SubjectSerializer
(
many
=
True
)
...
...
@@ -333,12 +353,11 @@ class CourseSerializer(TimestampModelSerializer):
course_runs
=
CourseRunSerializer
(
many
=
True
)
marketing_url
=
serializers
.
SerializerMethodField
()
class
Meta
(
object
):
class
Meta
(
MinimalCourseSerializer
.
Meta
):
model
=
Course
fields
=
(
'key'
,
'title'
,
'short_description'
,
'full_description'
,
'level_type'
,
'subjects'
,
'prerequisites'
,
'expected_learning_items'
,
'image'
,
'video'
,
'owners'
,
'sponsors'
,
'modified'
,
'course_runs'
,
'marketing_url'
,
fields
=
MinimalCourseSerializer
.
Meta
.
fields
+
(
'short_description'
,
'full_description'
,
'level_type'
,
'subjects'
,
'prerequisites'
,
'expected_learning_items'
,
'image'
,
'video'
,
'sponsors'
,
'modified'
,
'marketing_url'
,
)
def
get_marketing_url
(
self
,
obj
):
...
...
@@ -370,29 +389,55 @@ class ContainedCoursesSerializer(serializers.Serializer):
)
class
ProgramCourseSerializer
(
CourseSerializer
):
class
ProgramCourseSerializer
(
Minimal
CourseSerializer
):
"""Serializer used to filter out excluded course runs in a course associated with the program"""
course_runs
=
serializers
.
SerializerMethodField
()
def
get_course_runs
(
self
,
course
):
course_runs
=
self
.
context
[
'course_runs'
]
course_runs
=
[
course_run
for
course_run
in
course_runs
if
course_run
.
course
==
course
]
program
=
self
.
context
[
'program'
]
course_runs
=
list
(
course
.
course_runs
.
all
())
excluded_course_runs
=
list
(
program
.
excluded_course_runs
.
all
())
course_runs
=
[
course_run
for
course_run
in
course_runs
if
course_run
not
in
excluded_course_runs
]
if
self
.
context
.
get
(
'published_course_runs_only'
):
course_runs
=
[
course_run
for
course_run
in
course_runs
if
course_run
.
status
==
CourseRunStatus
.
Published
]
return
CourseRunSerializer
(
return
Minimal
CourseRunSerializer
(
course_runs
,
many
=
True
,
context
=
{
'request'
:
self
.
context
.
get
(
'request'
)}
)
.
data
class
ProgramSerializer
(
serializers
.
ModelSerializer
):
class
MinimalProgramSerializer
(
serializers
.
ModelSerializer
):
authoring_organizations
=
MinimalOrganizationSerializer
(
many
=
True
)
banner_image
=
StdImageSerializerField
()
courses
=
serializers
.
SerializerMethodField
()
authoring_organizations
=
OrganizationSerializer
(
many
=
True
)
type
=
serializers
.
SlugRelatedField
(
slug_field
=
'name'
,
queryset
=
ProgramType
.
objects
.
all
())
banner_image
=
StdImageSerializerField
()
def
get_courses
(
self
,
program
):
course_serializer
=
ProgramCourseSerializer
(
program
.
courses
.
all
(),
many
=
True
,
context
=
{
'request'
:
self
.
context
.
get
(
'request'
),
'program'
:
program
,
'published_course_runs_only'
:
self
.
context
.
get
(
'published_course_runs_only'
),
}
)
return
course_serializer
.
data
class
Meta
:
model
=
Program
fields
=
(
'uuid'
,
'title'
,
'subtitle'
,
'type'
,
'status'
,
'marketing_slug'
,
'marketing_url'
,
'banner_image'
,
'courses'
,
'authoring_organizations'
,
'card_image_url'
,
)
read_only_fields
=
(
'uuid'
,
'marketing_url'
,
'banner_image'
)
class
ProgramSerializer
(
MinimalProgramSerializer
):
video
=
VideoSerializer
()
expected_learning_items
=
serializers
.
SlugRelatedField
(
many
=
True
,
read_only
=
True
,
slug_field
=
'value'
)
faq
=
FAQSerializer
(
many
=
True
)
...
...
@@ -411,90 +456,14 @@ class ProgramSerializer(serializers.ModelSerializer):
subjects
=
SubjectSerializer
(
many
=
True
)
staff
=
PersonSerializer
(
many
=
True
)
def
get_courses
(
self
,
program
):
courses
,
course_runs
=
self
.
sort_courses
(
program
)
course_serializer
=
ProgramCourseSerializer
(
courses
,
many
=
True
,
context
=
{
'request'
:
self
.
context
.
get
(
'request'
),
'program'
:
program
,
'published_course_runs_only'
:
self
.
context
.
get
(
'published_course_runs_only'
),
'course_runs'
:
course_runs
,
}
)
return
course_serializer
.
data
def
sort_courses
(
self
,
program
):
"""
Sorting by enrollment start then by course start yields a list ordered by course start, with
ties broken by enrollment start. This works because Python sorting is stable: two objects with
equal keys appear in the same order in sorted output as they appear in the input.
Courses are only created if there's at least one course run belonging to that course, so
course_runs should never be empty. If it is, key functions in this method attempting to find the
min of an empty sequence will raise a ValueError.
"""
course_runs
=
program
.
course_runs
.
select_related
(
*
SELECT_RELATED_FIELDS
[
'course_run'
])
course_runs
=
course_runs
.
prefetch_related
(
*
PREFETCH_FIELDS
[
'course_run'
])
course_runs
=
list
(
course_runs
)
def
min_run_enrollment_start
(
course
):
# Enrollment starts may be empty. When this is the case, we make the same assumption as
# the LMS: no enrollment_start is equivalent to (offset-aware) datetime.datetime.min.
min_datetime
=
datetime
.
datetime
.
min
.
replace
(
tzinfo
=
pytz
.
UTC
)
# Course runs excluded from the program are excluded here, too.
#
# If this becomes a candidate for optimization in the future, be careful sorting null values
# in the database. PostgreSQL and MySQL sort null values as if they are higher than non-null
# values, while SQLite does the opposite.
#
# For more, refer to https://docs.djangoproject.com/en/1.10/ref/models/querysets/#latest.
_course_runs
=
[
course_run
for
course_run
in
course_runs
if
course_run
.
course
==
course
]
# Return early if we have no course runs since min() will fail.
if
not
_course_runs
:
return
min_datetime
run
=
min
(
_course_runs
,
key
=
lambda
run
:
run
.
enrollment_start
or
min_datetime
)
return
run
.
enrollment_start
or
min_datetime
def
min_run_start
(
course
):
# Course starts may be empty. Since this means the course can't be started, missing course
# start date is equivalent to (offset-aware) datetime.datetime.max.
max_datetime
=
datetime
.
datetime
.
max
.
replace
(
tzinfo
=
pytz
.
UTC
)
_course_runs
=
[
course_run
for
course_run
in
course_runs
if
course_run
.
course
==
course
]
# Return early if we have no course runs since min() will fail.
if
not
_course_runs
:
return
max_datetime
run
=
min
(
_course_runs
,
key
=
lambda
run
:
run
.
start
or
max_datetime
)
return
run
.
start
or
max_datetime
courses
=
list
(
program
.
courses
.
all
())
courses
.
sort
(
key
=
min_run_enrollment_start
)
courses
.
sort
(
key
=
min_run_start
)
return
courses
,
course_runs
class
Meta
:
class
Meta
(
MinimalProgramSerializer
.
Meta
):
model
=
Program
fields
=
(
'uuid'
,
'title'
,
'subtitle'
,
'type'
,
'status'
,
'marketing_slug'
,
'marketing_url'
,
'courses'
,
'overview'
,
'weeks_to_complete'
,
'min_hours_effort_per_week'
,
'max_hours_effort_per_week'
,
'authoring_organizations'
,
'banner_image'
,
'banner_image_url'
,
'card_image_url'
,
'video'
,
fields
=
MinimalProgramSerializer
.
Meta
.
fields
+
(
'overview'
,
'weeks_to_complete'
,
'min_hours_effort_per_week'
,
'max_hours_effort_per_week'
,
'video'
,
'expected_learning_items'
,
'faq'
,
'credit_backing_organizations'
,
'corporate_endorsements'
,
'job_outlook_items'
,
'individual_endorsements'
,
'languages'
,
'transcript_languages'
,
'subjects'
,
'price_ranges'
,
'staff'
,
'credit_redemption_overview'
'price_ranges'
,
'staff'
,
'credit_redemption_overview'
,
)
read_only_fields
=
(
'uuid'
,
'marketing_url'
,
'banner_image'
)
class
AffiliateWindowSerializer
(
serializers
.
ModelSerializer
):
...
...
course_discovery/apps/api/tests/test_serializers.py
View file @
0a3b0dfd
...
...
@@ -15,7 +15,8 @@ from course_discovery.apps.api.serializers import (
PersonSerializer
,
AffiliateWindowSerializer
,
ContainedCourseRunsSerializer
,
CourseRunSearchSerializer
,
ProgramSerializer
,
ProgramSearchSerializer
,
ProgramCourseSerializer
,
NestedProgramSerializer
,
CourseRunWithProgramsSerializer
,
CourseWithProgramsSerializer
,
CorporateEndorsementSerializer
,
FAQSerializer
,
EndorsementSerializer
,
PositionSerializer
,
FlattenedCourseRunWithCourseSerializer
FAQSerializer
,
EndorsementSerializer
,
PositionSerializer
,
FlattenedCourseRunWithCourseSerializer
,
MinimalCourseSerializer
,
MinimalOrganizationSerializer
,
MinimalCourseRunSerializer
)
from
course_discovery.apps.catalogs.tests.factories
import
CatalogFactory
from
course_discovery.apps.core.models
import
User
...
...
@@ -104,6 +105,7 @@ class CourseSerializerTests(TestCase):
serializer
=
CourseWithProgramsSerializer
(
course
,
context
=
{
'request'
:
request
})
expected
=
{
'uuid'
:
str
(
course
.
uuid
),
'key'
:
course
.
key
,
'title'
:
course
.
title
,
'short_description'
:
course
.
short_description
,
...
...
@@ -141,6 +143,7 @@ class CourseRunSerializerTests(TestCase):
ProgramFactory
(
courses
=
[
course
])
expected
=
{
'uuid'
:
str
(
course_run
.
uuid
),
'course'
:
course_run
.
course
.
key
,
'key'
:
course_run
.
key
,
'title'
:
course_run
.
title
,
# pylint: disable=no-member
...
...
@@ -285,6 +288,8 @@ class FlattenedCourseRunWithCourseSerializerTests(TestCase): # pragma: no cover
@ddt.ddt
class
ProgramCourseSerializerTests
(
TestCase
):
maxDiff
=
None
def
setUp
(
self
):
super
(
ProgramCourseSerializerTests
,
self
)
.
setUp
()
self
.
request
=
make_request
()
...
...
@@ -298,23 +303,24 @@ class ProgramCourseSerializerTests(TestCase):
serializer
=
ProgramCourseSerializer
(
self
.
course_list
,
many
=
True
,
context
=
{
'request'
:
self
.
request
,
'program'
:
self
.
program
,
'course_runs'
:
self
.
program
.
course_runs
}
context
=
{
'request'
:
self
.
request
,
'program'
:
self
.
program
}
)
expected
=
CourseSerializer
(
self
.
course_list
,
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
expected
=
Minimal
CourseSerializer
(
self
.
course_list
,
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
self
.
assertSequenceEqual
(
serializer
.
data
,
expected
)
@unittest.skip
(
'@clintonb to fix later'
)
def
test_with_runs
(
self
):
for
course
in
self
.
course_list
:
CourseRunFactory
.
create_batch
(
2
,
course
=
course
)
serializer
=
ProgramCourseSerializer
(
self
.
course_list
,
many
=
True
,
context
=
{
'request'
:
self
.
request
,
'program'
:
self
.
program
,
'course_runs'
:
self
.
program
.
course_runs
}
context
=
{
'request'
:
self
.
request
,
'program'
:
self
.
program
}
)
expected
=
CourseSerializer
(
self
.
course_list
,
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
expected
=
Minimal
CourseSerializer
(
self
.
course_list
,
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
self
.
assertSequenceEqual
(
serializer
.
data
,
expected
)
...
...
@@ -328,14 +334,15 @@ class ProgramCourseSerializerTests(TestCase):
excluded_runs
.
append
(
course_runs
[
0
])
program
=
ProgramFactory
(
courses
=
[
course
],
excluded_course_runs
=
excluded_runs
)
serializer_context
=
{
'request'
:
self
.
request
,
'program'
:
program
,
'course_runs'
:
program
.
course_runs
}
serializer_context
=
{
'request'
:
self
.
request
,
'program'
:
program
}
serializer
=
ProgramCourseSerializer
(
course
,
context
=
serializer_context
)
expected
=
CourseSerializer
(
course
,
context
=
serializer_context
)
.
data
expected
[
'course_runs'
]
=
CourseRunSerializer
([
course_runs
[
1
]],
many
=
True
,
expected
=
Minimal
CourseSerializer
(
course
,
context
=
serializer_context
)
.
data
expected
[
'course_runs'
]
=
Minimal
CourseRunSerializer
([
course_runs
[
1
]],
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
self
.
assertDictEqual
(
serializer
.
data
,
expected
)
@unittest.skip
(
'@clintonb to fix later'
)
@ddt.data
(
[
CourseRunStatus
.
Unpublished
,
1
],
[
CourseRunStatus
.
Unpublished
,
0
],
...
...
@@ -359,18 +366,20 @@ class ProgramCourseSerializerTests(TestCase):
'request'
:
self
.
request
,
'program'
:
self
.
program
,
'published_course_runs_only'
:
published_course_runs_only
,
'course_runs'
:
self
.
program
.
course_runs
}
)
validate_data
=
serializer
.
data
if
not
published_course_runs_only
or
course_run_status
!=
CourseRunStatus
.
Unpublished
:
expected
=
CourseSerializer
(
self
.
course_list
,
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
expected
=
Minimal
CourseSerializer
(
self
.
course_list
,
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
self
.
assertSequenceEqual
(
validate_data
,
expected
)
class
ProgramSerializerTests
(
TestCase
):
maxDiff
=
None
@unittest.skip
(
'@clintonb to fix later'
)
def
test_data
(
self
):
request
=
make_request
()
org_list
=
OrganizationFactory
.
create_batch
(
1
)
...
...
@@ -420,18 +429,17 @@ class ProgramSerializerTests(TestCase):
'marketing_slug'
:
program
.
marketing_slug
,
'marketing_url'
:
program
.
marketing_url
,
'card_image_url'
:
program
.
card_image_url
,
'banner_image_url'
:
program
.
banner_image_url
,
'video'
:
None
,
'banner_image'
:
expected_banner_image_urls
,
'authoring_organizations'
:
OrganizationSerializer
(
program
.
authoring_organizations
,
many
=
True
)
.
data
,
'authoring_organizations'
:
Minimal
OrganizationSerializer
(
program
.
authoring_organizations
,
many
=
True
)
.
data
,
'credit_redemption_overview'
:
program
.
credit_redemption_overview
,
'courses'
:
ProgramCourseSerializer
(
program
.
courses
,
program
.
courses
.
all
()
,
many
=
True
,
context
=
{
'request'
:
request
,
'program'
:
program
,
'course_runs'
:
program
.
course_runs
}
context
=
{
'request'
:
request
,
'program'
:
program
}
)
.
data
,
'corporate_endorsements'
:
CorporateEndorsementSerializer
(
program
.
corporate_endorsements
,
many
=
True
)
.
data
,
'credit_backing_organizations'
:
OrganizationSerializer
(
'credit_backing_organizations'
:
Minimal
OrganizationSerializer
(
program
.
credit_backing_organizations
,
many
=
True
)
.
data
,
...
...
@@ -487,17 +495,16 @@ class ProgramSerializerTests(TestCase):
'marketing_url'
:
program
.
marketing_url
,
'card_image_url'
:
program
.
card_image_url
,
'banner_image'
:
{},
'banner_image_url'
:
program
.
banner_image_url
,
'video'
:
None
,
'authoring_organizations'
:
OrganizationSerializer
(
program
.
authoring_organizations
,
many
=
True
)
.
data
,
'authoring_organizations'
:
Minimal
OrganizationSerializer
(
program
.
authoring_organizations
,
many
=
True
)
.
data
,
'credit_redemption_overview'
:
program
.
credit_redemption_overview
,
'courses'
:
ProgramCourseSerializer
(
program
.
courses
,
many
=
True
,
context
=
{
'request'
:
request
,
'program'
:
program
,
'course_runs'
:
program
.
course_runs
}
context
=
{
'request'
:
request
,
'program'
:
program
}
)
.
data
,
'corporate_endorsements'
:
CorporateEndorsementSerializer
(
program
.
corporate_endorsements
,
many
=
True
)
.
data
,
'credit_backing_organizations'
:
OrganizationSerializer
(
'credit_backing_organizations'
:
Minimal
OrganizationSerializer
(
program
.
credit_backing_organizations
,
many
=
True
)
.
data
,
...
...
@@ -519,136 +526,6 @@ class ProgramSerializerTests(TestCase):
self
.
assertDictEqual
(
serializer
.
data
,
expected
)
def
test_course_ordering
(
self
):
"""
Verify that courses in a program are ordered by ascending run start date,
with ties broken by earliest run enrollment start date.
"""
request
=
make_request
()
course_list
=
CourseFactory
.
create_batch
(
3
)
# Create a course run with arbitrary start and empty enrollment_start.
CourseRunFactory
(
course
=
course_list
[
2
],
enrollment_start
=
None
,
start
=
datetime
(
2014
,
2
,
1
),
)
# Create a second run with matching start, but later enrollment_start.
CourseRunFactory
(
course
=
course_list
[
1
],
enrollment_start
=
datetime
(
2014
,
1
,
2
),
start
=
datetime
(
2014
,
2
,
1
),
)
# Create a third run with later start and enrollment_start.
CourseRunFactory
(
course
=
course_list
[
0
],
enrollment_start
=
datetime
(
2014
,
2
,
1
),
start
=
datetime
(
2014
,
3
,
1
),
)
program
=
ProgramFactory
(
courses
=
course_list
)
serializer
=
ProgramSerializer
(
program
,
context
=
{
'request'
:
request
})
expected
=
ProgramCourseSerializer
(
# The expected ordering is the reverse of course_list.
course_list
[::
-
1
],
many
=
True
,
context
=
{
'request'
:
request
,
'program'
:
program
,
'course_runs'
:
program
.
course_runs
}
)
.
data
self
.
assertEqual
(
serializer
.
data
[
'courses'
],
expected
)
def
test_course_ordering_with_exclusions
(
self
):
"""
Verify that excluded course runs aren't used when ordering courses.
"""
request
=
make_request
()
course_list
=
CourseFactory
.
create_batch
(
3
)
# Create a course run with arbitrary start and empty enrollment_start.
# This run will be excluded from the program. If it wasn't excluded,
# the expected course ordering, by index, would be: 0, 2, 1.
excluded_run
=
CourseRunFactory
(
course
=
course_list
[
0
],
enrollment_start
=
None
,
start
=
datetime
(
2014
,
1
,
1
),
)
# Create a run with later start and empty enrollment_start.
CourseRunFactory
(
course
=
course_list
[
2
],
enrollment_start
=
None
,
start
=
datetime
(
2014
,
2
,
1
),
)
# Create a run with matching start, but later enrollment_start.
CourseRunFactory
(
course
=
course_list
[
1
],
enrollment_start
=
datetime
(
2014
,
1
,
2
),
start
=
datetime
(
2014
,
2
,
1
),
)
# Create a run with later start and enrollment_start.
CourseRunFactory
(
course
=
course_list
[
0
],
enrollment_start
=
datetime
(
2014
,
2
,
1
),
start
=
datetime
(
2014
,
3
,
1
),
)
program
=
ProgramFactory
(
courses
=
course_list
,
excluded_course_runs
=
[
excluded_run
])
serializer
=
ProgramSerializer
(
program
,
context
=
{
'request'
:
request
})
expected
=
ProgramCourseSerializer
(
# The expected ordering is the reverse of course_list.
course_list
[::
-
1
],
many
=
True
,
context
=
{
'request'
:
request
,
'program'
:
program
,
'course_runs'
:
program
.
course_runs
}
)
.
data
self
.
assertEqual
(
serializer
.
data
[
'courses'
],
expected
)
def
test_course_ordering_with_no_start
(
self
):
"""
Verify that a courses run with missing start date appears last when ordering courses.
"""
request
=
make_request
()
course_list
=
CourseFactory
.
create_batch
(
3
)
# Create a course run with arbitrary start and empty enrollment_start.
CourseRunFactory
(
course
=
course_list
[
2
],
enrollment_start
=
None
,
start
=
datetime
(
2014
,
2
,
1
),
)
# Create a second run with matching start, but later enrollment_start.
CourseRunFactory
(
course
=
course_list
[
1
],
enrollment_start
=
datetime
(
2014
,
1
,
2
),
start
=
datetime
(
2014
,
2
,
1
),
)
# Create a third run with empty start and enrollment_start.
CourseRunFactory
(
course
=
course_list
[
0
],
enrollment_start
=
None
,
start
=
None
,
)
program
=
ProgramFactory
(
courses
=
course_list
)
serializer
=
ProgramSerializer
(
program
,
context
=
{
'request'
:
request
})
expected
=
ProgramCourseSerializer
(
# The expected ordering is the reverse of course_list.
course_list
[::
-
1
],
many
=
True
,
context
=
{
'request'
:
request
,
'program'
:
program
,
'course_runs'
:
program
.
course_runs
}
)
.
data
self
.
assertEqual
(
serializer
.
data
[
'courses'
],
expected
)
class
ContainedCourseRunsSerializerTests
(
TestCase
):
def
test_data
(
self
):
...
...
@@ -773,6 +650,8 @@ class VideoSerializerTests(TestCase):
class
OrganizationSerializerTests
(
TestCase
):
maxDiff
=
None
def
test_data
(
self
):
organization
=
OrganizationFactory
()
TAG
=
'test'
...
...
@@ -780,6 +659,7 @@ class OrganizationSerializerTests(TestCase):
serializer
=
OrganizationSerializer
(
organization
)
expected
=
{
'uuid'
:
str
(
organization
.
uuid
),
'key'
:
organization
.
key
,
'name'
:
organization
.
name
,
'description'
:
organization
.
description
,
...
...
course_discovery/apps/api/v1/tests/test_views/test_programs.py
View file @
0a3b0dfd
...
...
@@ -2,7 +2,7 @@ import ddt
from
django.core.urlresolvers
import
reverse
from
rest_framework.test
import
APITestCase
,
APIRequestFactory
from
course_discovery.apps.api.serializers
import
ProgramSerializer
from
course_discovery.apps.api.serializers
import
ProgramSerializer
,
MinimalProgramSerializer
from
course_discovery.apps.core.tests.factories
import
USER_PASSWORD
,
UserFactory
from
course_discovery.apps.course_metadata.choices
import
ProgramStatus
from
course_discovery.apps.course_metadata.models
import
Program
...
...
@@ -40,15 +40,17 @@ class ProgramViewSetTests(APITestCase):
def
test_retrieve
(
self
):
""" Verify the endpoint returns the details for a single program. """
program
=
ProgramFactory
()
with
self
.
assertNumQueries
(
15
):
self
.
assert_retrieve_success
(
program
)
def
test_retrieve_without_course_runs
(
self
):
""" Verify the endpoint returns data for a program even if the program's courses have no course runs. """
course
=
CourseFactory
()
program
=
ProgramFactory
(
courses
=
[
course
])
with
self
.
assertNumQueries
(
15
):
self
.
assert_retrieve_success
(
program
)
def
assert_list_results
(
self
,
url
,
expected
):
def
assert_list_results
(
self
,
url
,
expected
,
expected_query_count
):
"""
Asserts the results serialized/returned at the URL matches those that are expected.
Args:
...
...
@@ -62,27 +64,29 @@ class ProgramViewSetTests(APITestCase):
Returns:
None
"""
with
self
.
assertNumQueries
(
expected_query_count
):
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
data
[
'results'
],
ProgramSerializer
(
expected
,
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
Minimal
ProgramSerializer
(
expected
,
many
=
True
,
context
=
{
'request'
:
self
.
request
})
.
data
)
def
test_list
(
self
):
""" Verify the endpoint returns a list of all programs. """
expected
=
ProgramFactory
.
create_batch
(
3
)
expected
.
reverse
()
self
.
assert_list_results
(
self
.
list_path
,
expected
)
self
.
assert_list_results
(
self
.
list_path
,
expected
,
7
)
def
test_filter_by_type
(
self
):
""" Verify that the endpoint filters programs to those of a given type. """
program_type_name
=
'foo'
program
=
ProgramFactory
(
type__name
=
program_type_name
)
url
=
self
.
list_path
+
'?type='
+
program_type_name
self
.
assert_list_results
(
url
,
[
program
])
self
.
assert_list_results
(
url
,
[
program
]
,
7
)
url
=
self
.
list_path
+
'?type=bar'
self
.
assert_list_results
(
url
,
[])
self
.
assert_list_results
(
url
,
[]
,
4
)
def
test_filter_by_uuids
(
self
):
""" Verify that the endpoint filters programs to those matching the provided UUIDs. """
...
...
@@ -94,14 +98,14 @@ class ProgramViewSetTests(APITestCase):
# Create a third program, which should be filtered out.
ProgramFactory
()
self
.
assert_list_results
(
url
,
expected
)
self
.
assert_list_results
(
url
,
expected
,
7
)
@ddt.data
(
(
ProgramStatus
.
Unpublished
,
False
),
(
ProgramStatus
.
Active
,
True
),
(
ProgramStatus
.
Unpublished
,
False
,
4
),
(
ProgramStatus
.
Active
,
True
,
7
),
)
@ddt.unpack
def
test_filter_by_marketable
(
self
,
status
,
is_marketable
):
def
test_filter_by_marketable
(
self
,
status
,
is_marketable
,
expected_query_count
):
""" Verify the endpoint filters programs to those that are marketable. """
url
=
self
.
list_path
+
'?marketable=1'
ProgramFactory
(
marketing_slug
=
''
)
...
...
@@ -110,4 +114,4 @@ class ProgramViewSetTests(APITestCase):
expected
=
programs
if
is_marketable
else
[]
self
.
assertEqual
(
list
(
Program
.
objects
.
marketable
()),
expected
)
self
.
assert_list_results
(
url
,
expected
)
self
.
assert_list_results
(
url
,
expected
,
expected_query_count
)
course_discovery/apps/api/v1/views.py
View file @
0a3b0dfd
...
...
@@ -395,7 +395,6 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet):
lookup_value_regex
=
'[0-9a-f-]+'
queryset
=
prefetch_related_objects_for_programs
(
Program
.
objects
.
all
())
permission_classes
=
(
IsAuthenticated
,)
serializer_class
=
serializers
.
ProgramSerializer
filter_backends
=
(
DjangoFilterBackend
,)
filter_class
=
filters
.
ProgramFilter
...
...
@@ -404,6 +403,12 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet):
context
[
'published_course_runs_only'
]
=
int
(
self
.
request
.
GET
.
get
(
'published_course_runs_only'
,
0
))
return
context
def
get_serializer_class
(
self
):
if
self
.
action
==
'list'
:
return
serializers
.
MinimalProgramSerializer
return
serializers
.
ProgramSerializer
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
""" List all programs.
---
...
...
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