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
from rest_framework import serializers
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 xmodule.modulestore.django import modulestore
......@@ -40,11 +40,23 @@ class CourseRunTeamSerializer(serializers.Serializer):
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)
title = serializers.CharField(source='display_name')
schedule = CourseRunScheduleSerializer(source='*', required=False)
team = CourseRunTeamSerializer(required=False)
def update(self, instance, validated_data):
team = validated_data.pop('team', [])
......@@ -82,3 +94,34 @@ class CourseRunCreateSerializer(CourseRunSerializer):
instance = create_new_course(user, _id['org'], _id['course'], _id['run'], validated_data)
self.update_team(instance, team)
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
from student.tests.factories import AdminFactory, TEST_PASSWORD, UserFactory
from xmodule.modulestore.django import modulestore
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 ...serializers.course_runs import CourseRunSerializer
......@@ -29,6 +29,11 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
assert course_run.enrollment_start == enrollment_start
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):
self.client.logout()
response = self.client.get(self.list_url)
......@@ -96,10 +101,7 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
}
response = self.client.put(url, data, format='json')
assert response.status_code == 200
# 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
self.assert_access_role(course_run, user, role)
course_run = modulestore().get_course(course_run.id)
assert response.data == CourseRunSerializer(course_run).data
......@@ -141,10 +143,7 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
url = reverse('api:v1:course_run-detail', kwargs={'pk': str(course_run.id)})
response = self.client.patch(url, data, format='json')
assert response.status_code == 200
# 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
self.assert_access_role(course_run, user, role)
course_run = modulestore().get_course(course_run.id)
self.assert_course_run_schedule(course_run, start, None, None, None)
......@@ -184,7 +183,48 @@ class CourseRunViewSetTests(ModuleStoreTestCase):
assert course_run.id.course == data['number']
assert course_run.id.run == data['run']
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
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_rerun(self):
course_run = ToyCourseFactory()
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
from opaque_keys.edx.keys import CourseKey
from rest_framework import permissions, status, viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import detail_route
from rest_framework.response import Response
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):
......@@ -64,3 +65,14 @@ class CourseRunViewSet(viewsets.ViewSet):
serializer.is_valid(raise_exception=True)
serializer.save()
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 (
from contentstore.course_info_model import delete_course_update, get_course_updates, update_course_updates
from contentstore.courseware_index import CoursewareSearchIndexer, SearchIndexingError
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 (
add_instructor,
get_lms_link_for_item,
......@@ -782,8 +782,14 @@ def _create_or_rerun_course(request):
definition_data = {'wiki_slug': wiki_slug}
fields.update(definition_data)
if 'source_course_key' in request.json:
return _rerun_course(request, org, course, run, fields)
source_course_key = request.json.get('source_course_key')
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:
try:
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):
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.
Returns the URL for the course listing page.
Rerun an existing course.
"""
source_course_key = CourseKey.from_string(request.json.get('source_course_key'))
# 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()
# create destination course key
......@@ -882,23 +885,23 @@ def _rerun_course(request, org, number, run, fields):
# Make sure user has instructor and staff access to the destination 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
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
fields['advertised_start'] = None
# Rerun the course as a new celery task
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
return JsonResponse({
'url': reverse_url('course_handler'),
'destination_course_key': unicode(destination_course_key)
})
if async:
rerun_course_task.delay(*args)
else:
rerun_course_task(*args)
return destination_course_key
# 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