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
a8f3a501
Commit
a8f3a501
authored
Jan 24, 2017
by
Anthony Mangano
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add boosting for courses with enrollable paid seats
ECOM-6715
parent
a0f3439b
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
276 additions
and
43 deletions
+276
-43
course_discovery/apps/api/v1/tests/test_views/test_search.py
+81
-42
course_discovery/apps/course_metadata/models.py
+45
-0
course_discovery/apps/course_metadata/search_indexes.py
+8
-0
course_discovery/apps/course_metadata/tests/test_models.py
+78
-1
course_discovery/apps/edx_haystack_extensions/migrations/0004_auto_20170126_1510.py
+64
-0
No files found.
course_discovery/apps/api/v1/tests/test_views/test_search.py
View file @
a8f3a501
import
datetime
import
json
import
urllib.parse
from
mock
import
patch
import
ddt
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
haystack.query
import
SearchQuerySet
import
pytz
from
rest_framework.test
import
APITestCase
from
course_discovery.apps.api.serializers
import
(
...
...
@@ -364,7 +367,6 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin
self
.
assertEqual
(
response
.
data
[
'objects'
][
'results'
],
expected
)
@ddt.ddt
class
TypeaheadSearchViewTests
(
DefaultPartnerMixin
,
TypeaheadSerializationMixin
,
LoginMixin
,
ElasticsearchTestMixin
,
APITestCase
):
path
=
reverse
(
'api:v1:search-typeahead'
)
...
...
@@ -459,47 +461,6 @@ class TypeaheadSearchViewTests(DefaultPartnerMixin, TypeaheadSerializationMixin,
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertEqual
(
response
.
data
,
[
"The 'q' querystring parameter is required for searching."
])
@ddt.data
(
'MicroMasters'
,
'Professional Certificate'
)
def
test_program_type_boosting
(
self
,
program_type
):
""" Verify MicroMasters and Professional Certificate are boosted over XSeries."""
title
=
program_type
ProgramFactory
(
title
=
title
+
"1"
,
status
=
ProgramStatus
.
Active
,
type
=
ProgramType
.
objects
.
get
(
name
=
'XSeries'
),
partner
=
self
.
partner
)
ProgramFactory
(
title
=
title
+
"2"
,
status
=
ProgramStatus
.
Active
,
type
=
ProgramType
.
objects
.
get
(
name
=
program_type
),
partner
=
self
.
partner
)
response
=
self
.
get_typeahead_response
(
title
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
self
.
assertEqual
(
response_data
[
'programs'
][
0
][
'type'
],
program_type
)
self
.
assertEqual
(
response_data
[
'programs'
][
0
][
'title'
],
title
+
"2"
)
def
test_start_date_boosting
(
self
):
""" Verify upcoming courses are boosted over past courses."""
title
=
"start"
now
=
datetime
.
datetime
.
utcnow
()
CourseRunFactory
(
title
=
title
+
"1"
,
start
=
now
-
datetime
.
timedelta
(
weeks
=
10
),
course__partner
=
self
.
partner
)
CourseRunFactory
(
title
=
title
+
"2"
,
start
=
now
+
datetime
.
timedelta
(
weeks
=
1
),
course__partner
=
self
.
partner
)
response
=
self
.
get_typeahead_response
(
title
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
self
.
assertEqual
(
response_data
[
'course_runs'
][
0
][
'title'
],
title
+
"2"
)
def
test_self_paced_boosting
(
self
):
""" Verify that self paced courses are boosted over instructor led courses."""
title
=
"paced"
CourseRunFactory
(
title
=
title
+
"1"
,
pacing_type
=
'instructor_paced'
,
course__partner
=
self
.
partner
)
CourseRunFactory
(
title
=
title
+
"2"
,
pacing_type
=
'self_paced'
,
course__partner
=
self
.
partner
)
response
=
self
.
get_typeahead_response
(
title
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response_data
=
response
.
json
()
self
.
assertEqual
(
response_data
[
'course_runs'
][
0
][
'title'
],
title
+
"2"
)
def
test_typeahead_authoring_organizations_partial_search
(
self
):
""" Test typeahead response with partial organization matching. """
authoring_organizations
=
OrganizationFactory
.
create_batch
(
3
)
...
...
@@ -568,3 +529,81 @@ class TypeaheadSearchViewTests(DefaultPartnerMixin, TypeaheadSerializationMixin,
edx_program
=
programs
[
0
]
self
.
assertDictEqual
(
response
.
data
,
{
'course_runs'
:
[
self
.
serialize_course_run
(
edx_course_run
)],
'programs'
:
[
self
.
serialize_program
(
edx_program
)]})
@ddt.ddt
class
SearchBoostingTests
(
ElasticsearchTestMixin
,
TestCase
):
def
build_normalized_course_run
(
self
,
**
kwargs
):
""" Builds a CourseRun with fields set to normalize boosting behavior."""
defaults
=
{
'pacing_type'
:
'instructor_paced'
,
'start'
:
datetime
.
datetime
.
now
(
pytz
.
timezone
(
'utc'
))
+
datetime
.
timedelta
(
weeks
=
52
),
}
defaults
.
update
(
kwargs
)
return
CourseRunFactory
(
**
defaults
)
def
test_start_date_boosting
(
self
):
""" Verify upcoming courses are boosted over past courses."""
now
=
datetime
.
datetime
.
now
(
pytz
.
timezone
(
'utc'
))
self
.
build_normalized_course_run
(
start
=
now
+
datetime
.
timedelta
(
weeks
=
10
))
test_record
=
self
.
build_normalized_course_run
(
start
=
now
+
datetime
.
timedelta
(
weeks
=
1
))
search_results
=
SearchQuerySet
()
.
models
(
CourseRun
)
.
all
()
self
.
assertEqual
(
2
,
len
(
search_results
))
self
.
assertGreater
(
search_results
[
0
]
.
score
,
search_results
[
1
]
.
score
)
self
.
assertEqual
(
int
(
test_record
.
start
.
timestamp
()),
int
(
search_results
[
0
]
.
start
.
timestamp
()))
# pylint: disable=no-member
def
test_self_paced_boosting
(
self
):
""" Verify that self paced courses are boosted over instructor led courses."""
self
.
build_normalized_course_run
(
pacing_type
=
'instructor_paced'
)
test_record
=
self
.
build_normalized_course_run
(
pacing_type
=
'self_paced'
)
search_results
=
SearchQuerySet
()
.
models
(
CourseRun
)
.
all
()
self
.
assertEqual
(
2
,
len
(
search_results
))
self
.
assertGreater
(
search_results
[
0
]
.
score
,
search_results
[
1
]
.
score
)
self
.
assertEqual
(
test_record
.
pacing_type
,
search_results
[
0
]
.
pacing_type
)
@ddt.data
(
# Case 1: Should not get boost if has_enrollable_paid_seats is False, has_enrollable_paid_seats is None or
# paid_seat_enrollment_end is in the past.
(
False
,
None
,
False
),
(
None
,
None
,
False
),
(
True
,
datetime
.
datetime
.
now
(
pytz
.
timezone
(
'utc'
))
-
datetime
.
timedelta
(
days
=
15
),
False
),
# Case 2: Should get boost if has_enrollable_paid_seats is True and paid_seat_enrollment_end is None or
# in the future.
(
True
,
None
,
True
),
(
True
,
datetime
.
datetime
.
now
(
pytz
.
timezone
(
'utc'
))
+
datetime
.
timedelta
(
days
=
15
),
True
)
)
@ddt.unpack
def
test_enrollable_paid_seat_boosting
(
self
,
has_enrollable_paid_seats
,
paid_seat_enrollment_end
,
expects_boost
):
""" Verify that CourseRuns for which an unenrolled user may enroll and purchase a paid Seat are boosted."""
# Create a control record (one that should never be boosted).
with
patch
.
object
(
CourseRun
,
'has_enrollable_paid_seats'
,
return_value
=
False
):
with
patch
.
object
(
CourseRun
,
'get_paid_seat_enrollment_end'
,
return_value
=
None
):
self
.
build_normalized_course_run
(
title
=
'test1'
)
# Create the test record (may be boosted).
with
patch
.
object
(
CourseRun
,
'has_enrollable_paid_seats'
,
return_value
=
has_enrollable_paid_seats
):
with
patch
.
object
(
CourseRun
,
'get_paid_seat_enrollment_end'
,
return_value
=
paid_seat_enrollment_end
):
test_record
=
self
.
build_normalized_course_run
(
title
=
'test2'
)
search_results
=
SearchQuerySet
()
.
models
(
CourseRun
)
.
all
()
self
.
assertEqual
(
2
,
len
(
search_results
))
if
expects_boost
:
self
.
assertGreater
(
search_results
[
0
]
.
score
,
search_results
[
1
]
.
score
)
self
.
assertEqual
(
test_record
.
title
,
search_results
[
0
]
.
title
)
else
:
self
.
assertEqual
(
search_results
[
0
]
.
score
,
search_results
[
1
]
.
score
)
@ddt.data
(
'MicroMasters'
,
'Professional Certificate'
)
def
test_program_type_boosting
(
self
,
program_type
):
""" Verify MicroMasters and Professional Certificate are boosted over XSeries."""
ProgramFactory
(
type
=
ProgramType
.
objects
.
get
(
name
=
'XSeries'
))
test_record
=
ProgramFactory
(
type
=
ProgramType
.
objects
.
get
(
name
=
program_type
))
search_results
=
SearchQuerySet
()
.
models
(
Program
)
.
all
()
self
.
assertEqual
(
2
,
len
(
search_results
))
self
.
assertGreater
(
search_results
[
0
]
.
score
,
search_results
[
1
]
.
score
)
self
.
assertEqual
(
str
(
test_record
.
type
),
str
(
search_results
[
0
]
.
type
))
course_discovery/apps/course_metadata/models.py
View file @
a8f3a501
...
...
@@ -381,6 +381,47 @@ class CourseRun(TimeStampedModel):
objects
=
CourseRunQuerySet
.
as_manager
()
def
_enrollable_paid_seats
(
self
):
"""
Return a QuerySet that may be used to fetch the enrollable paid Seats (Seats with price > 0 and no
prerequisites) associated with this CourseRun.
"""
return
self
.
seats
.
exclude
(
type__in
=
Seat
.
SEATS_WITH_PREREQUISITES
)
.
filter
(
price__gt
=
0.0
)
def
has_enrollable_paid_seats
(
self
):
"""
Return a boolean indicating whether or not enrollable paid Seats (Seats with price > 0 and no prerequisites)
are available for this CourseRun.
"""
return
len
(
self
.
_enrollable_paid_seats
()[:
1
])
>
0
def
get_paid_seat_enrollment_end
(
self
):
"""
Return the final date for which an unenrolled user may enroll and purchase a paid Seat for this CourseRun, or
None if the date is unknown or enrollable paid Seats are not available.
"""
seats
=
list
(
self
.
_enrollable_paid_seats
()
.
order_by
(
'-upgrade_deadline'
))
if
len
(
seats
)
==
0
:
# Enrollable paid seats are not available for this CourseRun.
return
None
# An unenrolled user may not enroll and purchase paid seats after the course has ended.
deadline
=
self
.
end
# An unenrolled user may not enroll and purchase paid seats after enrollment has ended.
if
self
.
enrollment_end
and
(
deadline
is
None
or
self
.
enrollment_end
<
deadline
):
deadline
=
self
.
enrollment_end
# Note that even though we're sorting in descending order by upgrade_deadline, we will need to look at
# both the first and last record in the result set to determine which Seat has the latest upgrade_deadline.
# We consider Null values to be > than non-Null values, and Null values may sort to the top or bottom of
# the result set, depending on the DB backend.
latest_seat
=
seats
[
-
1
]
if
seats
[
-
1
]
.
upgrade_deadline
is
None
else
seats
[
0
]
if
latest_seat
.
upgrade_deadline
and
(
deadline
is
None
or
latest_seat
.
upgrade_deadline
<
deadline
):
deadline
=
latest_seat
.
upgrade_deadline
return
deadline
@property
def
program_types
(
self
):
"""
...
...
@@ -525,6 +566,10 @@ class Seat(TimeStampedModel):
PROFESSIONAL
=
'professional'
CREDIT
=
'credit'
# Seat types that may not be purchased without first purchasing another Seat type.
# EX: 'credit' seats may not be purchased without first purchasing a 'verified' Seat.
SEATS_WITH_PREREQUISITES
=
[
CREDIT
]
SEAT_TYPE_CHOICES
=
(
(
HONOR
,
_
(
'Honor'
)),
(
AUDIT
,
_
(
'Audit'
)),
...
...
course_discovery/apps/course_metadata/search_indexes.py
View file @
a8f3a501
...
...
@@ -150,6 +150,14 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
authoring_organization_uuids
=
indexes
.
MultiValueField
()
staff_uuids
=
indexes
.
MultiValueField
()
subject_uuids
=
indexes
.
MultiValueField
()
has_enrollable_paid_seats
=
indexes
.
BooleanField
(
null
=
False
)
paid_seat_enrollment_end
=
indexes
.
DateTimeField
(
null
=
True
)
def
prepare_has_enrollable_paid_seats
(
self
,
obj
):
return
obj
.
has_enrollable_paid_seats
()
def
prepare_paid_seat_enrollment_end
(
self
,
obj
):
return
obj
.
get_paid_seat_enrollment_end
()
def
prepare_partner
(
self
,
obj
):
return
obj
.
course
.
partner
.
short_code
...
...
course_discovery/apps/course_metadata/tests/test_models.py
View file @
a8f3a501
...
...
@@ -19,7 +19,7 @@ from course_discovery.apps.core.utils import SearchQuerySetWrapper
from
course_discovery.apps.course_metadata.choices
import
ProgramStatus
from
course_discovery.apps.course_metadata.models
import
(
AbstractMediaModel
,
AbstractNamedModel
,
AbstractValueModel
,
CorporateEndorsement
,
Course
,
CourseRun
,
Endorsement
,
FAQ
,
SeatType
,
ProgramType
,
CorporateEndorsement
,
Course
,
CourseRun
,
Endorsement
,
FAQ
,
Seat
,
Seat
Type
,
ProgramType
,
)
from
course_discovery.apps.course_metadata.publishers
import
MarketingSitePublisher
from
course_discovery.apps.course_metadata.tests
import
factories
,
toggle_switch
...
...
@@ -186,6 +186,83 @@ class CourseRunTests(TestCase):
factories
.
ProgramFactory
(
courses
=
[
self
.
course_run
.
course
],
status
=
ProgramStatus
.
Deleted
)
self
.
assertEqual
(
self
.
course_run
.
program_types
,
[
active_program
.
type
.
name
])
@ddt.data
(
# Case 1: Return False when there are no paid Seats.
([(
'audit'
,
0
)],
False
),
([(
'audit'
,
0
),
(
'verified'
,
0
)],
False
),
# Case 2: Return False when there are no paid Seats without prerequisites.
([(
seat_type
,
1
)
for
seat_type
in
Seat
.
SEATS_WITH_PREREQUISITES
],
False
),
# Case 3: Return True when there is at least one paid Seat without prerequisites.
([(
'audit'
,
0
),
(
'verified'
,
1
)],
True
),
([(
'audit'
,
0
),
(
'verified'
,
1
),
(
'professional'
,
1
)],
True
),
([(
'audit'
,
0
),
(
'verified'
,
1
)]
+
[(
seat_type
,
1
)
for
seat_type
in
Seat
.
SEATS_WITH_PREREQUISITES
],
True
),
)
@ddt.unpack
def
test_has_enrollable_paid_seats
(
self
,
seat_config
,
expected_result
):
"""
Verify that has_enrollable_paid_seats is True when CourseRun has Seats with price > 0 and no prerequisites.
"""
course_run
=
factories
.
CourseRunFactory
.
create
()
for
seat_type
,
price
in
seat_config
:
factories
.
SeatFactory
.
create
(
course_run
=
course_run
,
type
=
seat_type
,
price
=
price
)
self
.
assertEqual
(
course_run
.
has_enrollable_paid_seats
(),
expected_result
)
@ddt.data
(
# Case 1: Return None when there are no enrollable paid Seats.
([(
'audit'
,
0
,
None
)],
'2016-12-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
,
None
),
([(
seat_type
,
1
,
None
)
for
seat_type
in
Seat
.
SEATS_WITH_PREREQUISITES
],
'2016-12-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
,
None
),
# Case 2: Return the latest upgrade_deadline of the enrollable paid Seats when it's earlier than
# enrollment_end and course end.
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
)],
'2016-12-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
,
'2016-07-30 00:00:00Z'
),
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
),
(
'professional'
,
1
,
'2016-08-15 00:00:00Z'
)],
'2016-12-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
,
'2016-08-15 00:00:00Z'
),
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
)]
+
[(
seat_type
,
1
,
'2016-08-15 00:00:00Z'
)
for
seat_type
in
Seat
.
SEATS_WITH_PREREQUISITES
],
'2016-12-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
,
'2016-07-30 00:00:00Z'
),
# Case 3: Return enrollment_end when it's earlier than course end and the latest upgrade_deadline of the
# enrollable paid Seats, or when one of those Seats does not have an upgrade_deadline.
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
),
(
'professional'
,
1
,
'2016-09-15 00:00:00Z'
)],
'2016-12-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
),
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
),
(
'professional'
,
1
,
None
)],
'2016-12-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
,
'2016-08-31 00:00:00Z'
),
# Case 4: Return course end when it's earlier than enrollment_end or enrollment_end is None, and it's earlier
# than the latest upgrade_deadline of the enrollable paid Seats or when one of those Seats does not have an
# upgrade_deadline.
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
),
(
'professional'
,
1
,
'2017-09-15 00:00:00Z'
)],
'2016-12-31 00:00:00Z'
,
'2017-08-31 00:00:00Z'
,
'2016-12-31 00:00:00Z'
),
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
),
(
'professional'
,
1
,
None
)],
'2016-12-31 00:00:00Z'
,
'2017-12-31 00:00:00Z'
,
'2016-12-31 00:00:00Z'
),
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
),
(
'professional'
,
1
,
None
)],
'2016-12-31 00:00:00Z'
,
None
,
'2016-12-31 00:00:00Z'
),
# Case 5: Return None when course end and enrollment_end are None and there's an enrollable paid Seat without
# an upgrade_deadline, even when there's another enrollable paid Seat with an upgrade_deadline.
([(
'audit'
,
0
,
None
),
(
'verified'
,
1
,
'2016-07-30 00:00:00Z'
),
(
'professional'
,
1
,
None
)],
None
,
None
,
None
)
)
@ddt.unpack
def
test_get_paid_seat_enrollment_end
(
self
,
seat_config
,
course_end
,
course_enrollment_end
,
expected_result
):
"""
Verify that paid_seat_enrollment_end returns the latest possible date for which an unerolled user may
enroll and purchase an upgrade for the CourseRun or None if date unknown or paid Seats are not available.
"""
end
=
parse
(
course_end
)
if
course_end
else
None
enrollment_end
=
parse
(
course_enrollment_end
)
if
course_enrollment_end
else
None
course_run
=
factories
.
CourseRunFactory
.
create
(
end
=
end
,
enrollment_end
=
enrollment_end
)
for
seat_type
,
price
,
deadline
in
seat_config
:
deadline
=
parse
(
deadline
)
if
deadline
else
None
factories
.
SeatFactory
.
create
(
course_run
=
course_run
,
type
=
seat_type
,
price
=
price
,
upgrade_deadline
=
deadline
)
expected_result
=
parse
(
expected_result
)
if
expected_result
else
None
self
.
assertEqual
(
course_run
.
get_paid_seat_enrollment_end
(),
expected_result
)
class
OrganizationTests
(
TestCase
):
""" Tests for the `Organization` model. """
...
...
course_discovery/apps/edx_haystack_extensions/migrations/0004_auto_20170126_1510.py
0 → 100644
View file @
a8f3a501
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-01-26 15:10
from
__future__
import
unicode_literals
from
django.db
import
migrations
def
add_enrollable_paid_seat_boosting
(
apps
,
schema_editor
):
"""Add enrollable paid Seat boosting function to ElasticsearchBoostConfig instance."""
# Get the model from the versioned app registry to ensure the correct version is used, as described in
# https://docs.djangoproject.com/en/1.8/ref/migration-operations/#runpython
ElasticsearchBoostConfig
=
apps
.
get_model
(
'edx_haystack_extensions'
,
'ElasticsearchBoostConfig'
)
ElasticsearchBoostConfig
.
objects
.
update_or_create
(
# The `solo` library uses 1 for the PrimaryKey to create/lookup the singleton record
# See https://github.com/lazybird/django-solo/blob/1.1.2/solo/models.py
pk
=
1
,
defaults
=
{
'function_score'
:
{
'boost_mode'
:
'sum'
,
'boost'
:
1.0
,
'score_mode'
:
'sum'
,
'functions'
:
[
{
'filter'
:
{
'term'
:
{
'pacing_type_exact'
:
'self_paced'
}},
'weight'
:
1.0
},
{
'filter'
:
{
'term'
:
{
'type_exact'
:
'Professional Certificate'
}},
'weight'
:
1.0
},
{
'filter'
:
{
'term'
:
{
'type_exact'
:
'MicroMasters'
}},
'weight'
:
1.0
},
{
'linear'
:
{
'start'
:
{
'origin'
:
'now'
,
'decay'
:
0.95
,
'scale'
:
'1d'
}},
'weight'
:
5.0
},
# Boost function for CourseRuns with enrollable paid Seats.
# We want to boost if:
# - The course run has at least one enrollable paid Seat (has_enrollable_paid_seats is True)
# AND one of the following two conditions are true
# - The paid_seat_enrollment_end is unspecified.
# - The paid_seat_enrollment_end is in the future.
# We apply a weight of 1.0 to match the boost given for self paced courses.
{
'filter'
:
{
'bool'
:
{
'must'
:
[
{
'exists'
:
{
'field'
:
'has_enrollable_paid_seats'
}},
{
'term'
:
{
'has_enrollable_paid_seats'
:
True
}}
],
'should'
:
[
{
'bool'
:
{
'must_not'
:
{
'exists'
:
{
'field'
:
'paid_seat_enrollment_end'
}}}},
{
'range'
:
{
'paid_seat_enrollment_end'
:
{
'gte'
:
'now'
}}}
]
}
},
'weight'
:
1.0
}
]
}
}
)
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'edx_haystack_extensions'
,
'0003_auto_20170124_1834'
),
]
operations
=
[
migrations
.
RunPython
(
add_enrollable_paid_seat_boosting
,
migrations
.
RunPython
.
noop
)
]
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