Commit 2361dd47 by Waheed Ahmed

Added CourseRunState change endpoint.

ECOM-7004
parent 7f2cad81
...@@ -10,7 +10,7 @@ from rest_framework import serializers ...@@ -10,7 +10,7 @@ from rest_framework import serializers
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.emails import send_email_for_studio_instance_created from course_discovery.apps.publisher.emails import send_email_for_studio_instance_created
from course_discovery.apps.publisher.models import CourseUserRole, CourseRun, CourseState from course_discovery.apps.publisher.models import CourseUserRole, CourseRun, CourseState, CourseRunState
class CourseUserRoleSerializer(serializers.ModelSerializer): class CourseUserRoleSerializer(serializers.ModelSerializer):
...@@ -128,3 +128,32 @@ class CourseStateSerializer(serializers.ModelSerializer): ...@@ -128,3 +128,32 @@ class CourseStateSerializer(serializers.ModelSerializer):
) )
return instance return instance
class CourseRunStateSerializer(serializers.ModelSerializer):
"""Serializer for `CourseRunState` model to change course-run workflow state. """
class Meta:
model = CourseRunState
fields = ('name', 'approved_by_role', 'owner_role', 'course_run',)
extra_kwargs = {
'course_run': {'read_only': True},
'approved_by_role': {'read_only': True},
'owner_role': {'read_only': True}
}
def update(self, instance, validated_data):
state = validated_data.get('name')
try:
instance.change_state(state=state)
except TransitionNotAllowed:
# pylint: disable=no-member
raise serializers.ValidationError(
{
'name': _('Cannot switch from state `{state}` to `{target_state}`').format(
state=instance.name, target_state=state
)
}
)
return instance
...@@ -5,12 +5,13 @@ from rest_framework.exceptions import ValidationError ...@@ -5,12 +5,13 @@ from rest_framework.exceptions import ValidationError
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.api.serializers import ( from course_discovery.apps.publisher.api.serializers import (
CourseUserRoleSerializer, GroupUserSerializer, UpdateCourseKeySerializer, CourseRevisionSerializer, CourseUserRoleSerializer, GroupUserSerializer, UpdateCourseKeySerializer, CourseRevisionSerializer,
CourseStateSerializer CourseStateSerializer, CourseRunStateSerializer
)
from course_discovery.apps.publisher.choices import CourseStateChoices, CourseRunStateChoices
from course_discovery.apps.publisher.models import CourseState, CourseRunState
from course_discovery.apps.publisher.tests.factories import (
CourseFactory, CourseRunFactory, CourseRunStateFactory, CourseStateFactory, CourseUserRoleFactory
) )
from course_discovery.apps.publisher.tests.factories import CourseFactory
from course_discovery.apps.publisher.choices import CourseStateChoices
from course_discovery.apps.publisher.models import CourseState
from course_discovery.apps.publisher.tests.factories import CourseUserRoleFactory, CourseRunFactory, CourseStateFactory
class CourseUserRoleSerializerTests(TestCase): class CourseUserRoleSerializerTests(TestCase):
...@@ -157,3 +158,33 @@ class CourseStateSerializerTests(TestCase): ...@@ -157,3 +158,33 @@ class CourseStateSerializerTests(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
serializer.update(self.course_state, data) serializer.update(self.course_state, data)
class CourseRunStateSerializerTests(TestCase):
serializer_class = CourseRunStateSerializer
def setUp(self):
super(CourseRunStateSerializerTests, self).setUp()
self.run_state = CourseRunStateFactory(name=CourseRunStateChoices.Draft)
def test_update(self):
"""
Verify that we can update course-run workflow state with serializer.
"""
self.assertNotEqual(self.run_state, CourseRunStateChoices.Review)
serializer = self.serializer_class(self.run_state)
data = {'name': CourseRunStateChoices.Review}
serializer.update(self.run_state, data)
self.run_state = CourseRunState.objects.get(course_run=self.run_state.course_run)
self.assertEqual(self.run_state.name, CourseRunStateChoices.Review)
def test_update_with_error(self):
"""
Verify that serializer raises `ValidationError` with wrong transition.
"""
serializer = self.serializer_class(self.run_state)
data = {'name': CourseRunStateChoices.Published}
with self.assertRaises(ValidationError):
serializer.update(self.run_state, data)
...@@ -14,9 +14,9 @@ from guardian.shortcuts import assign_perm ...@@ -14,9 +14,9 @@ from guardian.shortcuts import assign_perm
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.course_metadata.tests import toggle_switch from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.publisher.choices import PublisherUserRole, CourseStateChoices from course_discovery.apps.publisher.choices import PublisherUserRole, CourseStateChoices, CourseRunStateChoices
from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.models import CourseRun, OrganizationExtension, CourseState from course_discovery.apps.publisher.models import CourseRun, CourseState, CourseRunState, OrganizationExtension
from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE
...@@ -424,3 +424,54 @@ class ChangeCourseStateViewTests(TestCase): ...@@ -424,3 +424,54 @@ class ChangeCourseStateViewTests(TestCase):
} }
self.assertEqual(response.data, expected) self.assertEqual(response.data, expected)
class ChangeCourseRunStateViewTests(TestCase):
def setUp(self):
super(ChangeCourseRunStateViewTests, self).setUp()
self.run_state = factories.CourseRunStateFactory(name=CourseRunStateChoices.Draft)
self.user = UserFactory()
self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
self.change_state_url = reverse('publisher:api:change_course_run_state', kwargs={'pk': self.run_state.id})
self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_change_course_run_state(self):
"""
Verify that publisher user can change course-run workflow state.
"""
self.assertNotEqual(self.run_state.name, CourseRunStateChoices.Review)
response = self.client.patch(
self.change_state_url,
data=json.dumps({'name': CourseRunStateChoices.Review}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 200)
self.run_state = CourseRunState.objects.get(course_run=self.run_state.course_run)
self.assertEqual(self.run_state.name, CourseRunStateChoices.Review)
def test_change_course_run_state_with_error(self):
"""
Verify that user cannot change course-run workflow state directly from `Draft` to `Published`.
"""
response = self.client.patch(
self.change_state_url,
data=json.dumps({'name': CourseRunStateChoices.Published}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 400)
expected = {
'name': 'Cannot switch from state `{state}` to `{target_state}`'.format(
state=self.run_state.name, target_state=CourseRunStateChoices.Published
)
}
self.assertEqual(response.data, expected)
...@@ -3,7 +3,7 @@ from django.conf.urls import url ...@@ -3,7 +3,7 @@ from django.conf.urls import url
from course_discovery.apps.publisher.api.views import ( from course_discovery.apps.publisher.api.views import (
CourseRoleAssignmentView, OrganizationGroupUserView, UpdateCourseKeyView, CourseRevisionDetailView, CourseRoleAssignmentView, OrganizationGroupUserView, UpdateCourseKeyView, CourseRevisionDetailView,
ChangeCourseStateView ChangeCourseStateView, ChangeCourseRunStateView
) )
urlpatterns = [ urlpatterns = [
...@@ -13,4 +13,5 @@ urlpatterns = [ ...@@ -13,4 +13,5 @@ urlpatterns = [
url(r'^course_state/(?P<pk>\d+)/$', ChangeCourseStateView.as_view(), name='change_course_state'), url(r'^course_state/(?P<pk>\d+)/$', ChangeCourseStateView.as_view(), name='change_course_state'),
url(r'^course_runs/(?P<pk>\d+)/$', UpdateCourseKeyView.as_view(), name='update_course_key'), url(r'^course_runs/(?P<pk>\d+)/$', UpdateCourseKeyView.as_view(), name='update_course_key'),
url(r'^course_revisions/(?P<history_id>\d+)/$', CourseRevisionDetailView.as_view(), name='course_revisions'), url(r'^course_revisions/(?P<history_id>\d+)/$', CourseRevisionDetailView.as_view(), name='course_revisions'),
url(r'^course_run_state/(?P<pk>\d+)/$', ChangeCourseRunStateView.as_view(), name='change_course_run_state'),
] ]
...@@ -7,9 +7,11 @@ from course_discovery.apps.publisher.api.permissions import ( ...@@ -7,9 +7,11 @@ from course_discovery.apps.publisher.api.permissions import (
) )
from course_discovery.apps.publisher.api.serializers import ( from course_discovery.apps.publisher.api.serializers import (
CourseUserRoleSerializer, GroupUserSerializer, UpdateCourseKeySerializer, CourseRevisionSerializer, CourseUserRoleSerializer, GroupUserSerializer, UpdateCourseKeySerializer, CourseRevisionSerializer,
CourseStateSerializer CourseStateSerializer, CourseRunStateSerializer
)
from course_discovery.apps.publisher.models import (
Course, CourseState, CourseRun, CourseRunState, CourseUserRole, OrganizationExtension
) )
from course_discovery.apps.publisher.models import CourseUserRole, OrganizationExtension, CourseRun, CourseState, Course
class CourseRoleAssignmentView(UpdateAPIView): class CourseRoleAssignmentView(UpdateAPIView):
...@@ -45,3 +47,9 @@ class ChangeCourseStateView(UpdateAPIView): ...@@ -45,3 +47,9 @@ class ChangeCourseStateView(UpdateAPIView):
permission_classes = (IsAuthenticated, PublisherUserPermission,) permission_classes = (IsAuthenticated, PublisherUserPermission,)
queryset = CourseState.objects.all() queryset = CourseState.objects.all()
serializer_class = CourseStateSerializer serializer_class = CourseStateSerializer
class ChangeCourseRunStateView(UpdateAPIView):
permission_classes = (IsAuthenticated, PublisherUserPermission,)
queryset = CourseRunState.objects.all()
serializer_class = CourseRunStateSerializer
...@@ -552,3 +552,15 @@ class CourseRunState(TimeStampedModel, ChangedByMixin): ...@@ -552,3 +552,15 @@ class CourseRunState(TimeStampedModel, ChangedByMixin):
def published(self): def published(self):
# TODO: send email etc. # TODO: send email etc.
pass pass
def change_state(self, state):
if state == CourseRunStateChoices.Draft:
self.draft()
elif state == CourseRunStateChoices.Review:
self.review()
elif state == CourseRunStateChoices.Approved:
self.approved()
elif state == CourseRunStateChoices.Published:
self.published()
self.save()
...@@ -439,12 +439,14 @@ class CourseStateTests(TestCase): ...@@ -439,12 +439,14 @@ class CourseStateTests(TestCase):
self.assertEqual(self.course_state.name, state) self.assertEqual(self.course_state.name, state)
@ddt.ddt
class CourseRunStateTests(TestCase): class CourseRunStateTests(TestCase):
""" Tests for the publisher `CourseRunState` model. """ """ Tests for the publisher `CourseRunState` model. """
def setUp(self): @classmethod
super(CourseRunStateTests, self).setUp() def setUpClass(cls):
self.run_state = factories.CourseRunStateFactory(name=CourseRunStateChoices.Draft) super(CourseRunStateTests, cls).setUpClass()
cls.run_state = factories.CourseRunStateFactory(name=CourseRunStateChoices.Draft)
def test_str(self): def test_str(self):
""" """
...@@ -452,37 +454,18 @@ class CourseRunStateTests(TestCase): ...@@ -452,37 +454,18 @@ class CourseRunStateTests(TestCase):
""" """
self.assertEqual(str(self.run_state), self.run_state.get_name_display()) self.assertEqual(str(self.run_state), self.run_state.get_name_display())
def test_draft(self): @ddt.data(
self.run_state.review() CourseRunStateChoices.Review,
CourseRunStateChoices.Approved,
self.assertNotEqual(self.run_state.name, CourseRunStateChoices.Draft) CourseRunStateChoices.Published,
CourseRunStateChoices.Draft
self.run_state.draft() )
def test_change_state(self, state):
self.assertEqual(self.run_state.name, CourseRunStateChoices.Draft) """
Verify that we can change course-run state according to workflow.
def test_review(self): """
self.assertNotEqual(self.run_state.name, CourseRunStateChoices.Review) self.assertNotEqual(self.run_state.name, state)
self.run_state.review()
self.assertEqual(self.run_state.name, CourseRunStateChoices.Review)
def test_approved(self):
self.run_state.review()
self.assertNotEqual(self.run_state.name, CourseRunStateChoices.Approved)
self.run_state.approved()
self.assertEqual(self.run_state.name, CourseRunStateChoices.Approved)
def test_published(self):
self.run_state.name = CourseRunStateChoices.Approved
self.run_state.save()
self.assertNotEqual(self.run_state.name, CourseRunStateChoices.Published)
self.run_state.published() self.run_state.change_state(state=state)
self.assertEqual(self.run_state.name, CourseRunStateChoices.Published) self.assertEqual(self.run_state.name, state)
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-03 13:07+0500\n" "POT-Creation-Date: 2017-02-03 14:54+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-03 13:07+0500\n" "POT-Creation-Date: 2017-02-03 14:54+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-03 13:07+0500\n" "POT-Creation-Date: 2017-02-03 14:54+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-03 13:07+0500\n" "POT-Creation-Date: 2017-02-03 14:54+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
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