Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
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
edx-platform
Commits
317f719d
Commit
317f719d
authored
Sep 02, 2017
by
Clinton Blackburn
Committed by
Clinton Blackburn
Sep 13, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added Studio API endpoint to re-run course runs
LEARNER-2470
parent
25684995
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
132 additions
and
34 deletions
+132
-34
cms/djangoapps/api/v1/serializers/course_runs.py
+46
-3
cms/djangoapps/api/v1/tests/test_views/test_course_runs.py
+52
-12
cms/djangoapps/api/v1/views/course_runs.py
+13
-1
cms/djangoapps/contentstore/views/course.py
+21
-18
No files found.
cms/djangoapps/api/v1/serializers/course_runs.py
View file @
317f719d
...
@@ -4,7 +4,7 @@ from django.db import transaction
...
@@ -4,7 +4,7 @@ from django.db import transaction
from
rest_framework
import
serializers
from
rest_framework
import
serializers
from
rest_framework.fields
import
get_attribute
from
rest_framework.fields
import
get_attribute
from
cms.djangoapps.contentstore.views.course
import
create_new_course
from
cms.djangoapps.contentstore.views.course
import
create_new_course
,
get_course_and_check_access
,
rerun_course
from
student.models
import
CourseAccessRole
from
student.models
import
CourseAccessRole
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
...
@@ -40,11 +40,23 @@ class CourseRunTeamSerializer(serializers.Serializer):
...
@@ -40,11 +40,23 @@ class CourseRunTeamSerializer(serializers.Serializer):
return
instance
return
instance
class
CourseRunSerializer
(
serializers
.
Serializer
):
class
CourseRunTeamSerializerMixin
(
serializers
.
Serializer
):
team
=
CourseRunTeamSerializer
(
required
=
False
)
def
update_team
(
self
,
instance
,
team
):
CourseAccessRole
.
objects
.
filter
(
course_id
=
instance
.
id
)
.
delete
()
# TODO In the future we can optimize by getting users in a single query.
CourseAccessRole
.
objects
.
bulk_create
([
CourseAccessRole
(
course_id
=
instance
.
id
,
role
=
member
[
'role'
],
user
=
User
.
objects
.
get
(
username
=
member
[
'user'
]))
for
member
in
team
])
class
CourseRunSerializer
(
CourseRunTeamSerializerMixin
,
serializers
.
Serializer
):
id
=
serializers
.
CharField
(
read_only
=
True
)
id
=
serializers
.
CharField
(
read_only
=
True
)
title
=
serializers
.
CharField
(
source
=
'display_name'
)
title
=
serializers
.
CharField
(
source
=
'display_name'
)
schedule
=
CourseRunScheduleSerializer
(
source
=
'*'
,
required
=
False
)
schedule
=
CourseRunScheduleSerializer
(
source
=
'*'
,
required
=
False
)
team
=
CourseRunTeamSerializer
(
required
=
False
)
def
update
(
self
,
instance
,
validated_data
):
def
update
(
self
,
instance
,
validated_data
):
team
=
validated_data
.
pop
(
'team'
,
[])
team
=
validated_data
.
pop
(
'team'
,
[])
...
@@ -82,3 +94,34 @@ class CourseRunCreateSerializer(CourseRunSerializer):
...
@@ -82,3 +94,34 @@ class CourseRunCreateSerializer(CourseRunSerializer):
instance
=
create_new_course
(
user
,
_id
[
'org'
],
_id
[
'course'
],
_id
[
'run'
],
validated_data
)
instance
=
create_new_course
(
user
,
_id
[
'org'
],
_id
[
'course'
],
_id
[
'run'
],
validated_data
)
self
.
update_team
(
instance
,
team
)
self
.
update_team
(
instance
,
team
)
return
instance
return
instance
class
CourseRunRerunSerializer
(
CourseRunTeamSerializerMixin
,
serializers
.
Serializer
):
title
=
serializers
.
CharField
(
source
=
'display_name'
,
required
=
False
)
run
=
serializers
.
CharField
(
source
=
'id.run'
)
schedule
=
CourseRunScheduleSerializer
(
source
=
'*'
,
required
=
False
)
def
validate_run
(
self
,
value
):
course_run_key
=
self
.
instance
.
id
store
=
modulestore
()
with
store
.
default_store
(
'split'
):
new_course_run_key
=
store
.
make_course_key
(
course_run_key
.
org
,
course_run_key
.
course
,
value
)
if
store
.
has_course
(
new_course_run_key
,
ignore_case
=
True
):
raise
serializers
.
ValidationError
(
'Course run {key} already exists'
.
format
(
key
=
new_course_run_key
))
return
value
def
update
(
self
,
instance
,
validated_data
):
course_run_key
=
instance
.
id
_id
=
validated_data
.
pop
(
'id'
)
team
=
validated_data
.
pop
(
'team'
,
[])
user
=
self
.
context
[
'request'
]
.
user
fields
=
{
'display_name'
:
instance
.
display_name
}
fields
.
update
(
validated_data
)
new_course_run_key
=
rerun_course
(
user
,
course_run_key
,
course_run_key
.
org
,
course_run_key
.
course
,
_id
[
'run'
],
fields
,
async
=
False
)
course_run
=
get_course_and_check_access
(
new_course_run_key
,
user
)
self
.
update_team
(
course_run
,
team
)
return
course_run
cms/djangoapps/api/v1/tests/test_views/test_course_runs.py
View file @
317f719d
...
@@ -9,7 +9,7 @@ from student.models import CourseAccessRole
...
@@ -9,7 +9,7 @@ from student.models import CourseAccessRole
from
student.tests.factories
import
AdminFactory
,
TEST_PASSWORD
,
UserFactory
from
student.tests.factories
import
AdminFactory
,
TEST_PASSWORD
,
UserFactory
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ToyCourseFactory
from
..utils
import
serialize_datetime
from
..utils
import
serialize_datetime
from
...serializers.course_runs
import
CourseRunSerializer
from
...serializers.course_runs
import
CourseRunSerializer
...
@@ -29,6 +29,11 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
...
@@ -29,6 +29,11 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
assert
course_run
.
enrollment_start
==
enrollment_start
assert
course_run
.
enrollment_start
==
enrollment_start
assert
course_run
.
enrollment_end
==
enrollment_end
assert
course_run
.
enrollment_end
==
enrollment_end
def
assert_access_role
(
self
,
course_run
,
user
,
role
):
# An error will be raised if the endpoint doesn't create the role
CourseAccessRole
.
objects
.
get
(
course_id
=
course_run
.
id
,
user
=
user
,
role
=
role
)
assert
CourseAccessRole
.
objects
.
filter
(
course_id
=
course_run
.
id
)
.
count
()
==
1
def
test_without_authentication
(
self
):
def
test_without_authentication
(
self
):
self
.
client
.
logout
()
self
.
client
.
logout
()
response
=
self
.
client
.
get
(
self
.
list_url
)
response
=
self
.
client
.
get
(
self
.
list_url
)
...
@@ -96,10 +101,7 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
...
@@ -96,10 +101,7 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
}
}
response
=
self
.
client
.
put
(
url
,
data
,
format
=
'json'
)
response
=
self
.
client
.
put
(
url
,
data
,
format
=
'json'
)
assert
response
.
status_code
==
200
assert
response
.
status_code
==
200
self
.
assert_access_role
(
course_run
,
user
,
role
)
# An error will be raised if the endpoint doesn't create the role
CourseAccessRole
.
objects
.
get
(
course_id
=
course_run
.
id
,
user
=
user
,
role
=
role
)
assert
CourseAccessRole
.
objects
.
filter
(
course_id
=
course_run
.
id
)
.
count
()
==
1
course_run
=
modulestore
()
.
get_course
(
course_run
.
id
)
course_run
=
modulestore
()
.
get_course
(
course_run
.
id
)
assert
response
.
data
==
CourseRunSerializer
(
course_run
)
.
data
assert
response
.
data
==
CourseRunSerializer
(
course_run
)
.
data
...
@@ -141,10 +143,7 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
...
@@ -141,10 +143,7 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
url
=
reverse
(
'api:v1:course_run-detail'
,
kwargs
=
{
'pk'
:
str
(
course_run
.
id
)})
url
=
reverse
(
'api:v1:course_run-detail'
,
kwargs
=
{
'pk'
:
str
(
course_run
.
id
)})
response
=
self
.
client
.
patch
(
url
,
data
,
format
=
'json'
)
response
=
self
.
client
.
patch
(
url
,
data
,
format
=
'json'
)
assert
response
.
status_code
==
200
assert
response
.
status_code
==
200
self
.
assert_access_role
(
course_run
,
user
,
role
)
# An error will be raised if the endpoint doesn't create the role
CourseAccessRole
.
objects
.
get
(
course_id
=
course_run
.
id
,
user
=
user
,
role
=
role
)
assert
CourseAccessRole
.
objects
.
filter
(
course_id
=
course_run
.
id
)
.
count
()
==
1
course_run
=
modulestore
()
.
get_course
(
course_run
.
id
)
course_run
=
modulestore
()
.
get_course
(
course_run
.
id
)
self
.
assert_course_run_schedule
(
course_run
,
start
,
None
,
None
,
None
)
self
.
assert_course_run_schedule
(
course_run
,
start
,
None
,
None
,
None
)
...
@@ -184,7 +183,48 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
...
@@ -184,7 +183,48 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
assert
course_run
.
id
.
course
==
data
[
'number'
]
assert
course_run
.
id
.
course
==
data
[
'number'
]
assert
course_run
.
id
.
run
==
data
[
'run'
]
assert
course_run
.
id
.
run
==
data
[
'run'
]
self
.
assert_course_run_schedule
(
course_run
,
start
,
end
,
enrollment_start
,
enrollment_end
)
self
.
assert_course_run_schedule
(
course_run
,
start
,
end
,
enrollment_start
,
enrollment_end
)
self
.
assert_access_role
(
course_run
,
user
,
role
)
# An error will be raised if the endpoint doesn't create the role
def
test_rerun
(
self
):
CourseAccessRole
.
objects
.
get
(
course_id
=
course_run
.
id
,
user
=
user
,
role
=
role
)
course_run
=
ToyCourseFactory
()
assert
CourseAccessRole
.
objects
.
filter
(
course_id
=
course_run
.
id
)
.
count
()
==
1
start
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
.
replace
(
microsecond
=
0
)
end
=
start
+
datetime
.
timedelta
(
days
=
30
)
enrollment_start
=
start
-
datetime
.
timedelta
(
days
=
7
)
enrollment_end
=
end
-
datetime
.
timedelta
(
days
=
14
)
user
=
UserFactory
()
role
=
'instructor'
run
=
'3T2017'
url
=
reverse
(
'api:v1:course_run-rerun'
,
kwargs
=
{
'pk'
:
str
(
course_run
.
id
)})
data
=
{
'run'
:
run
,
'schedule'
:
{
'start'
:
serialize_datetime
(
start
),
'end'
:
serialize_datetime
(
end
),
'enrollment_start'
:
serialize_datetime
(
enrollment_start
),
'enrollment_end'
:
serialize_datetime
(
enrollment_end
),
},
'team'
:
[
{
'user'
:
user
.
username
,
'role'
:
role
,
}
],
}
response
=
self
.
client
.
post
(
url
,
data
,
format
=
'json'
)
assert
response
.
status_code
==
201
course_run_key
=
CourseKey
.
from_string
(
response
.
data
[
'id'
])
course_run
=
modulestore
()
.
get_course
(
course_run_key
)
assert
course_run
.
id
.
run
==
run
self
.
assert_course_run_schedule
(
course_run
,
start
,
end
,
enrollment_start
,
enrollment_end
)
self
.
assert_access_role
(
course_run
,
user
,
role
)
def
test_rerun_duplicate_run
(
self
):
course_run
=
ToyCourseFactory
()
url
=
reverse
(
'api:v1:course_run-rerun'
,
kwargs
=
{
'pk'
:
str
(
course_run
.
id
)})
data
=
{
'run'
:
course_run
.
id
.
run
,
}
response
=
self
.
client
.
post
(
url
,
data
,
format
=
'json'
)
assert
response
.
status_code
==
400
assert
response
.
data
==
{
'run'
:
[
'Course run {key} already exists'
.
format
(
key
=
course_run
.
id
)]}
cms/djangoapps/api/v1/views/course_runs.py
View file @
317f719d
...
@@ -4,10 +4,11 @@ from edx_rest_framework_extensions.authentication import JwtAuthentication
...
@@ -4,10 +4,11 @@ from edx_rest_framework_extensions.authentication import JwtAuthentication
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
rest_framework
import
permissions
,
status
,
viewsets
from
rest_framework
import
permissions
,
status
,
viewsets
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework.authentication
import
SessionAuthentication
from
rest_framework.decorators
import
detail_route
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
contentstore.views.course
import
_accessible_courses_iter
,
get_course_and_check_access
from
contentstore.views.course
import
_accessible_courses_iter
,
get_course_and_check_access
from
..serializers.course_runs
import
CourseRunCreateSerializer
,
CourseRunSerializer
from
..serializers.course_runs
import
CourseRunCreateSerializer
,
CourseRun
RerunSerializer
,
CourseRun
Serializer
class
CourseRunViewSet
(
viewsets
.
ViewSet
):
class
CourseRunViewSet
(
viewsets
.
ViewSet
):
...
@@ -64,3 +65,14 @@ class CourseRunViewSet(viewsets.ViewSet):
...
@@ -64,3 +65,14 @@ class CourseRunViewSet(viewsets.ViewSet):
serializer
.
is_valid
(
raise_exception
=
True
)
serializer
.
is_valid
(
raise_exception
=
True
)
serializer
.
save
()
serializer
.
save
()
return
Response
(
serializer
.
data
,
status
=
status
.
HTTP_201_CREATED
)
return
Response
(
serializer
.
data
,
status
=
status
.
HTTP_201_CREATED
)
@detail_route
(
methods
=
[
'post'
])
def
rerun
(
self
,
request
,
pk
=
None
):
course_run_key
=
CourseKey
.
from_string
(
pk
)
user
=
request
.
user
course_run
=
self
.
get_course_run_or_raise_404
(
course_run_key
,
user
)
serializer
=
CourseRunRerunSerializer
(
course_run
,
data
=
request
.
data
,
context
=
self
.
get_serializer_context
())
serializer
.
is_valid
(
raise_exception
=
True
)
new_course_run
=
serializer
.
save
()
serializer
=
self
.
get_serializer
(
new_course_run
)
return
Response
(
serializer
.
data
,
status
=
status
.
HTTP_201_CREATED
)
cms/djangoapps/contentstore/views/course.py
View file @
317f719d
...
@@ -35,7 +35,7 @@ from contentstore.course_group_config import (
...
@@ -35,7 +35,7 @@ from contentstore.course_group_config import (
from
contentstore.course_info_model
import
delete_course_update
,
get_course_updates
,
update_course_updates
from
contentstore.course_info_model
import
delete_course_update
,
get_course_updates
,
update_course_updates
from
contentstore.courseware_index
import
CoursewareSearchIndexer
,
SearchIndexingError
from
contentstore.courseware_index
import
CoursewareSearchIndexer
,
SearchIndexingError
from
contentstore.push_notification
import
push_notification_enabled
from
contentstore.push_notification
import
push_notification_enabled
from
contentstore.tasks
import
rerun_course
from
contentstore.tasks
import
rerun_course
as
rerun_course_task
from
contentstore.utils
import
(
from
contentstore.utils
import
(
add_instructor
,
add_instructor
,
get_lms_link_for_item
,
get_lms_link_for_item
,
...
@@ -782,8 +782,14 @@ def _create_or_rerun_course(request):
...
@@ -782,8 +782,14 @@ def _create_or_rerun_course(request):
definition_data
=
{
'wiki_slug'
:
wiki_slug
}
definition_data
=
{
'wiki_slug'
:
wiki_slug
}
fields
.
update
(
definition_data
)
fields
.
update
(
definition_data
)
if
'source_course_key'
in
request
.
json
:
source_course_key
=
request
.
json
.
get
(
'source_course_key'
)
return
_rerun_course
(
request
,
org
,
course
,
run
,
fields
)
if
source_course_key
:
source_course_key
=
CourseKey
.
from_string
(
source_course_key
)
destination_course_key
=
rerun_course
(
request
.
user
,
source_course_key
,
org
,
course
,
run
,
fields
)
return
JsonResponse
({
'url'
:
reverse_url
(
'course_handler'
),
'destination_course_key'
:
unicode
(
destination_course_key
)
})
else
:
else
:
try
:
try
:
new_course
=
create_new_course
(
request
.
user
,
org
,
course
,
run
,
fields
)
new_course
=
create_new_course
(
request
.
user
,
org
,
course
,
run
,
fields
)
...
@@ -860,15 +866,12 @@ def create_new_course_in_store(store, user, org, number, run, fields):
...
@@ -860,15 +866,12 @@ def create_new_course_in_store(store, user, org, number, run, fields):
return
new_course
return
new_course
def
_rerun_course
(
request
,
org
,
number
,
run
,
fields
):
def
rerun_course
(
user
,
source_course_key
,
org
,
number
,
run
,
fields
,
async
=
True
):
"""
"""
Reruns an existing course.
Rerun an existing course.
Returns the URL for the course listing page.
"""
"""
source_course_key
=
CourseKey
.
from_string
(
request
.
json
.
get
(
'source_course_key'
))
# verify user has access to the original course
# verify user has access to the original course
if
not
has_studio_write_access
(
request
.
user
,
source_course_key
):
if
not
has_studio_write_access
(
user
,
source_course_key
):
raise
PermissionDenied
()
raise
PermissionDenied
()
# create destination course key
# create destination course key
...
@@ -882,23 +885,23 @@ def _rerun_course(request, org, number, run, fields):
...
@@ -882,23 +885,23 @@ def _rerun_course(request, org, number, run, fields):
# Make sure user has instructor and staff access to the destination course
# Make sure user has instructor and staff access to the destination course
# so the user can see the updated status for that course
# so the user can see the updated status for that course
add_instructor
(
destination_course_key
,
request
.
user
,
request
.
user
)
add_instructor
(
destination_course_key
,
user
,
user
)
# Mark the action as initiated
# Mark the action as initiated
CourseRerunState
.
objects
.
initiated
(
source_course_key
,
destination_course_key
,
request
.
user
,
fields
[
'display_name'
])
CourseRerunState
.
objects
.
initiated
(
source_course_key
,
destination_course_key
,
user
,
fields
[
'display_name'
])
# Clear the fields that must be reset for the rerun
# Clear the fields that must be reset for the rerun
fields
[
'advertised_start'
]
=
None
fields
[
'advertised_start'
]
=
None
# Rerun the course as a new celery task
json_fields
=
json
.
dumps
(
fields
,
cls
=
EdxJSONEncoder
)
json_fields
=
json
.
dumps
(
fields
,
cls
=
EdxJSONEncoder
)
rerun_course
.
delay
(
unicode
(
source_course_key
),
unicode
(
destination_course_key
),
request
.
user
.
id
,
json_fields
)
args
=
[
unicode
(
source_course_key
),
unicode
(
destination_course_key
),
user
.
id
,
json_fields
]
# Return course listing page
if
async
:
return
JsonResponse
({
rerun_course_task
.
delay
(
*
args
)
'url'
:
reverse_url
(
'course_handler'
),
else
:
'destination_course_key'
:
unicode
(
destination_course_key
)
rerun_course_task
(
*
args
)
})
return
destination_course_key
# pylint: disable=unused-argument
# pylint: disable=unused-argument
...
...
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