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
8e084214
Commit
8e084214
authored
Sep 13, 2016
by
Renzo Lucioni
Committed by
GitHub
Sep 13, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #317 from edx/renzo/program-marketability
Marketable programs must be active
parents
70ea8c41
2dd8cf68
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
137 additions
and
96 deletions
+137
-96
course_discovery/apps/api/tests/test_serializers.py
+3
-2
course_discovery/apps/api/v1/tests/test_views/test_programs.py
+13
-3
course_discovery/apps/api/v1/tests/test_views/test_search.py
+14
-13
course_discovery/apps/api/v1/views.py
+2
-1
course_discovery/apps/course_metadata/choices.py
+22
-0
course_discovery/apps/course_metadata/data_loaders/api.py
+4
-3
course_discovery/apps/course_metadata/data_loaders/marketing_site.py
+4
-3
course_discovery/apps/course_metadata/data_loaders/tests/test_api.py
+6
-5
course_discovery/apps/course_metadata/data_loaders/tests/test_marketing_site.py
+8
-8
course_discovery/apps/course_metadata/forms.py
+2
-1
course_discovery/apps/course_metadata/models.py
+7
-24
course_discovery/apps/course_metadata/query.py
+10
-2
course_discovery/apps/course_metadata/search_indexes.py
+3
-2
course_discovery/apps/course_metadata/tests/factories.py
+4
-3
course_discovery/apps/course_metadata/tests/test_admin.py
+11
-11
course_discovery/apps/course_metadata/tests/test_models.py
+4
-4
course_discovery/apps/course_metadata/tests/test_publishers.py
+3
-2
course_discovery/apps/course_metadata/tests/test_query.py
+12
-4
course_discovery/apps/publisher/models.py
+3
-3
course_discovery/apps/publisher/tests/factories.py
+2
-2
No files found.
course_discovery/apps/api/tests/test_serializers.py
View file @
8e084214
...
...
@@ -20,6 +20,7 @@ from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from
course_discovery.apps.core.models
import
User
from
course_discovery.apps.core.tests.factories
import
UserFactory
from
course_discovery.apps.core.tests.helpers
import
make_image_file
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
,
SubjectFactory
,
PrerequisiteFactory
,
ImageFactory
,
VideoFactory
,
...
...
@@ -715,7 +716,7 @@ class CourseRunSearchSerializerTests(TestCase):
'type'
:
course_run
.
type
,
'level_type'
:
course_run
.
level_type
.
name
,
'availability'
:
course_run
.
availability
,
'published'
:
course_run
.
status
==
CourseRun
.
Status
.
Published
,
'published'
:
course_run
.
status
==
CourseRunStatus
.
Published
,
'partner'
:
course_run
.
course
.
partner
.
short_code
,
'program_types'
:
course_run
.
program_types
,
'authoring_organization_uuids'
:
get_uuids
(
course_run
.
authoring_organizations
.
all
()),
...
...
@@ -749,7 +750,7 @@ class ProgramSearchSerializerTests(TestCase):
'content_type'
:
'program'
,
'card_image_url'
:
program
.
card_image_url
,
'status'
:
program
.
status
,
'published'
:
program
.
status
==
Program
.
Status
.
Active
,
'published'
:
program
.
status
==
ProgramStatus
.
Active
,
'partner'
:
program
.
partner
.
short_code
,
'authoring_organization_uuids'
:
get_uuids
(
program
.
authoring_organizations
.
all
()),
'subject_uuids'
:
get_uuids
([
course
.
subjects
for
course
in
program
.
courses
.
all
()]),
...
...
course_discovery/apps/api/v1/tests/test_views/test_programs.py
View file @
8e084214
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.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
from
course_discovery.apps.course_metadata.tests.factories
import
ProgramFactory
@ddt.ddt
class
ProgramViewSetTests
(
APITestCase
):
list_path
=
reverse
(
'api:v1:program-list'
)
...
...
@@ -83,11 +86,18 @@ class ProgramViewSetTests(APITestCase):
self
.
assert_list_results
(
url
,
expected
)
def
test_filter_by_marketable
(
self
):
@ddt.data
(
(
ProgramStatus
.
Unpublished
,
False
),
(
ProgramStatus
.
Active
,
True
),
)
@ddt.unpack
def
test_filter_by_marketable
(
self
,
status
,
is_marketable
):
""" Verify the endpoint filters programs to those that are marketable. """
url
=
self
.
list_path
+
'?marketable=1'
ProgramFactory
(
marketing_slug
=
''
)
expected
=
ProgramFactory
.
create_batch
(
3
)
expected
.
reverse
()
programs
=
ProgramFactory
.
create_batch
(
3
,
status
=
status
)
programs
.
reverse
()
expected
=
programs
if
is_marketable
else
[]
self
.
assertEqual
(
list
(
Program
.
objects
.
marketable
()),
expected
)
self
.
assert_list_results
(
url
,
expected
)
course_discovery/apps/api/v1/tests/test_views/test_search.py
View file @
8e084214
...
...
@@ -11,6 +11,7 @@ from rest_framework.test import APITestCase
from
course_discovery.apps.api.serializers
import
CourseRunSearchSerializer
,
ProgramSearchSerializer
from
course_discovery.apps.core.tests.factories
import
UserFactory
,
USER_PASSWORD
,
PartnerFactory
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
CourseRunFactory
,
ProgramFactory
...
...
@@ -82,7 +83,7 @@ class CourseRunSearchViewSetTests(DefaultPartnerMixin, SerializationMixin, Login
# Generate data that should be indexed and returned by the query
course_run
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
course__title
=
'Software Testing'
,
status
=
CourseRun
.
Status
.
Published
)
status
=
CourseRunStatus
.
Published
)
response
=
self
.
get_search_response
(
'software'
,
faceted
=
faceted
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
@@ -119,13 +120,13 @@ class CourseRunSearchViewSetTests(DefaultPartnerMixin, SerializationMixin, Login
""" Verify the endpoint returns availability facets with the results. """
now
=
datetime
.
datetime
.
utcnow
()
archived
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
start
=
now
-
datetime
.
timedelta
(
weeks
=
2
),
end
=
now
-
datetime
.
timedelta
(
weeks
=
1
),
status
=
CourseRun
.
Status
.
Published
)
end
=
now
-
datetime
.
timedelta
(
weeks
=
1
),
status
=
CourseRunStatus
.
Published
)
current
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
start
=
now
-
datetime
.
timedelta
(
weeks
=
2
),
end
=
now
+
datetime
.
timedelta
(
weeks
=
1
),
status
=
CourseRun
.
Status
.
Published
)
end
=
now
+
datetime
.
timedelta
(
weeks
=
1
),
status
=
CourseRunStatus
.
Published
)
starting_soon
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
start
=
now
+
datetime
.
timedelta
(
days
=
10
),
end
=
now
+
datetime
.
timedelta
(
days
=
90
),
status
=
CourseRun
.
Status
.
Published
)
end
=
now
+
datetime
.
timedelta
(
days
=
90
),
status
=
CourseRunStatus
.
Published
)
upcoming
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
start
=
now
+
datetime
.
timedelta
(
days
=
61
),
end
=
now
+
datetime
.
timedelta
(
days
=
90
),
status
=
CourseRun
.
Status
.
Published
)
end
=
now
+
datetime
.
timedelta
(
days
=
90
),
status
=
CourseRunStatus
.
Published
)
response
=
self
.
get_search_response
(
faceted
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
@@ -184,11 +185,11 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin
def
test_results_only_include_published_objects
(
self
):
""" Verify the search results only include items with status set to 'Published'. """
# These items should NOT be in the results
CourseRunFactory
(
course__partner
=
self
.
partner
,
status
=
CourseRun
.
Status
.
Unpublished
)
ProgramFactory
(
partner
=
self
.
partner
,
status
=
Program
.
Status
.
Unpublished
)
CourseRunFactory
(
course__partner
=
self
.
partner
,
status
=
CourseRunStatus
.
Unpublished
)
ProgramFactory
(
partner
=
self
.
partner
,
status
=
ProgramStatus
.
Unpublished
)
course_run
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
status
=
CourseRun
.
Status
.
Published
)
program
=
ProgramFactory
(
partner
=
self
.
partner
,
status
=
Program
.
Status
.
Active
)
course_run
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
status
=
CourseRunStatus
.
Published
)
program
=
ProgramFactory
(
partner
=
self
.
partner
,
status
=
ProgramStatus
.
Active
)
response
=
self
.
get_search_response
()
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
@@ -199,13 +200,13 @@ class AggregateSearchViewSet(DefaultPartnerMixin, SerializationMixin, LoginMixin
def
test_results_filtered_by_default_partner
(
self
):
""" Verify the search results only include items related to the default partner if no partner is
specified on the request. If a partner is included, the data should be filtered to the requested partner. """
course_run
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
status
=
CourseRun
.
Status
.
Published
)
program
=
ProgramFactory
(
partner
=
self
.
partner
,
status
=
Program
.
Status
.
Active
)
course_run
=
CourseRunFactory
(
course__partner
=
self
.
partner
,
status
=
CourseRunStatus
.
Published
)
program
=
ProgramFactory
(
partner
=
self
.
partner
,
status
=
ProgramStatus
.
Active
)
# This data should NOT be in the results
other_partner
=
PartnerFactory
()
other_course_run
=
CourseRunFactory
(
course__partner
=
other_partner
,
status
=
CourseRun
.
Status
.
Published
)
other_program
=
ProgramFactory
(
partner
=
other_partner
,
status
=
Program
.
Status
.
Active
)
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
)
...
...
course_discovery/apps/api/v1/views.py
View file @
8e084214
...
...
@@ -402,7 +402,8 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet):
paramType: query
multiple: false
- name: marketable
description: Retrieve marketable programs. A program is considered marketable if it has a marketing slug.
description: Retrieve marketable programs. A program is considered marketable if it is active
and has a marketing slug.
required: false
type: integer
paramType: query
...
...
course_discovery/apps/course_metadata/choices.py
0 → 100644
View file @
8e084214
from
django.utils.translation
import
ugettext_lazy
as
_
from
djchoices
import
DjangoChoices
,
ChoiceItem
class
CourseRunStatus
(
DjangoChoices
):
Published
=
ChoiceItem
(
'published'
,
_
(
'Published'
))
Unpublished
=
ChoiceItem
(
'unpublished'
,
_
(
'Unpublished'
))
class
CourseRunPacing
(
DjangoChoices
):
# Translators: Instructor-paced refers to course runs that operate on a schedule set by the instructor,
# similar to a normal university course.
Instructor
=
ChoiceItem
(
'instructor_paced'
,
_
(
'Instructor-paced'
))
# Translators: Self-paced refers to course runs that operate on the student's schedule.
Self
=
ChoiceItem
(
'self_paced'
,
_
(
'Self-paced'
))
class
ProgramStatus
(
DjangoChoices
):
Unpublished
=
ChoiceItem
(
'unpublished'
,
_
(
'Unpublished'
))
Active
=
ChoiceItem
(
'active'
,
_
(
'Active'
))
Retired
=
ChoiceItem
(
'retired'
,
_
(
'Retired'
))
Deleted
=
ChoiceItem
(
'deleted'
,
_
(
'Deleted'
))
course_discovery/apps/course_metadata/data_loaders/api.py
View file @
8e084214
...
...
@@ -9,6 +9,7 @@ from opaque_keys.edx.keys import CourseKey
import
requests
from
course_discovery.apps.core.models
import
Currency
from
course_discovery.apps.course_metadata.choices
import
CourseRunStatus
,
CourseRunPacing
from
course_discovery.apps.course_metadata.data_loaders
import
AbstractDataLoader
from
course_discovery.apps.course_metadata.models
import
(
Video
,
Organization
,
Seat
,
CourseRun
,
Program
,
Course
,
ProgramType
,
...
...
@@ -141,7 +142,7 @@ class CoursesApiDataLoader(AbstractDataLoader):
'title_override'
:
body
[
'name'
],
'short_description_override'
:
body
[
'short_description'
],
'video'
:
self
.
get_courserun_video
(
body
),
'status'
:
CourseRun
.
Status
.
Published
,
'status'
:
CourseRunStatus
.
Published
,
'pacing_type'
:
self
.
get_pacing_type
(
body
),
})
...
...
@@ -157,9 +158,9 @@ class CoursesApiDataLoader(AbstractDataLoader):
pacing
=
pacing
.
lower
()
if
pacing
==
'instructor'
:
return
CourseRun
.
Pacing
.
Instructor
return
CourseRunPacing
.
Instructor
elif
pacing
==
'self'
:
return
CourseRun
.
Pacing
.
Self
return
CourseRunPacing
.
Self
else
:
return
None
...
...
course_discovery/apps/course_metadata/data_loaders/marketing_site.py
View file @
8e084214
...
...
@@ -11,6 +11,7 @@ from django.db.models import Q
from
django.utils.functional
import
cached_property
from
opaque_keys.edx.keys
import
CourseKey
from
course_discovery.apps.course_metadata.choices
import
CourseRunStatus
,
CourseRunPacing
from
course_discovery.apps.course_metadata.data_loaders
import
AbstractDataLoader
from
course_discovery.apps.course_metadata.models
import
(
Course
,
Organization
,
Person
,
Subject
,
Program
,
Position
,
LevelType
,
CourseRun
...
...
@@ -382,7 +383,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
# If the course already exists update the fields only if the course_run we got from drupal is published.
# People often put temp data into required drupal fields for unpublished courses. We don't want to overwrite
# the course info with this data, so we only update course info from published sources.
published
=
self
.
get_course_run_status
(
data
)
==
CourseRun
.
Status
.
Published
published
=
self
.
get_course_run_status
(
data
)
==
CourseRunStatus
.
Published
if
not
created
and
published
:
for
attr
,
value
in
defaults
.
items
():
setattr
(
course
,
attr
,
value
)
...
...
@@ -403,7 +404,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
return
description
def
get_course_run_status
(
self
,
data
):
return
CourseRun
.
Status
.
Published
if
bool
(
int
(
data
[
'status'
]))
else
CourseRun
.
Status
.
Unpublished
return
CourseRun
Status
.
Published
if
bool
(
int
(
data
[
'status'
]))
else
CourseRun
Status
.
Unpublished
def
get_level_type
(
self
,
name
):
level_type
=
None
...
...
@@ -420,7 +421,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
def
get_pacing_type
(
self
,
data
):
self_paced
=
data
.
get
(
'field_course_self_paced'
,
False
)
return
CourseRun
.
Pacing
.
Self
if
self_paced
else
CourseRun
.
Pacing
.
Instructor
return
CourseRun
Pacing
.
Self
if
self_paced
else
CourseRun
Pacing
.
Instructor
def
create_course_run
(
self
,
course
,
data
):
uuid
=
data
[
'uuid'
]
...
...
course_discovery/apps/course_metadata/data_loaders/tests/test_api.py
View file @
8e084214
...
...
@@ -8,6 +8,7 @@ from django.test import TestCase
from
pytz
import
UTC
from
course_discovery.apps.core.tests.utils
import
mock_api_callback
,
mock_jpeg_callback
from
course_discovery.apps.course_metadata.choices
import
CourseRunStatus
,
CourseRunPacing
from
course_discovery.apps.course_metadata.data_loaders.api
import
(
OrganizationsApiDataLoader
,
CoursesApiDataLoader
,
EcommerceApiDataLoader
,
AbstractDataLoader
,
ProgramsApiDataLoader
)
...
...
@@ -157,7 +158,7 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
'title_override'
:
body
[
'name'
],
'short_description_override'
:
self
.
loader
.
clean_string
(
body
[
'short_description'
]),
'video'
:
self
.
loader
.
get_courserun_video
(
body
),
'status'
:
CourseRun
.
Status
.
Published
,
'status'
:
CourseRunStatus
.
Published
,
'pacing_type'
:
self
.
loader
.
get_pacing_type
(
body
),
})
...
...
@@ -215,10 +216,10 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
(
''
,
None
),
(
'foo'
,
None
),
(
None
,
None
),
(
'instructor'
,
CourseRun
.
Pacing
.
Instructor
),
(
'Instructor'
,
CourseRun
.
Pacing
.
Instructor
),
(
'self'
,
CourseRun
.
Pacing
.
Self
),
(
'Self'
,
CourseRun
.
Pacing
.
Self
),
(
'instructor'
,
CourseRunPacing
.
Instructor
),
(
'Instructor'
,
CourseRunPacing
.
Instructor
),
(
'self'
,
CourseRunPacing
.
Self
),
(
'Self'
,
CourseRunPacing
.
Self
),
)
def
test_get_pacing_type
(
self
,
pacing
,
expected_pacing_type
):
""" Verify the method returns a pacing type corresponding to the API response's pacing field. """
...
...
course_discovery/apps/course_metadata/data_loaders/tests/test_marketing_site.py
View file @
8e084214
...
...
@@ -11,14 +11,14 @@ import responses
from
django.test
import
TestCase
from
opaque_keys.edx.keys
import
CourseKey
from
course_discovery.apps.course_metadata.choices
import
CourseRunStatus
,
CourseRunPacing
from
course_discovery.apps.course_metadata.data_loaders.marketing_site
import
(
XSeriesMarketingSiteDataLoader
,
SubjectMarketingSiteDataLoader
,
SchoolMarketingSiteDataLoader
,
SponsorMarketingSiteDataLoader
,
PersonMarketingSiteDataLoader
,
CourseMarketingSiteDataLoader
)
from
course_discovery.apps.course_metadata.data_loaders.tests
import
JSON
,
mock_data
from
course_discovery.apps.course_metadata.data_loaders.tests.mixins
import
DataLoaderTestMixin
from
course_discovery.apps.course_metadata.models
import
Organization
,
Subject
,
Program
,
Video
,
Person
,
Course
,
\
CourseRun
from
course_discovery.apps.course_metadata.models
import
Organization
,
Subject
,
Program
,
Video
,
Person
,
Course
from
course_discovery.apps.course_metadata.tests
import
factories
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
...
...
@@ -364,8 +364,8 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
@ddt.unpack
@ddt.data
(
(
'0'
,
CourseRun
.
Status
.
Unpublished
),
(
'1'
,
CourseRun
.
Status
.
Published
),
(
'0'
,
CourseRunStatus
.
Unpublished
),
(
'1'
,
CourseRunStatus
.
Published
),
)
def
test_get_course_run_status
(
self
,
marketing_site_status
,
expected
):
data
=
{
'status'
:
marketing_site_status
}
...
...
@@ -394,10 +394,10 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
@ddt.unpack
@ddt.data
(
(
True
,
CourseRun
.
Pacing
.
Self
),
(
False
,
CourseRun
.
Pacing
.
Instructor
),
(
None
,
CourseRun
.
Pacing
.
Instructor
),
(
''
,
CourseRun
.
Pacing
.
Instructor
),
(
True
,
CourseRunPacing
.
Self
),
(
False
,
CourseRunPacing
.
Instructor
),
(
None
,
CourseRunPacing
.
Instructor
),
(
''
,
CourseRunPacing
.
Instructor
),
)
def
test_get_pacing_type
(
self
,
data_value
,
expected_pacing_type
):
data
=
{
'field_course_self_paced'
:
data_value
}
...
...
course_discovery/apps/course_metadata/forms.py
View file @
8e084214
...
...
@@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
from
django.forms.utils
import
ErrorList
from
django.utils.translation
import
ugettext_lazy
as
_
from
course_discovery.apps.course_metadata.choices
import
ProgramStatus
from
course_discovery.apps.course_metadata.models
import
Program
,
CourseRun
...
...
@@ -46,7 +47,7 @@ class ProgramAdminForm(forms.ModelForm):
def
clean
(
self
):
status
=
self
.
cleaned_data
.
get
(
'status'
)
banner_image
=
self
.
cleaned_data
.
get
(
'banner_image'
)
if
status
==
Program
.
Status
.
Active
and
not
banner_image
:
if
status
==
ProgramStatus
.
Active
and
not
banner_image
:
raise
ValidationError
(
_
(
'Status cannot be change to active without banner image.'
))
return
self
.
cleaned_data
...
...
course_discovery/apps/course_metadata/models.py
View file @
8e084214
...
...
@@ -10,7 +10,6 @@ from django.db.models.query_utils import Q
from
django.utils.translation
import
ugettext_lazy
as
_
from
django_extensions.db.fields
import
AutoSlugField
from
django_extensions.db.models
import
TimeStampedModel
from
djchoices
import
DjangoChoices
,
ChoiceItem
from
haystack
import
connections
from
haystack.query
import
SearchQuerySet
from
simple_history.models
import
HistoricalRecords
...
...
@@ -20,6 +19,7 @@ from taggit.managers import TaggableManager
import
waffle
from
course_discovery.apps.core.models
import
Currency
,
Partner
from
course_discovery.apps.course_metadata.choices
import
CourseRunStatus
,
CourseRunPacing
,
ProgramStatus
from
course_discovery.apps.course_metadata.publishers
import
MarketingSitePublisher
from
course_discovery.apps.course_metadata.query
import
CourseQuerySet
,
CourseRunQuerySet
,
ProgramQuerySet
from
course_discovery.apps.course_metadata.utils
import
UploadToFieldNamePath
...
...
@@ -325,22 +325,11 @@ class Course(TimeStampedModel):
class
CourseRun
(
TimeStampedModel
):
""" CourseRun model. """
class
Status
(
DjangoChoices
):
Published
=
ChoiceItem
(
'published'
,
_
(
'Published'
))
Unpublished
=
ChoiceItem
(
'unpublished'
,
_
(
'Unpublished'
))
class
Pacing
(
DjangoChoices
):
# Translators: Instructor-paced refers to course runs that operate on a schedule set by the instructor,
# similar to a normal university course.
Instructor
=
ChoiceItem
(
'instructor_paced'
,
_
(
'Instructor-paced'
))
# Translators: Self-paced refers to course runs that operate on the student's schedule.
Self
=
ChoiceItem
(
'self_paced'
,
_
(
'Self-paced'
))
uuid
=
models
.
UUIDField
(
default
=
uuid4
,
editable
=
False
,
verbose_name
=
_
(
'UUID'
))
course
=
models
.
ForeignKey
(
Course
,
related_name
=
'course_runs'
)
key
=
models
.
CharField
(
max_length
=
255
,
unique
=
True
)
status
=
models
.
CharField
(
max_length
=
255
,
null
=
False
,
blank
=
False
,
db_index
=
True
,
choices
=
Status
.
choices
,
validators
=
[
Status
.
validator
])
status
=
models
.
CharField
(
max_length
=
255
,
null
=
False
,
blank
=
False
,
db_index
=
True
,
choices
=
CourseRun
Status
.
choices
,
validators
=
[
CourseRun
Status
.
validator
])
title_override
=
models
.
CharField
(
max_length
=
255
,
default
=
None
,
null
=
True
,
blank
=
True
,
help_text
=
_
(
...
...
@@ -369,8 +358,8 @@ class CourseRun(TimeStampedModel):
help_text
=
_
(
'Estimated maximum number of hours per week needed to complete a course run.'
))
language
=
models
.
ForeignKey
(
LanguageTag
,
null
=
True
,
blank
=
True
)
transcript_languages
=
models
.
ManyToManyField
(
LanguageTag
,
blank
=
True
,
related_name
=
'transcript_courses'
)
pacing_type
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
,
null
=
True
,
blank
=
True
,
choices
=
Pacing
.
choices
,
validators
=
[
Pacing
.
validator
])
pacing_type
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
,
null
=
True
,
blank
=
True
,
choices
=
CourseRunPacing
.
choices
,
validators
=
[
CourseRun
Pacing
.
validator
])
syllabus
=
models
.
ForeignKey
(
SyllabusItem
,
default
=
None
,
null
=
True
,
blank
=
True
)
card_image_url
=
models
.
URLField
(
null
=
True
,
blank
=
True
)
video
=
models
.
ForeignKey
(
Video
,
default
=
None
,
null
=
True
,
blank
=
True
)
...
...
@@ -588,12 +577,6 @@ class ProgramType(TimeStampedModel):
class
Program
(
TimeStampedModel
):
class
Status
(
DjangoChoices
):
Unpublished
=
ChoiceItem
(
'unpublished'
,
_
(
'Unpublished'
))
Active
=
ChoiceItem
(
'active'
,
_
(
'Active'
))
Retired
=
ChoiceItem
(
'retired'
,
_
(
'Retired'
))
Deleted
=
ChoiceItem
(
'deleted'
,
_
(
'Deleted'
))
uuid
=
models
.
UUIDField
(
blank
=
True
,
default
=
uuid4
,
editable
=
False
,
unique
=
True
,
verbose_name
=
_
(
'UUID'
))
title
=
models
.
CharField
(
help_text
=
_
(
'The user-facing display title for this Program.'
),
max_length
=
255
,
unique
=
True
)
...
...
@@ -602,7 +585,7 @@ class Program(TimeStampedModel):
type
=
models
.
ForeignKey
(
ProgramType
,
null
=
True
,
blank
=
True
)
status
=
models
.
CharField
(
help_text
=
_
(
'The lifecycle status of this Program.'
),
max_length
=
24
,
null
=
False
,
blank
=
False
,
db_index
=
True
,
choices
=
Status
.
choices
,
validators
=
[
Status
.
validator
]
choices
=
ProgramStatus
.
choices
,
validators
=
[
Program
Status
.
validator
]
)
marketing_slug
=
models
.
CharField
(
help_text
=
_
(
'Slug used to generate links to the marketing site'
),
blank
=
True
,
max_length
=
255
,
db_index
=
True
)
...
...
@@ -721,7 +704,7 @@ class Program(TimeStampedModel):
@property
def
is_active
(
self
):
return
self
.
status
==
self
.
Status
.
Active
return
self
.
status
==
Program
Status
.
Active
def
save
(
self
,
*
args
,
**
kwargs
):
if
waffle
.
switch_is_active
(
'publish_program_to_marketing_site'
)
and
\
...
...
course_discovery/apps/course_metadata/query.py
View file @
8e084214
...
...
@@ -4,6 +4,8 @@ import pytz
from
django.db
import
models
from
django.db.models.query_utils
import
Q
from
course_discovery.apps.course_metadata.choices
import
ProgramStatus
class
CourseQuerySet
(
models
.
QuerySet
):
def
active
(
self
):
...
...
@@ -54,10 +56,16 @@ class ProgramQuerySet(models.QuerySet):
def
marketable
(
self
):
""" Returns Programs that can be marketed to learners.
A Program is considered marketable if it has a defined marketing slug.
A Program is considered marketable if it
is active and
has a defined marketing slug.
Returns:
QuerySet
"""
return
self
.
exclude
(
marketing_slug__isnull
=
True
)
.
exclude
(
marketing_slug
=
''
)
return
self
.
filter
(
status
=
ProgramStatus
.
Active
)
.
exclude
(
marketing_slug__isnull
=
True
)
.
exclude
(
marketing_slug
=
''
)
course_discovery/apps/course_metadata/search_indexes.py
View file @
8e084214
...
...
@@ -3,6 +3,7 @@ import json
from
haystack
import
indexes
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
...
...
@@ -125,7 +126,7 @@ class CourseRunIndex(BaseCourseIndex, indexes.Indexable):
return
obj
.
course
.
partner
.
short_code
def
prepare_published
(
self
,
obj
):
return
obj
.
status
==
CourseRun
.
Status
.
Published
return
obj
.
status
==
CourseRunStatus
.
Published
def
_prepare_language
(
self
,
language
):
if
language
:
...
...
@@ -187,7 +188,7 @@ class ProgramIndex(BaseIndex, indexes.Indexable, OrganizationsMixin):
published
=
indexes
.
BooleanField
(
null
=
False
,
faceted
=
True
)
def
prepare_published
(
self
,
obj
):
return
obj
.
status
==
Program
.
Status
.
Active
return
obj
.
status
==
ProgramStatus
.
Active
def
prepare_organizations
(
self
,
obj
):
return
self
.
prepare_authoring_organizations
(
obj
)
+
self
.
prepare_credit_backing_organizations
(
obj
)
...
...
course_discovery/apps/course_metadata/tests/factories.py
View file @
8e084214
...
...
@@ -6,6 +6,7 @@ from pytz import UTC
from
course_discovery.apps.core.tests.factories
import
PartnerFactory
from
course_discovery.apps.core.tests.utils
import
FuzzyURL
from
course_discovery.apps.course_metadata.choices
import
CourseRunStatus
,
CourseRunPacing
,
ProgramStatus
from
course_discovery.apps.course_metadata.models
import
*
# pylint: disable=wildcard-import
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
...
...
@@ -106,7 +107,7 @@ class CourseFactory(factory.DjangoModelFactory):
class
CourseRunFactory
(
factory
.
DjangoModelFactory
):
status
=
CourseRun
.
Status
.
Published
status
=
CourseRunStatus
.
Published
uuid
=
factory
.
LazyFunction
(
uuid4
)
key
=
FuzzyText
(
prefix
=
'course-run-id/'
,
suffix
=
'/fake'
)
course
=
factory
.
SubFactory
(
CourseFactory
)
...
...
@@ -123,7 +124,7 @@ class CourseRunFactory(factory.DjangoModelFactory):
video
=
factory
.
SubFactory
(
VideoFactory
)
min_effort
=
FuzzyInteger
(
1
,
10
)
max_effort
=
FuzzyInteger
(
10
,
20
)
pacing_type
=
FuzzyChoice
([
name
for
name
,
__
in
CourseRun
.
Pacing
.
choices
])
pacing_type
=
FuzzyChoice
([
name
for
name
,
__
in
CourseRunPacing
.
choices
])
slug
=
FuzzyText
()
@factory.post_generation
...
...
@@ -240,7 +241,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
uuid
=
factory
.
LazyFunction
(
uuid4
)
subtitle
=
'test-subtitle'
type
=
factory
.
SubFactory
(
ProgramTypeFactory
)
status
=
Program
.
Status
.
Unpublished
status
=
ProgramStatus
.
Unpublished
marketing_slug
=
factory
.
Sequence
(
lambda
n
:
'test-slug-{}'
.
format
(
n
))
# pylint: disable=unnecessary-lambda
banner_image_url
=
FuzzyText
(
prefix
=
'https://example.com/program/banner'
)
card_image_url
=
FuzzyText
(
prefix
=
'https://example.com/program/card'
)
...
...
course_discovery/apps/course_metadata/tests/test_admin.py
View file @
8e084214
import
ddt
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
course_discovery.apps.course_metadata.forms
import
ProgramAdminForm
from
course_discovery.apps.course_metadata.models
import
Program
from
course_discovery.apps.course_metadata.choices
import
ProgramStatus
from
course_discovery.apps.course_metadata.forms
import
ProgramAdminForm
from
course_discovery.apps.course_metadata.tests
import
factories
from
course_discovery.apps.core.tests.factories
import
UserFactory
,
USER_PASSWORD
from
course_discovery.apps.core.tests.helpers
import
make_image_file
...
...
@@ -109,7 +109,7 @@ class AdminTests(TestCase):
def
test_program_without_image_and_active_status
(
self
):
""" Verify that new program cannot be added without `image` and active status together."""
data
=
self
.
_post_data
(
Program
.
Status
.
Active
)
data
=
self
.
_post_data
(
ProgramStatus
.
Active
)
form
=
ProgramAdminForm
(
data
,
{
'banner_image'
:
''
})
self
.
assertFalse
(
form
.
is_valid
())
self
.
assertEqual
(
form
.
errors
[
'__all__'
],
[
'Status cannot be change to active without banner image.'
])
...
...
@@ -117,9 +117,9 @@ class AdminTests(TestCase):
form
.
save
()
@ddt.data
(
Program
.
Status
.
Deleted
,
Program
.
Status
.
Retired
,
Program
.
Status
.
Unpublished
ProgramStatus
.
Deleted
,
ProgramStatus
.
Retired
,
ProgramStatus
.
Unpublished
)
def
test_program_without_image_and_non_active_status
(
self
,
status
):
""" Verify that new program can be added without `image` and non-active
...
...
@@ -129,10 +129,10 @@ class AdminTests(TestCase):
self
.
valid_post_form
(
data
,
{
'banner_image'
:
''
})
@ddt.data
(
Program
.
Status
.
Deleted
,
Program
.
Status
.
Retired
,
Program
.
Status
.
Unpublished
,
Program
.
Status
.
Active
ProgramStatus
.
Deleted
,
ProgramStatus
.
Retired
,
ProgramStatus
.
Unpublished
,
ProgramStatus
.
Active
)
def
test_program_with_image
(
self
,
status
):
""" Verify that new program can be added with `image` and any status."""
...
...
@@ -157,7 +157,7 @@ class AdminTests(TestCase):
def
test_new_program_without_courses
(
self
):
""" Verify that new program can be added without `courses`."""
data
=
self
.
_post_data
(
Program
.
Status
.
Unpublished
)
data
=
self
.
_post_data
(
ProgramStatus
.
Unpublished
)
data
[
'courses'
]
=
[]
form
=
ProgramAdminForm
(
data
)
self
.
assertTrue
(
form
.
is_valid
())
...
...
course_discovery/apps/course_metadata/tests/test_models.py
View file @
8e084214
...
...
@@ -14,10 +14,10 @@ import responses
from
course_discovery.apps.core.models
import
Currency
from
course_discovery.apps.core.tests.helpers
import
make_image_file
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
,
Program
,
Course
,
CourseRun
,
Endorsement
,
FAQ
,
SeatType
,
ProgramType
,
CorporateEndorsement
,
Course
,
CourseRun
,
Endorsement
,
FAQ
,
SeatType
,
ProgramType
,
)
from
course_discovery.apps.course_metadata.tests
import
factories
,
toggle_switch
from
course_discovery.apps.course_metadata.tests.factories
import
CourseRunFactory
,
ImageFactory
...
...
@@ -386,10 +386,10 @@ class ProgramTests(MarketingSitePublisherTestMixin):
program
=
self
.
create_program_with_seats
()
self
.
assertEqual
(
program
.
seat_types
,
set
([
'credit'
,
'verified'
]))
@ddt.data
(
Program
.
Status
.
choices
)
@ddt.data
(
ProgramStatus
.
choices
)
def
test_is_active
(
self
,
status
):
self
.
program
.
status
=
status
self
.
assertEqual
(
self
.
program
.
is_active
,
status
==
Program
.
Status
.
Active
)
self
.
assertEqual
(
self
.
program
.
is_active
,
status
==
ProgramStatus
.
Active
)
@responses.activate
def
test_save_without_publish
(
self
):
...
...
course_discovery/apps/course_metadata/tests/test_publishers.py
View file @
8e084214
import
responses
from
course_discovery.apps.course_metadata.choices
import
ProgramStatus
from
course_discovery.apps.course_metadata.publishers
import
(
MarketingSiteAPIClient
,
MarketingSitePublisher
,
...
...
@@ -10,7 +11,7 @@ from course_discovery.apps.course_metadata.tests.mixins import (
MarketingSiteAPIClientTestMixin
,
MarketingSitePublisherTestMixin
,
)
from
course_discovery.apps.course_metadata.models
import
Program
,
Program
Type
from
course_discovery.apps.course_metadata.models
import
ProgramType
class
MarketingSiteAPIClientTests
(
MarketingSiteAPIClientTestMixin
):
...
...
@@ -115,7 +116,7 @@ class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
'author'
:
{
'id'
:
self
.
user_id
,
},
'status'
:
1
if
self
.
program
.
status
==
Program
.
Status
.
Active
else
0
'status'
:
1
if
self
.
program
.
status
==
ProgramStatus
.
Active
else
0
}
self
.
assertDictEqual
(
publish_data
,
expected
)
...
...
course_discovery/apps/course_metadata/tests/test_query.py
View file @
8e084214
...
...
@@ -4,6 +4,7 @@ import ddt
import
pytz
from
django.test
import
TestCase
from
course_discovery.apps.course_metadata.choices
import
ProgramStatus
from
course_discovery.apps.course_metadata.models
import
Course
,
CourseRun
,
Program
from
course_discovery.apps.course_metadata.tests.factories
import
CourseRunFactory
,
ProgramFactory
...
...
@@ -72,11 +73,18 @@ class CourseRunQuerySetTests(TestCase):
self
.
assertEqual
(
CourseRun
.
objects
.
marketable
()
.
count
(),
0
)
@ddt.ddt
class
ProgramQuerySetTests
(
TestCase
):
def
test_marketable
(
self
):
""" Verify the method filters Programs to those with marketing slugs. """
program
=
ProgramFactory
()
self
.
assertEqual
(
list
(
Program
.
objects
.
marketable
()),
[
program
])
@ddt.data
(
(
ProgramStatus
.
Unpublished
,
False
),
(
ProgramStatus
.
Active
,
True
),
)
@ddt.unpack
def
test_marketable
(
self
,
status
,
is_marketable
):
""" Verify the method filters Programs to those which are active and have marketing slugs. """
program
=
ProgramFactory
(
status
=
status
)
expected
=
[
program
]
if
is_marketable
else
[]
self
.
assertEqual
(
list
(
Program
.
objects
.
marketable
()),
expected
)
def
test_marketable_exclusions
(
self
):
""" Verify the method excludes Programs without a marketing slug. """
...
...
course_discovery/apps/publisher/models.py
View file @
8e084214
...
...
@@ -12,8 +12,8 @@ from simple_history.models import HistoricalRecords
from
sortedm2m.fields
import
SortedManyToManyField
from
course_discovery.apps.core.models
import
User
,
Currency
from
course_discovery.apps.course_metadata.choices
import
CourseRunPacing
from
course_discovery.apps.course_metadata.models
import
LevelType
,
Subject
,
Person
,
Organization
from
course_discovery.apps.course_metadata.models
import
CourseRun
as
CourseMetadataCourseRun
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -152,8 +152,8 @@ class CourseRun(TimeStampedModel, ChangedByMixin):
enrollment_end
=
models
.
DateTimeField
(
null
=
True
,
blank
=
True
)
certificate_generation
=
models
.
DateTimeField
(
null
=
True
,
blank
=
True
)
pacing_type
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
,
null
=
True
,
blank
=
True
,
choices
=
Course
MetadataCourseRun
.
Pacing
.
choices
,
validators
=
[
Course
MetadataCourseRun
.
Pacing
.
validator
]
max_length
=
255
,
db_index
=
True
,
null
=
True
,
blank
=
True
,
choices
=
Course
Run
Pacing
.
choices
,
validators
=
[
Course
Run
Pacing
.
validator
]
)
staff
=
SortedManyToManyField
(
Person
,
blank
=
True
,
related_name
=
'publisher_course_runs_staffed'
)
min_effort
=
models
.
PositiveSmallIntegerField
(
...
...
course_discovery/apps/publisher/tests/factories.py
View file @
8e084214
...
...
@@ -6,8 +6,8 @@ from factory.fuzzy import FuzzyText, FuzzyChoice, FuzzyDecimal, FuzzyDateTime, F
from
pytz
import
UTC
from
course_discovery.apps.core.models
import
Currency
from
course_discovery.apps.course_metadata.choices
import
CourseRunPacing
from
course_discovery.apps.course_metadata.tests
import
factories
from
course_discovery.apps.course_metadata.models
import
CourseRun
as
CourseMetadataCourseRun
from
course_discovery.apps.ietf_language_tags.models
import
LanguageTag
from
course_discovery.apps.publisher.models
import
Course
,
CourseRun
,
Seat
,
State
...
...
@@ -48,7 +48,7 @@ class CourseRunFactory(factory.DjangoModelFactory):
min_effort
=
FuzzyInteger
(
1
,
10
)
max_effort
=
FuzzyInteger
(
10
,
20
)
language
=
factory
.
Iterator
(
LanguageTag
.
objects
.
all
())
pacing_type
=
FuzzyChoice
([
name
for
name
,
__
in
Course
MetadataCourseRun
.
Pacing
.
choices
])
pacing_type
=
FuzzyChoice
([
name
for
name
,
__
in
Course
Run
Pacing
.
choices
])
length
=
FuzzyInteger
(
1
,
10
)
seo_review
=
"test-seo-review"
keywords
=
"Test1, Test2, Test3"
...
...
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