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
cf05b1f4
Commit
cf05b1f4
authored
Feb 08, 2017
by
Matthew Piatetsky
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add marketable_enrollable_course_runs_with_archived filter
ECOM-6820
parent
6ad30b26
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
135 additions
and
34 deletions
+135
-34
course_discovery/apps/api/serializers.py
+4
-0
course_discovery/apps/api/tests/test_serializers.py
+44
-13
course_discovery/apps/api/v1/tests/test_views/test_courses.py
+38
-14
course_discovery/apps/api/v1/views/courses.py
+12
-7
course_discovery/apps/course_metadata/query.py
+21
-0
course_discovery/apps/course_metadata/tests/test_query.py
+16
-0
No files found.
course_discovery/apps/api/serializers.py
View file @
cf05b1f4
...
@@ -514,6 +514,10 @@ class CourseWithProgramsSerializer(CourseSerializer):
...
@@ -514,6 +514,10 @@ class CourseWithProgramsSerializer(CourseSerializer):
# closed - achieving this requires applying both the marketable and active filters.
# closed - achieving this requires applying both the marketable and active filters.
course_runs
=
course_runs
.
marketable
()
.
active
()
course_runs
=
course_runs
.
marketable
()
.
active
()
if
self
.
context
.
get
(
'marketable_enrollable_course_runs_with_archived'
):
# Same as "marketable_course_runs_only", but includes courses with an end date in the past
course_runs
=
course_runs
.
marketable
()
.
enrollable
()
if
self
.
context
.
get
(
'published_course_runs_only'
):
if
self
.
context
.
get
(
'published_course_runs_only'
):
course_runs
=
course_runs
.
filter
(
status
=
CourseRunStatus
.
Published
)
course_runs
=
course_runs
.
filter
(
status
=
CourseRunStatus
.
Published
)
...
...
course_discovery/apps/api/tests/test_serializers.py
View file @
cf05b1f4
...
@@ -207,36 +207,27 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
...
@@ -207,36 +207,27 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
Verify that the marketable_course_runs_only option is respected, restricting returned
Verify that the marketable_course_runs_only option is respected, restricting returned
course runs to those that are published, have seats, and can still be enrolled in.
course runs to those that are published, have seats, and can still be enrolled in.
"""
"""
# Published course run with a seat, no enrollment start or end, and an end date in the future.
enrollable_course_run
=
CourseRunFactory
(
enrollable_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
status
=
CourseRunStatus
.
Published
,
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
10
),
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
10
),
enrollment_start
=
None
,
enrollment_start
=
None
,
enrollment_end
=
None
,
enrollment_end
=
None
,
course
=
self
.
course
)
)
SeatFactory
(
course_run
=
enrollable_course_run
)
SeatFactory
(
course_run
=
enrollable_course_run
)
# Unpublished course run with a seat.
unpublished_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Unpublished
,
course
=
self
.
course
)
unpublished_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Unpublished
)
SeatFactory
(
course_run
=
unpublished_course_run
)
SeatFactory
(
course_run
=
unpublished_course_run
)
# Published course run with no seats.
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
course
=
self
.
course
)
no_seats_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
)
# Published course run with a seat and an end date in the past.
closed_course_run
=
CourseRunFactory
(
closed_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
status
=
CourseRunStatus
.
Published
,
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
-
datetime
.
timedelta
(
days
=
10
),
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
-
datetime
.
timedelta
(
days
=
10
),
course
=
self
.
course
)
)
SeatFactory
(
course_run
=
closed_course_run
)
SeatFactory
(
course_run
=
closed_course_run
)
self
.
course
.
course_runs
.
add
(
enrollable_course_run
,
unpublished_course_run
,
no_seats_course_run
,
closed_course_run
,
)
serializer
=
self
.
serializer_class
(
serializer
=
self
.
serializer_class
(
self
.
course
,
self
.
course
,
context
=
{
'request'
:
self
.
request
,
'marketable_course_runs_only'
:
marketable_course_runs_only
}
context
=
{
'request'
:
self
.
request
,
'marketable_course_runs_only'
:
marketable_course_runs_only
}
...
@@ -247,6 +238,46 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
...
@@ -247,6 +238,46 @@ class CourseWithProgramsSerializerTests(CourseSerializerTests):
1
if
marketable_course_runs_only
else
4
1
if
marketable_course_runs_only
else
4
)
)
def
test_marketable_enrollable_course_runs_with_archived
(
self
):
"""
Verify that the marketable_enrollable_course_runs_with_archived option is respected, restricting returned
course runs to those that are published, have seats, and can still be enrolled in
(including courses with an end date in the past.)
"""
enrollable_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
10
),
enrollment_start
=
None
,
enrollment_end
=
None
,
course
=
self
.
course
)
unpublished_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Unpublished
,
course
=
self
.
course
)
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
course
=
self
.
course
)
archived_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
-
datetime
.
timedelta
(
days
=
10
),
enrollment_start
=
None
,
enrollment_end
=
None
,
course
=
self
.
course
)
SeatFactory
(
course_run
=
unpublished_course_run
)
SeatFactory
(
course_run
=
enrollable_course_run
)
SeatFactory
(
course_run
=
archived_course_run
)
context
=
{
'request'
:
self
.
request
,
'marketable_enrollable_course_runs_with_archived'
:
1
}
course_serializer
=
self
.
serializer_class
(
self
.
course
,
context
=
context
)
course_run_keys
=
[
course_run
[
'key'
]
for
course_run
in
course_serializer
.
data
[
'course_runs'
]]
# order doesn't matter
assert
sorted
(
course_run_keys
)
==
sorted
([
enrollable_course_run
.
key
,
archived_course_run
.
key
])
@ddt.data
(
0
,
1
)
@ddt.data
(
0
,
1
)
def
test_published_course_runs_only
(
self
,
published_course_runs_only
):
def
test_published_course_runs_only
(
self
,
published_course_runs_only
):
"""
"""
...
...
course_discovery/apps/api/v1/tests/test_views/test_courses.py
View file @
cf05b1f4
...
@@ -71,32 +71,27 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
...
@@ -71,32 +71,27 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
10
),
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
10
),
enrollment_start
=
None
,
enrollment_start
=
None
,
enrollment_end
=
None
,
enrollment_end
=
None
,
course
=
self
.
course
)
)
SeatFactory
(
course_run
=
enrollable_course_run
)
SeatFactory
(
course_run
=
enrollable_course_run
)
# Unpublished course run with a seat.
# Unpublished course run with a seat.
unpublished_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Unpublished
)
unpublished_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Unpublished
,
course
=
self
.
course
)
SeatFactory
(
course_run
=
unpublished_course_run
)
SeatFactory
(
course_run
=
unpublished_course_run
)
# Published course run with no seats.
# Published course run with no seats.
no_seats_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
)
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
course
=
self
.
course
)
# Published course run with a seat and an end date in the past.
# Published course run with a seat and an end date in the past.
closed_course_run
=
CourseRunFactory
(
closed_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
status
=
CourseRunStatus
.
Published
,
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
-
datetime
.
timedelta
(
days
=
10
),
end
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
-
datetime
.
timedelta
(
days
=
10
),
course
=
self
.
course
)
)
SeatFactory
(
course_run
=
closed_course_run
)
SeatFactory
(
course_run
=
closed_course_run
)
self
.
course
.
course_runs
.
add
(
enrollable_course_run
,
unpublished_course_run
,
no_seats_course_run
,
closed_course_run
,
)
url
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'key'
:
self
.
course
.
key
})
url
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'key'
:
self
.
course
.
key
})
url
+=
'?marketable_course_runs_only={}'
.
format
(
marketable_course_runs_only
)
url
=
'{}?marketable_course_runs_only={}'
.
format
(
url
,
marketable_course_runs_only
)
response
=
self
.
client
.
get
(
url
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
@@ -109,16 +104,45 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
...
@@ -109,16 +104,45 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
)
)
@ddt.data
(
1
,
0
)
@ddt.data
(
1
,
0
)
def
test_marketable_enrollable_course_runs_with_archived
(
self
,
marketable_enrollable_course_runs_with_archived
):
""" Verify the endpoint filters course runs to those that are marketable and
enrollable, including archived course runs (with an end date in the past). """
past
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
-
datetime
.
timedelta
(
days
=
2
)
future
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
2
)
CourseRunFactory
(
enrollment_start
=
None
,
enrollment_end
=
future
,
course
=
self
.
course
)
CourseRunFactory
(
enrollment_start
=
None
,
enrollment_end
=
None
,
course
=
self
.
course
)
CourseRunFactory
(
enrollment_start
=
past
,
enrollment_end
=
future
,
course
=
self
.
course
)
CourseRunFactory
(
enrollment_start
=
future
,
course
=
self
.
course
)
CourseRunFactory
(
enrollment_end
=
past
,
course
=
self
.
course
)
url
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'key'
:
self
.
course
.
key
})
url
=
'{}?marketable_enrollable_course_runs_with_archived={}'
.
format
(
url
,
marketable_enrollable_course_runs_with_archived
)
response
=
self
.
client
.
get
(
url
)
assert
response
.
status_code
==
200
assert
response
.
data
==
self
.
serialize_course
(
self
.
course
,
extra_context
=
{
'marketable_enrollable_course_runs_with_archived'
:
marketable_enrollable_course_runs_with_archived
}
)
@ddt.data
(
1
,
0
)
def
test_get_include_published_course_run
(
self
,
published_course_runs_only
):
def
test_get_include_published_course_run
(
self
,
published_course_runs_only
):
"""
"""
Verify the endpoint returns hides unpublished programs if
Verify the endpoint returns hides unpublished programs if
the 'published_course_runs_only' flag is set to True
the 'published_course_runs_only' flag is set to True
"""
"""
published_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
)
CourseRunFactory
(
status
=
CourseRunStatus
.
Published
,
course
=
self
.
course
)
unpublished_course_run
=
CourseRunFactory
(
status
=
CourseRunStatus
.
Unpublished
)
CourseRunFactory
(
status
=
CourseRunStatus
.
Unpublished
,
course
=
self
.
course
)
self
.
course
.
course_runs
.
add
(
published_course_run
,
unpublished_course_run
)
url
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'key'
:
self
.
course
.
key
})
url
=
reverse
(
'api:v1:course-detail'
,
kwargs
=
{
'key'
:
self
.
course
.
key
})
url
+=
'?published_course_runs_only={}'
.
format
(
published_course_runs_only
)
url
=
'{}?published_course_runs_only={}'
.
format
(
url
,
published_course_runs_only
)
response
=
self
.
client
.
get
(
url
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
self
.
assertEqual
(
...
...
course_discovery/apps/api/v1/views/courses.py
View file @
cf05b1f4
...
@@ -38,12 +38,10 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
...
@@ -38,12 +38,10 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
def
get_serializer_context
(
self
,
*
args
,
**
kwargs
):
def
get_serializer_context
(
self
,
*
args
,
**
kwargs
):
context
=
super
()
.
get_serializer_context
(
*
args
,
**
kwargs
)
context
=
super
()
.
get_serializer_context
(
*
args
,
**
kwargs
)
context
.
update
({
query_params
=
[
'exclude_utm'
,
'include_deleted_programs'
,
'marketable_course_runs_only'
,
'exclude_utm'
:
get_query_param
(
self
.
request
,
'exclude_utm'
),
'marketable_enrollable_course_runs_with_archived'
,
'published_course_runs_only'
]
'include_deleted_programs'
:
get_query_param
(
self
.
request
,
'include_deleted_programs'
),
for
query_param
in
query_params
:
'marketable_course_runs_only'
:
get_query_param
(
self
.
request
,
'marketable_course_runs_only'
),
context
[
query_param
]
=
get_query_param
(
self
.
request
,
query_param
)
'published_course_runs_only'
:
get_query_param
(
self
.
request
,
'published_course_runs_only'
),
})
return
context
return
context
...
@@ -71,7 +69,14 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
...
@@ -71,7 +69,14 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
multiple: false
multiple: false
- name: marketable_course_runs_only
- name: marketable_course_runs_only
description: Restrict returned course runs to those that are published, have seats,
description: Restrict returned course runs to those that are published, have seats,
and can still be enrolled in.
and are enrollable or will be enrollable in the future
required: false
type: integer
paramType: query
mulitple: false
- name: marketable_enrollable_course_runs_with_archived
description: Restrict returned course runs to those that are published, have seats,
and can be enrolled in now. Includes archived courses.
required: false
required: false
type: integer
type: integer
paramType: query
paramType: query
...
...
course_discovery/apps/course_metadata/query.py
View file @
cf05b1f4
...
@@ -43,6 +43,27 @@ class CourseRunQuerySet(models.QuerySet):
...
@@ -43,6 +43,27 @@ class CourseRunQuerySet(models.QuerySet):
)
)
)
)
def
enrollable
(
self
):
""" Returns course runs that are currently open for enrollment.
A course run is considered open for enrollment if its enrollment start date
has passed, is now or is None, AND its enrollment end date is in the future or is None.
Returns:
QuerySet
"""
now
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
return
self
.
filter
(
(
Q
(
enrollment_end__gt
=
now
)
|
Q
(
enrollment_end__isnull
=
True
)
)
&
(
Q
(
enrollment_start__lte
=
now
)
|
Q
(
enrollment_start__isnull
=
True
)
)
)
def
marketable
(
self
):
def
marketable
(
self
):
""" Returns CourseRuns that can be marketed to learners.
""" Returns CourseRuns that can be marketed to learners.
...
...
course_discovery/apps/course_metadata/tests/test_query.py
View file @
cf05b1f4
...
@@ -68,6 +68,22 @@ class CourseRunQuerySetTests(TestCase):
...
@@ -68,6 +68,22 @@ class CourseRunQuerySetTests(TestCase):
self
.
assertEqual
(
set
(
CourseRun
.
objects
.
active
()),
{
active_enrollment_end
,
active_no_enrollment_end
})
self
.
assertEqual
(
set
(
CourseRun
.
objects
.
active
()),
{
active_enrollment_end
,
active_no_enrollment_end
})
def
test_enrollable
(
self
):
""" Verify the method returns only course runs currently open for enrollment. """
past
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
-
datetime
.
timedelta
(
days
=
2
)
future
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
2
)
enrollable
=
CourseRunFactory
(
enrollment_start
=
past
,
enrollment_end
=
future
)
enrollable_no_enrollment_end
=
CourseRunFactory
(
enrollment_start
=
past
,
enrollment_end
=
None
)
enrollable_no_enrollment_start
=
CourseRunFactory
(
enrollment_start
=
None
,
enrollment_end
=
future
)
CourseRunFactory
(
enrollment_start
=
future
)
CourseRunFactory
(
enrollment_end
=
past
)
# order doesn't matter
assert
list
(
CourseRun
.
objects
.
enrollable
()
.
order_by
(
'id'
))
==
sorted
([
enrollable
,
enrollable_no_enrollment_end
,
enrollable_no_enrollment_start
],
key
=
lambda
x
:
x
.
id
)
def
test_marketable
(
self
):
def
test_marketable
(
self
):
""" Verify the method filters CourseRuns to those with slugs. """
""" Verify the method filters CourseRuns to those with slugs. """
course_run
=
CourseRunFactory
()
course_run
=
CourseRunFactory
()
...
...
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