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