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
ef635991
Commit
ef635991
authored
Nov 16, 2016
by
Bill DeRusha
Committed by
GitHub
Nov 16, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #417 from edx/bderusha/manually-update-course-run-parent-ECOM-6226
Add 'canonical' field
parents
2e0757af
9bd55913
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
357 additions
and
97 deletions
+357
-97
course_discovery/apps/course_metadata/admin.py
+2
-1
course_discovery/apps/course_metadata/data_loaders/api.py
+63
-14
course_discovery/apps/course_metadata/data_loaders/marketing_site.py
+108
-64
course_discovery/apps/course_metadata/data_loaders/tests/mock_data.py
+0
-0
course_discovery/apps/course_metadata/data_loaders/tests/test_api.py
+37
-1
course_discovery/apps/course_metadata/data_loaders/tests/test_marketing_site.py
+26
-13
course_discovery/apps/course_metadata/forms.py
+15
-1
course_discovery/apps/course_metadata/lookups.py
+13
-1
course_discovery/apps/course_metadata/migrations/0036_course_canonical_course_run.py
+19
-0
course_discovery/apps/course_metadata/migrations/0037_migrate_courses_with_canonical.py
+38
-0
course_discovery/apps/course_metadata/models.py
+3
-0
course_discovery/apps/course_metadata/tests/test_lookups.py
+31
-1
course_discovery/apps/course_metadata/urls.py
+2
-1
No files found.
course_discovery/apps/course_metadata/admin.py
View file @
ef635991
...
...
@@ -4,7 +4,7 @@ from django.http import HttpResponseRedirect
from
django.utils.safestring
import
mark_safe
from
django.utils.translation
import
ugettext_lazy
as
_
from
course_discovery.apps.course_metadata.forms
import
ProgramAdminForm
from
course_discovery.apps.course_metadata.forms
import
ProgramAdminForm
,
CourseAdminForm
from
course_discovery.apps.course_metadata.models
import
*
# pylint: disable=wildcard-import
from
course_discovery.apps.course_metadata.publishers
import
ProgramPublisherException
...
...
@@ -42,6 +42,7 @@ class CorporateEndorsementsInline(admin.TabularInline):
@admin.register
(
Course
)
class
CourseAdmin
(
admin
.
ModelAdmin
):
form
=
CourseAdminForm
list_display
=
(
'uuid'
,
'key'
,
'title'
,)
list_filter
=
(
'partner'
,)
ordering
=
(
'key'
,
'title'
,)
...
...
course_discovery/apps/course_metadata/data_loaders/api.py
View file @
ef635991
...
...
@@ -107,8 +107,20 @@ class CoursesApiDataLoader(AbstractDataLoader):
try
:
body
=
self
.
clean_strings
(
body
)
course
=
self
.
update_course
(
body
)
self
.
update_course_run
(
course
,
body
)
course_run
=
self
.
get_course_run
(
body
)
if
course_run
:
self
.
update_course_run
(
course_run
,
body
)
course
=
getattr
(
course_run
,
'canonical_for_course'
,
False
)
if
course
:
course
=
self
.
update_course
(
course
,
body
)
logger
.
info
(
'Processed course with key [
%
s].'
,
course
.
key
)
else
:
course
,
created
=
self
.
get_or_create_course
(
body
)
course_run
=
self
.
create_course_run
(
course
,
body
)
if
created
:
course
.
canonical_course_run
=
course_run
course
.
save
()
except
:
# pylint: disable=bare-except
msg
=
'An error occurred while updating {course_run} from {api_url}'
.
format
(
course_run
=
course_run_id
,
...
...
@@ -116,14 +128,33 @@ class CoursesApiDataLoader(AbstractDataLoader):
)
logger
.
exception
(
msg
)
def
update_course
(
self
,
body
):
def
get_course_run
(
self
,
body
):
course_run_key
=
body
[
'id'
]
try
:
return
CourseRun
.
objects
.
get
(
key__iexact
=
course_run_key
)
except
CourseRun
.
DoesNotExist
:
return
None
def
update_course_run
(
self
,
course_run
,
body
):
validated_data
=
self
.
format_course_run_data
(
body
)
self
.
_update_instance
(
course_run
,
validated_data
)
logger
.
info
(
'Processed course run with UUID [
%
s].'
,
course_run
.
uuid
)
def
create_course_run
(
self
,
course
,
body
):
defaults
=
self
.
format_course_run_data
(
body
,
course
=
course
)
return
CourseRun
.
objects
.
create
(
**
defaults
)
def
get_or_create_course
(
self
,
body
):
course_run_key
=
CourseKey
.
from_string
(
body
[
'id'
])
course_key
=
self
.
get_course_key_from_course_run_key
(
course_run_key
)
defaults
=
self
.
format_course_data
(
body
)
# We need to add the key to the defaults because django ignores kwargs with __
# separators when constructing the create request
defaults
[
'key'
]
=
course_key
defaults
[
'partner'
]
=
self
.
partner
defaults
=
{
'key'
:
course_key
,
'title'
:
body
[
'name'
],
}
course
,
created
=
Course
.
objects
.
get_or_create
(
key__iexact
=
course_key
,
partner
=
self
.
partner
,
defaults
=
defaults
)
if
created
:
...
...
@@ -133,16 +164,27 @@ class CoursesApiDataLoader(AbstractDataLoader):
defaults
=
{
'key'
:
key
}
organization
,
__
=
Organization
.
objects
.
get_or_create
(
key__iexact
=
key
,
partner
=
self
.
partner
,
defaults
=
defaults
)
course
.
authoring_organizations
.
add
(
organization
)
logger
.
info
(
'Processed course with key [
%
s].'
,
course_key
)
return
(
course
,
created
)
def
update_course
(
self
,
course
,
body
):
validated_data
=
self
.
format_course_data
(
body
)
self
.
_update_instance
(
course
,
validated_data
)
logger
.
info
(
'Processed course with key [
%
s].'
,
course
.
key
)
return
course
def
update_course_run
(
self
,
course
,
body
):
key
=
body
[
'id'
]
def
_update_instance
(
self
,
instance
,
validated_data
):
for
attr
,
value
in
validated_data
.
items
():
setattr
(
instance
,
attr
,
value
)
instance
.
save
()
def
format_course_run_data
(
self
,
body
,
course
=
None
):
defaults
=
{
'key'
:
key
,
'key'
:
body
[
'id'
]
,
'end'
:
self
.
parse_date
(
body
[
'end'
]),
'enrollment_start'
:
self
.
parse_date
(
body
[
'enrollment_start'
]),
'enrollment_end'
:
self
.
parse_date
(
body
[
'enrollment_end'
]),
...
...
@@ -162,10 +204,17 @@ class CoursesApiDataLoader(AbstractDataLoader):
'mobile_available'
:
body
.
get
(
'mobile_available'
)
or
False
,
})
course_run
,
__
=
course
.
course_runs
.
update_or_create
(
key__iexact
=
key
,
defaults
=
defaults
)
if
course
:
defaults
[
'course'
]
=
course
return
defaults
def
format_course_data
(
self
,
body
):
defaults
=
{
'title'
:
body
[
'name'
],
}
logger
.
info
(
'Processed course run with key [
%
s].'
,
course_run
.
key
)
return
course_run
return
defaults
def
get_pacing_type
(
self
,
body
):
pacing
=
body
.
get
(
'pacing'
)
...
...
course_discovery/apps/course_metadata/data_loaders/marketing_site.py
View file @
ef635991
...
...
@@ -377,73 +377,78 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
return
kwargs
def
process_node
(
self
,
data
):
course_run_key
=
CourseKey
.
from_string
(
data
[
'field_course_id'
])
key
=
self
.
get_course_key_from_course_run_key
(
course_run_key
)
course_run
=
self
.
get_course_run
(
data
)
# Clean the title for the course and course run
data
[
'field_course_course_title'
][
'value'
]
=
self
.
clean_html
(
data
[
'field_course_course_title'
][
'value'
])
if
course_run
:
self
.
update_course_run
(
course_run
,
data
)
try
:
course
=
self
.
update_course
(
course_run
.
canonical_for_course
,
data
)
self
.
set_subjects
(
course
,
data
)
self
.
set_authoring_organizations
(
course
,
data
)
logger
.
info
(
'Processed course with key [
%
s].'
,
course
.
key
)
except
AttributeError
:
pass
else
:
course
,
created
=
self
.
get_or_create_course
(
data
)
course_run
=
self
.
create_course_run
(
course
,
data
)
if
created
:
course
.
canonical_course_run
=
course_run
course
.
save
()
defaults
=
{
'key'
:
key
,
'title'
:
self
.
clean_html
(
data
[
'field_course_course_title'
][
'value'
]),
'number'
:
data
[
'field_course_code'
],
'full_description'
:
self
.
get_description
(
data
),
'video'
:
self
.
get_video
(
data
),
'short_description'
:
self
.
clean_html
(
data
[
'field_course_sub_title_short'
]),
'level_type'
:
self
.
get_level_type
(
data
[
'field_course_level'
]),
'card_image_url'
:
self
.
_get_nested_url
(
data
.
get
(
'field_course_image_promoted'
)),
}
course
,
created
=
Course
.
objects
.
get_or_create
(
key__iexact
=
key
,
partner
=
self
.
partner
,
defaults
=
defaults
)
def
get_course_run
(
self
,
data
):
course_run_key
=
data
[
'field_course_id'
]
try
:
return
CourseRun
.
objects
.
get
(
key__iexact
=
course_run_key
)
except
CourseRun
.
DoesNotExist
:
return
None
# 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
)
==
CourseRunStatus
.
Published
if
not
created
and
published
:
for
attr
,
value
in
defaults
.
items
():
setattr
(
course
,
attr
,
value
)
course
.
save
()
def
update_course_run
(
self
,
course_run
,
data
):
validated_data
=
self
.
format_course_run_data
(
data
,
course_run
.
course
)
self
.
_update_instance
(
course_run
,
validated_data
)
self
.
set_course_run_staff
(
course_run
,
data
)
self
.
set_course_run_transcript_languages
(
course_run
,
data
)
self
.
set_subjects
(
course
,
data
)
self
.
set_authoring_organizations
(
course
,
data
)
self
.
create_course_run
(
course
,
data
)
logger
.
info
(
'Processed course run with UUID [
%
s].'
,
course_run
.
uuid
)
logger
.
info
(
'Processed course with key [
%
s].'
,
key
)
return
course
def
create_course_run
(
self
,
course
,
data
):
defaults
=
self
.
format_course_run_data
(
data
,
course
)
def
get_description
(
self
,
data
):
description
=
(
data
.
get
(
'field_course_body'
,
{})
or
{})
.
get
(
'value'
)
description
=
description
or
(
data
.
get
(
'field_course_description'
,
{})
or
{})
.
get
(
'value'
)
description
=
description
or
''
description
=
self
.
clean_html
(
description
)
return
description
course_run
=
CourseRun
.
objects
.
create
(
**
defaults
)
self
.
set_course_run_staff
(
course_run
,
data
)
self
.
set_course_run_transcript_languages
(
course_run
,
data
)
def
get_course_run_status
(
self
,
data
):
return
CourseRunStatus
.
Published
if
bool
(
int
(
data
[
'status'
]))
else
CourseRunStatus
.
Unpublished
return
course_run
def
get_level_type
(
self
,
name
):
level_type
=
None
def
get_or_create_course
(
self
,
data
):
course_run_key
=
CourseKey
.
from_string
(
data
[
'field_course_id'
])
key
=
self
.
get_course_key_from_course_run_key
(
course_run_key
)
defaults
=
self
.
format_course_data
(
data
,
key
=
key
)
if
name
:
level_type
,
__
=
LevelType
.
objects
.
get_or_create
(
name
=
name
)
course
,
created
=
Course
.
objects
.
get_or_create
(
key__iexact
=
key
,
partner
=
self
.
partner
,
defaults
=
defaults
)
return
level_type
if
created
:
self
.
set_subjects
(
course
,
data
)
self
.
set_authoring_organizations
(
course
,
data
)
def
get_video
(
self
,
data
):
video_url
=
self
.
_get_nested_url
(
data
.
get
(
'field_course_video'
)
or
data
.
get
(
'field_product_video'
))
image_url
=
self
.
_get_nested_url
(
data
.
get
(
'field_course_image_featured_card'
))
return
self
.
get_or_create_video
(
video_url
,
image_url
)
return
(
course
,
created
)
def
get_pacing_type
(
self
,
data
):
self_paced
=
data
.
get
(
'field_course_self_paced'
,
False
)
return
CourseRunPacing
.
Self
if
self_paced
else
CourseRunPacing
.
Instructor
def
update_course
(
self
,
course
,
data
):
validated_data
=
self
.
format_course_data
(
data
)
self
.
_update_instance
(
course
,
validated_data
)
def
get_hidden
(
self
,
data
)
:
# 'couse' [sic]. The field is misspelled on Drupal. ಠ_ಠ
hidden
=
data
.
get
(
'field_couse_is_hidden'
,
False
)
return
hidden
is
True
if
self
.
get_course_run_status
(
data
)
!=
CourseRunStatus
.
Published
:
logger
.
warning
(
'Updating course [
%
s] with data from unpublished course_run [
%
s].'
,
course
.
uuid
,
data
[
'field_course_id'
]
)
def
create_course_run
(
self
,
course
,
data
):
return
course
def
_update_instance
(
self
,
instance
,
validated_data
):
for
attr
,
value
in
validated_data
.
items
():
setattr
(
instance
,
attr
,
value
)
instance
.
save
()
def
format_course_run_data
(
self
,
data
,
course
):
uuid
=
data
[
'uuid'
]
key
=
data
[
'field_course_id'
]
slug
=
data
[
'url'
]
.
split
(
'/'
)[
-
1
]
...
...
@@ -457,7 +462,6 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
defaults
=
{
'key'
:
key
,
'course'
:
course
,
'uuid'
:
uuid
,
'title_override'
:
self
.
clean_html
(
data
[
'field_course_course_title'
][
'value'
]),
'language'
:
language
,
...
...
@@ -470,6 +474,7 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
'weeks_to_complete'
:
None
,
'mobile_available'
:
data
.
get
(
'field_course_enrollment_mobile'
)
or
False
,
'video'
:
course
.
video
,
'course'
:
course
,
}
if
weeks_to_complete
:
...
...
@@ -478,18 +483,57 @@ class CourseMarketingSiteDataLoader(AbstractMarketingSiteDataLoader):
weeks_to_complete
=
rrule
.
rrule
(
rrule
.
WEEKLY
,
dtstart
=
start
,
until
=
end
)
.
count
()
defaults
[
'weeks_to_complete'
]
=
int
(
weeks_to_complete
)
try
:
course_run
,
__
=
CourseRun
.
objects
.
update_or_create
(
key__iexact
=
key
,
defaults
=
defaults
)
except
TypeError
:
# TODO Fix the data in Drupal (ECOM-5304)
logger
.
error
(
'Multiple course runs are identified by the key [
%
s] or UUID [
%
s].'
,
key
,
uuid
)
return
None
return
defaults
self
.
set_course_run_staff
(
course_run
,
data
)
self
.
set_course_run_transcript_languages
(
course_run
,
data
)
def
format_course_data
(
self
,
data
,
key
=
None
):
if
not
key
:
course_run_key
=
CourseKey
.
from_string
(
data
[
'field_course_id'
])
key
=
self
.
get_course_key_from_course_run_key
(
course_run_key
)
logger
.
info
(
'Processed course run with UUID [
%
s].'
,
uuid
)
return
course_run
defaults
=
{
'key'
:
key
,
'title'
:
self
.
clean_html
(
data
[
'field_course_course_title'
][
'value'
]),
'number'
:
data
[
'field_course_code'
],
'full_description'
:
self
.
get_description
(
data
),
'video'
:
self
.
get_video
(
data
),
'short_description'
:
self
.
clean_html
(
data
[
'field_course_sub_title_short'
]),
'level_type'
:
self
.
get_level_type
(
data
[
'field_course_level'
]),
'card_image_url'
:
self
.
_get_nested_url
(
data
.
get
(
'field_course_image_promoted'
)),
}
return
defaults
def
get_description
(
self
,
data
):
description
=
(
data
.
get
(
'field_course_body'
,
{})
or
{})
.
get
(
'value'
)
description
=
description
or
(
data
.
get
(
'field_course_description'
,
{})
or
{})
.
get
(
'value'
)
description
=
description
or
''
description
=
self
.
clean_html
(
description
)
return
description
def
get_course_run_status
(
self
,
data
):
return
CourseRunStatus
.
Published
if
bool
(
int
(
data
[
'status'
]))
else
CourseRunStatus
.
Unpublished
def
get_level_type
(
self
,
name
):
level_type
=
None
if
name
:
level_type
,
__
=
LevelType
.
objects
.
get_or_create
(
name
=
name
)
return
level_type
def
get_video
(
self
,
data
):
video_url
=
self
.
_get_nested_url
(
data
.
get
(
'field_course_video'
)
or
data
.
get
(
'field_product_video'
))
image_url
=
self
.
_get_nested_url
(
data
.
get
(
'field_course_image_featured_card'
))
return
self
.
get_or_create_video
(
video_url
,
image_url
)
def
get_pacing_type
(
self
,
data
):
self_paced
=
data
.
get
(
'field_course_self_paced'
,
False
)
return
CourseRunPacing
.
Self
if
self_paced
else
CourseRunPacing
.
Instructor
def
get_hidden
(
self
,
data
):
# 'couse' [sic]. The field is misspelled on Drupal. ಠ_ಠ
hidden
=
data
.
get
(
'field_couse_is_hidden'
,
False
)
return
hidden
is
True
def
_get_objects_by_uuid
(
self
,
object_type
,
raw_objects_data
):
uuids
=
[
_object
.
get
(
'uuid'
)
for
_object
in
raw_objects_data
]
...
...
course_discovery/apps/course_metadata/data_loaders/tests/mock_data.py
View file @
ef635991
This diff is collapsed.
Click to expand it.
course_discovery/apps/course_metadata/data_loaders/tests/test_api.py
View file @
ef635991
...
...
@@ -133,7 +133,8 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
def
api_url
(
self
):
return
self
.
partner
.
courses_api_url
def
mock_api
(
self
):
def
mock_api
(
self
,
bodies
=
None
):
if
not
bodies
:
bodies
=
mock_data
.
COURSES_API_BODIES
url
=
self
.
api_url
+
'courses/'
responses
.
add_callback
(
...
...
@@ -185,6 +186,8 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
for
field
,
value
in
expected_values
.
items
():
self
.
assertEqual
(
getattr
(
course_run
,
field
),
value
,
'Field {} is invalid.'
.
format
(
field
))
return
course_run
@responses.activate
@ddt.data
(
True
,
False
)
def
test_ingest
(
self
,
partner_has_marketing_site
):
...
...
@@ -227,6 +230,39 @@ class CoursesApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestCas
)
mock_logger
.
exception
.
assert_called_with
(
msg
)
@responses.activate
def
test_ingest_canonical
(
self
):
""" Verify the method ingests data from the Courses API. """
self
.
assertEqual
(
Course
.
objects
.
count
(),
0
)
self
.
assertEqual
(
CourseRun
.
objects
.
count
(),
0
)
self
.
mock_api
([
mock_data
.
COURSES_API_BODY_ORIGINAL
,
mock_data
.
COURSES_API_BODY_SECOND
,
mock_data
.
COURSES_API_BODY_UPDATED
,
])
self
.
loader
.
ingest
()
# Verify the CourseRun was created correctly by no errors raised
course_run_orig
=
CourseRun
.
objects
.
get
(
key
=
mock_data
.
COURSES_API_BODY_ORIGINAL
[
'id'
])
# Verify that a course has been created and set as canonical by no errors raised
course
=
course_run_orig
.
canonical_for_course
# Verify the CourseRun was created correctly by no errors raised
course_run_second
=
CourseRun
.
objects
.
get
(
key
=
mock_data
.
COURSES_API_BODY_SECOND
[
'id'
])
# Verify not set as canonical
with
self
.
assertRaises
(
AttributeError
):
course_run_second
.
canonical_for_course
# pylint: disable=pointless-statement
# Verify second course not used to update course
self
.
assertNotEqual
(
mock_data
.
COURSES_API_BODY_SECOND
[
'name'
],
course
.
title
)
# Verify udpated canonical course used to update course
self
.
assertEqual
(
mock_data
.
COURSES_API_BODY_UPDATED
[
'name'
],
course
.
title
)
# Verify the updated course run updated the original course run
self
.
assertEqual
(
mock_data
.
COURSES_API_BODY_UPDATED
[
'hidden'
],
course_run_orig
.
hidden
)
def
test_get_pacing_type_field_missing
(
self
):
""" Verify the method returns None if the API response does not include a pacing field. """
self
.
assertIsNone
(
self
.
loader
.
get_pacing_type
({}))
...
...
course_discovery/apps/course_metadata/data_loaders/tests/test_marketing_site.py
View file @
ef635991
...
...
@@ -325,9 +325,7 @@ class PersonMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
@ddt.ddt
class
CourseMarketingSiteDataLoaderTests
(
AbstractMarketingSiteDataLoaderTestMixin
,
TestCase
):
loader_class
=
CourseMarketingSiteDataLoader
mocked_data
=
mock_data
.
MARKETING_SITE_API_COURSE_BODIES
mocked_data
.
append
(
mock_data
.
MARKETING_SITE_API_UNPUBLISHED_COPY_COURSE_BODY
)
mocked_data
.
append
(
mock_data
.
MARKETING_SITE_API_PUBLISHED_COPY_COURSE_BODY
)
mocked_data
=
mock_data
.
UNIQUE_MARKETING_SITE_API_COURSE_BODIES
def
_get_uuids
(
self
,
items
):
return
[
item
[
'uuid'
]
for
item
in
items
]
...
...
@@ -513,6 +511,8 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
expected_transcript_languages
=
self
.
loader
.
get_language_tags_from_names
(
language_names
)
self
.
assertEqual
(
list
(
course_run
.
transcript_languages
.
all
()),
list
(
expected_transcript_languages
))
return
course_run
def
_get_course
(
self
,
data
):
course_run_key
=
CourseKey
.
from_string
(
data
[
'field_course_id'
])
return
Course
.
objects
.
get
(
key
=
self
.
loader
.
get_course_key_from_course_run_key
(
course_run_key
),
...
...
@@ -522,20 +522,33 @@ class CourseMarketingSiteDataLoaderTests(AbstractMarketingSiteDataLoaderTestMixi
def
test_ingest
(
self
):
self
.
mock_login_response
()
data
=
self
.
mock_api
()
published_course_run_key
=
mock_data
.
MARKETING_SITE_API_PUBLISHED_COPY_COURSE_BODY
[
'field_course_id'
]
self
.
loader
.
ingest
()
for
datum
in
data
:
self
.
assert_course_run_loaded
(
datum
)
if
datum
[
'field_course_code'
]
==
mock_data
.
MULTI_COURSE_RUN_COURSE_NUMBER
:
# For the original course and the unpublished course ensure course fields are not present.
if
datum
[
'field_course_id'
]
!=
published_course_run_key
:
self
.
assert_no_override_unpublished_course_fields
(
datum
)
# For the latest published course ensure course fields match the latest saved course.
else
:
self
.
assert_course_loaded
(
datum
)
else
:
self
.
assert_course_loaded
(
datum
)
@responses.activate
def
test_canonical
(
self
):
self
.
mocked_data
=
[
mock_data
.
ORIGINAL_MARKETING_SITE_API_COURSE_BODY
,
mock_data
.
NEW_RUN_MARKETING_SITE_API_COURSE_BODY
,
mock_data
.
UPDATED_MARKETING_SITE_API_COURSE_BODY
,
]
self
.
mock_login_response
()
self
.
mock_api
()
self
.
loader
.
ingest
()
course_run
=
self
.
assert_course_run_loaded
(
mock_data
.
UPDATED_MARKETING_SITE_API_COURSE_BODY
)
self
.
assert_course_loaded
(
mock_data
.
UPDATED_MARKETING_SITE_API_COURSE_BODY
)
self
.
assertTrue
(
course_run
.
canonical_for_course
)
course_run
=
self
.
assert_course_run_loaded
(
mock_data
.
NEW_RUN_MARKETING_SITE_API_COURSE_BODY
)
course
=
course_run
.
course
new_run_title
=
mock_data
.
NEW_RUN_MARKETING_SITE_API_COURSE_BODY
[
'field_course_course_title'
][
'value'
]
self
.
assertNotEqual
(
course
.
title
,
new_run_title
)
with
self
.
assertRaises
(
AttributeError
):
course_run
.
canonical_for_course
# pylint: disable=pointless-statement
course_discovery/apps/course_metadata/forms.py
View file @
ef635991
...
...
@@ -5,7 +5,7 @@ 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
from
course_discovery.apps.course_metadata.models
import
Program
,
CourseRun
,
Course
def
filter_choices_to_render_with_order_preserved
(
self
,
selected_choices
):
...
...
@@ -99,3 +99,17 @@ class CourseRunSelectionForm(forms.ModelForm):
self
.
fields
[
'excluded_course_runs'
]
.
queryset
=
CourseRun
.
objects
.
filter
(
course__id__in
=
query_set
)
class
CourseAdminForm
(
forms
.
ModelForm
):
class
Meta
:
model
=
Course
fields
=
'__all__'
widgets
=
{
'canonical_course_run'
:
autocomplete
.
ModelSelect2
(
url
=
'admin_metadata:course-run-autocomplete'
,
attrs
=
{
'data-minimum-input-length'
:
3
,
}
),
}
course_discovery/apps/course_metadata/lookups.py
View file @
ef635991
from
django.db.models
import
Q
from
dal
import
autocomplete
from
.models
import
Course
,
Organization
,
Video
from
.models
import
Course
,
CourseRun
,
Organization
,
Video
class
CourseAutocomplete
(
autocomplete
.
Select2QuerySetView
):
...
...
@@ -16,6 +16,18 @@ class CourseAutocomplete(autocomplete.Select2QuerySetView):
return
[]
class
CourseRunAutocomplete
(
autocomplete
.
Select2QuerySetView
):
def
get_queryset
(
self
):
if
self
.
request
.
user
.
is_authenticated
()
and
self
.
request
.
user
.
is_staff
:
qs
=
CourseRun
.
objects
.
all
()
.
select_related
(
'course'
)
if
self
.
q
:
qs
=
qs
.
filter
(
Q
(
key__icontains
=
self
.
q
)
|
Q
(
course__title__icontains
=
self
.
q
))
return
qs
return
[]
class
OrganizationAutocomplete
(
autocomplete
.
Select2QuerySetView
):
def
get_queryset
(
self
):
if
self
.
request
.
user
.
is_authenticated
()
and
self
.
request
.
user
.
is_staff
:
...
...
course_discovery/apps/course_metadata/migrations/0036_course_canonical_course_run.py
0 → 100644
View file @
ef635991
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'course_metadata'
,
'0035_auto_20161103_2129'
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
'course'
,
name
=
'canonical_course_run'
,
field
=
models
.
OneToOneField
(
null
=
True
,
default
=
None
,
blank
=
True
,
to
=
'course_metadata.CourseRun'
,
related_name
=
'canonical_for_course'
),
),
]
course_discovery/apps/course_metadata/migrations/0037_migrate_courses_with_canonical.py
0 → 100644
View file @
ef635991
from
django.db
import
migrations
from
course_discovery.apps.course_metadata.choices
import
CourseRunStatus
def
create_canonical
(
apps
,
schema_editor
):
"""Create the canonical course run associations."""
Course
=
apps
.
get_model
(
'course_metadata'
,
'Course'
)
courses
=
Course
.
objects
.
prefetch_related
(
'course_runs'
)
.
all
()
for
course
in
courses
:
course_runs
=
course
.
course_runs
.
all
()
.
order_by
(
'-start'
)
published_course_runs
=
course_runs
.
filter
(
status
=
CourseRunStatus
.
Published
)
if
published_course_runs
:
# If there is a published course_run use the latest
canonical_course_run
=
published_course_runs
[
0
]
else
:
# otherwise just use the latest in general
canonical_course_run
=
course_runs
.
first
()
course
.
canonical_course_run
=
canonical_course_run
course
.
save
()
def
delete_canonical
(
apps
,
schema_editor
):
"""Delete the canonical course run associations."""
Course
=
apps
.
get_model
(
'course_metadata'
,
'Course'
)
Course
.
objects
.
all
()
.
update
(
canonical_course_run
=
None
)
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'course_metadata'
,
'0036_course_canonical_course_run'
),
]
operations
=
[
migrations
.
RunPython
(
create_canonical
,
reverse_code
=
delete_canonical
),
]
course_discovery/apps/course_metadata/models.py
View file @
ef635991
...
...
@@ -232,6 +232,9 @@ class Course(TimeStampedModel):
""" Course model. """
partner
=
models
.
ForeignKey
(
Partner
)
uuid
=
models
.
UUIDField
(
default
=
uuid4
,
editable
=
False
,
verbose_name
=
_
(
'UUID'
))
canonical_course_run
=
models
.
OneToOneField
(
'course_metadata.CourseRun'
,
related_name
=
'canonical_for_course'
,
default
=
None
,
null
=
True
,
blank
=
True
)
key
=
models
.
CharField
(
max_length
=
255
)
title
=
models
.
CharField
(
max_length
=
255
,
default
=
None
,
null
=
True
,
blank
=
True
)
short_description
=
models
.
CharField
(
max_length
=
255
,
default
=
None
,
null
=
True
,
blank
=
True
)
...
...
course_discovery/apps/course_metadata/tests/test_lookups.py
View file @
ef635991
...
...
@@ -17,6 +17,8 @@ class AutocompleteTests(TestCase):
self
.
user
=
UserFactory
(
is_staff
=
True
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
USER_PASSWORD
)
self
.
courses
=
factories
.
CourseFactory
.
create_batch
(
3
,
title
=
'Some random course title'
)
for
course
in
self
.
courses
:
factories
.
CourseRunFactory
(
course
=
course
)
self
.
organizations
=
factories
.
OrganizationFactory
.
create_batch
(
3
)
@ddt.data
(
'dum'
,
'ing'
)
...
...
@@ -43,6 +45,34 @@ class AutocompleteTests(TestCase):
data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertEqual
(
data
[
'results'
],
[])
@ddt.data
(
'ing'
,
'dum'
)
def
test_course_run_autocomplete
(
self
,
search_key
):
""" Verify course run autocomplete returns the data. """
response
=
self
.
client
.
get
(
reverse
(
'admin_metadata:course-run-autocomplete'
))
data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
len
(
data
[
'results'
]),
3
)
# update the first course title
course
=
self
.
courses
[
0
]
course
.
title
=
'this is some thing new'
course
.
save
()
course_run
=
self
.
courses
[
0
]
.
course_runs
.
first
()
course_run
.
key
=
'edx/dummy/testrun'
course_run
.
save
()
response
=
self
.
client
.
get
(
reverse
(
'admin_metadata:course-run-autocomplete'
)
+
'?q={q}'
.
format
(
q
=
search_key
)
)
data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertEqual
(
data
[
'results'
][
0
][
'text'
],
str
(
course_run
))
def
test_course_run_autocomplete_un_authorize_user
(
self
):
""" Verify course run autocomplete returns empty list for un-authorized users. """
self
.
_make_user_non_staff
()
response
=
self
.
client
.
get
(
reverse
(
'admin_metadata:course-run-autocomplete'
))
data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertEqual
(
data
[
'results'
],
[])
@ddt.data
(
'irc'
,
'ing'
)
def
test_organization_autocomplete
(
self
,
search_key
):
""" Verify Organization autocomplete returns the data. """
...
...
@@ -77,7 +107,7 @@ class AutocompleteTests(TestCase):
response
=
self
.
client
.
get
(
reverse
(
'admin_metadata:video-autocomplete'
))
data
=
json
.
loads
(
response
.
content
.
decode
(
'utf-8'
))
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
len
(
data
[
'results'
]),
3
)
self
.
assertEqual
(
len
(
data
[
'results'
]),
6
)
self
.
courses
[
0
]
.
video
.
src
=
'http://www.youtube.com/dummyurl'
self
.
courses
[
0
]
.
video
.
description
=
'testing description'
...
...
course_discovery/apps/course_metadata/urls.py
View file @
ef635991
...
...
@@ -5,12 +5,13 @@ from django.conf.urls import url
from
course_discovery.apps.course_metadata.views
import
CourseRunSelectionAdmin
from
course_discovery.apps.course_metadata.lookups
import
(
CourseAutocomplete
,
OrganizationAutocomplete
,
VideoAutocomplete
CourseAutocomplete
,
CourseRunAutocomplete
,
OrganizationAutocomplete
,
VideoAutocomplete
)
urlpatterns
=
[
url
(
r'^update_course_runs/(?P<pk>\d+)/$'
,
CourseRunSelectionAdmin
.
as_view
(),
name
=
'update_course_runs'
,),
url
(
r'^course-autocomplete/$'
,
CourseAutocomplete
.
as_view
(),
name
=
'course-autocomplete'
,),
url
(
r'^course-run-autocomplete/$'
,
CourseRunAutocomplete
.
as_view
(),
name
=
'course-run-autocomplete'
,),
url
(
r'^organisation-autocomplete/$'
,
OrganizationAutocomplete
.
as_view
(),
name
=
'organisation-autocomplete'
,),
url
(
r'^video-autocomplete/$'
,
VideoAutocomplete
.
as_view
(),
name
=
'video-autocomplete'
,),
]
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