Commit 317f719d by Clinton Blackburn Committed by Clinton Blackburn

Added Studio API endpoint to re-run course runs

LEARNER-2470
parent 25684995
...@@ -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
...@@ -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)]}
...@@ -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, CourseRunRerunSerializer, CourseRunSerializer
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)
...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment