Commit 9d3a782b by Waheed Ahmed

Fixed/Moved UpdateCourseKeyView to API directory.

ECOM-6692
parent 17b1f519
"""Publisher API Serializers"""
import waffle
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import serializers
from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.models import CourseUserRole
from course_discovery.apps.publisher.emails import send_email_for_studio_instance_created
from course_discovery.apps.publisher.models import CourseUserRole, CourseRun
class CourseUserRoleSerializer(serializers.ModelSerializer):
......@@ -30,3 +33,36 @@ class GroupUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'full_name', )
class UpdateCourseKeySerializer(serializers.ModelSerializer):
"""
Serializer for the `CourseRun` model to update 'lms_course_id'.
"""
class Meta:
model = CourseRun
fields = ('lms_course_id', 'changed_by',)
def validate(self, data):
validated_values = super(UpdateCourseKeySerializer, self).validate(data)
lms_course_id = validated_values.get('lms_course_id')
try:
CourseKey.from_string(lms_course_id)
except InvalidKeyError:
raise serializers.ValidationError('Invalid course key [{}]'.format(lms_course_id))
request = self.context.get('request')
if request:
validated_values.update({'changed_by': request.user})
return validated_values
def update(self, instance, validated_data):
instance = super(UpdateCourseKeySerializer, self).update(instance, validated_data)
if waffle.switch_is_active('enable_publisher_email_notifications'):
send_email_for_studio_instance_created(instance)
return instance
......@@ -2,10 +2,13 @@
from unittest import TestCase
from django.test import RequestFactory
from rest_framework.exceptions import ValidationError
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.api.serializers import CourseUserRoleSerializer, GroupUserSerializer
from course_discovery.apps.publisher.tests.factories import CourseUserRoleFactory
from course_discovery.apps.publisher.api.serializers import (
CourseUserRoleSerializer, GroupUserSerializer, UpdateCourseKeySerializer
)
from course_discovery.apps.publisher.tests.factories import CourseUserRoleFactory, CourseRunFactory
class CourseUserRoleSerializerTests(TestCase):
......@@ -39,3 +42,34 @@ class GroupUserSerializerTests(TestCase):
serializer = GroupUserSerializer(user)
self.assertDictEqual(serializer.data, {'id': user.id, 'full_name': user.full_name})
class UpdateCourseKeySerializerTests(TestCase):
serializer_class = UpdateCourseKeySerializer
def setUp(self):
super(UpdateCourseKeySerializerTests, self).setUp()
self.course_run = CourseRunFactory()
self.request = RequestFactory()
self.user = UserFactory()
self.request.user = self.user
def get_expected_data(self):
return {
'lms_course_id': self.course_run.lms_course_id,
'changed_by': self.user
}
def test_validation(self):
self.course_run.lms_course_id = 'course-v1:edxTest+TC101+2016_Q1'
self.course_run.save() # pylint: disable=no-member
serializer = self.serializer_class(self.course_run, context={'request': self.request})
expected = serializer.validate(serializer.data)
self.assertEqual(self.get_expected_data(), expected)
def test_validation_error(self):
self.course_run.lms_course_id = 'invalid-course-id'
self.course_run.save() # pylint: disable=no-member
serializer = self.serializer_class(self.course_run)
with self.assertRaises(ValidationError):
serializer.validate(serializer.data)
......@@ -2,13 +2,18 @@
import json
import ddt
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.sites.models import Site
from django.core import mail
from django.core.urlresolvers import reverse
from django.test import TestCase
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.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.constants import INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.models import CourseRun
from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE
......@@ -137,3 +142,90 @@ class OrganizationGroupUserViewTests(TestCase):
return reverse(
'publisher:api:organization_group_users', kwargs={'pk': org_id}
)
class UpdateCourseKeyViewTests(TestCase):
def setUp(self):
super(UpdateCourseKeyViewTests, self).setUp()
self.course_run = factories.CourseRunFactory()
self.user = UserFactory()
self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
self.organization_extension = factories.OrganizationExtensionFactory()
self.course_run.course.organizations.add(self.organization_extension.organization)
self.update_course_key_url = reverse(
'publisher:api:update_course_key', kwargs={'pk': self.course_run.id}
)
factories.CourseUserRoleFactory(
role=PublisherUserRole.PartnerCoordinator,
course=self.course_run.course,
user=self.user
)
factories.UserAttributeFactory(user=self.user, enable_email_notification=True)
toggle_switch('enable_publisher_email_notifications', True)
self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_update_course_key_with_errors(self):
"""
Test that api returns error with invalid course key.
"""
invalid_course_id = 'invalid-course-key'
response = self.client.patch(
self.update_course_key_url,
data=json.dumps({'lms_course_id': invalid_course_id}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data.get('non_field_errors'), ['Invalid course key [{}]'.format(invalid_course_id)]
)
def test_update_course_key(self):
"""
Test that internal user can update `lms_course_id` for a course run.
"""
# Verify that `lms_course_id` and `changed_by` are None
self.assert_course_key_and_changed_by()
lms_course_id = 'course-v1:edxTest+TC12+2050Q1'
response = self.client.patch(
self.update_course_key_url,
data=json.dumps({'lms_course_id': lms_course_id}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 200)
# Verify that `lms_course_id` and `changed_by` are not None
self.assert_course_key_and_changed_by(lms_course_id=lms_course_id, changed_by=self.user)
# Assert email sent
self.assert_email_sent(
reverse('publisher:publisher_course_run_detail', kwargs={'pk': self.course_run.id}),
'Studio instance created',
'Studio instance created for the following course run'
)
def assert_course_key_and_changed_by(self, lms_course_id=None, changed_by=None):
self.course_run = CourseRun.objects.get(id=self.course_run.id)
self.assertEqual(self.course_run.lms_course_id, lms_course_id)
self.assertEqual(self.course_run.changed_by, changed_by)
def assert_email_sent(self, object_path, subject, expected_body):
"""
DRY method to assert sent email data.
"""
self.assertEqual(len(mail.outbox), 1)
self.assertEqual([settings.PUBLISHER_FROM_EMAIL], mail.outbox[0].to)
self.assertEqual([self.user.email], mail.outbox[0].bcc)
self.assertEqual(str(mail.outbox[0].subject), subject)
body = mail.outbox[0].body.strip()
self.assertIn(expected_body, body)
page_url = 'https://{host}{path}'.format(host=Site.objects.get_current().domain.strip('/'), path=object_path)
self.assertIn(page_url, body)
""" Publisher API URLs. """
from django.conf.urls import url
from course_discovery.apps.publisher.api.views import CourseRoleAssignmentView, OrganizationGroupUserView
from course_discovery.apps.publisher.api.views import (
CourseRoleAssignmentView, OrganizationGroupUserView, UpdateCourseKeyView
)
urlpatterns = [
url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'),
url(r'^admins/organizations/(?P<pk>\d+)/users/$', OrganizationGroupUserView.as_view(),
name='organization_group_users'),
url(r'^course_runs/(?P<pk>\d+)/$', UpdateCourseKeyView.as_view(), name='update_course_key'),
]
......@@ -2,9 +2,11 @@ from rest_framework.generics import UpdateAPIView, ListAPIView, get_object_or_40
from rest_framework.permissions import IsAuthenticated
from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.models import CourseUserRole, OrganizationExtension
from course_discovery.apps.publisher.models import CourseUserRole, OrganizationExtension, CourseRun
from course_discovery.apps.publisher.api.permissions import CanViewAssociatedCourse, InternalUserPermission
from course_discovery.apps.publisher.api.serializers import CourseUserRoleSerializer, GroupUserSerializer
from course_discovery.apps.publisher.api.serializers import (
CourseUserRoleSerializer, GroupUserSerializer, UpdateCourseKeySerializer
)
class CourseRoleAssignmentView(UpdateAPIView):
......@@ -21,3 +23,9 @@ class OrganizationGroupUserView(ListAPIView):
org_extension = get_object_or_404(OrganizationExtension, organization=self.kwargs.get('pk'))
queryset = User.objects.filter(groups__name=org_extension.group)
return queryset
class UpdateCourseKeyView(UpdateAPIView):
permission_classes = (IsAuthenticated, InternalUserPermission,)
queryset = CourseRun.objects.all()
serializer_class = UpdateCourseKeySerializer
"""Publisher Serializers"""
import waffle
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import serializers
from course_discovery.apps.publisher.emails import send_email_for_studio_instance_created
from course_discovery.apps.publisher.models import CourseRun
class UpdateCourseKeySerializer(serializers.ModelSerializer):
"""Serializer for the `CourseRun` model to update 'lms_course_id'. """
class Meta:
model = CourseRun
fields = ('lms_course_id', 'changed_by',)
def validate(self, data):
validated_values = super(UpdateCourseKeySerializer, self).validate(data)
lms_course_id = validated_values.get('lms_course_id')
try:
CourseKey.from_string(lms_course_id)
except InvalidKeyError:
raise serializers.ValidationError('Invalid course key [{}]'.format(lms_course_id))
request = self.context.get('request')
if request:
validated_values.update({'changed_by': request.user})
return validated_values
def update(self, instance, validated_data):
instance = super(UpdateCourseKeySerializer, self).update(instance, validated_data)
if waffle.switch_is_active('enable_publisher_email_notifications'):
send_email_for_studio_instance_created(instance)
return instance
"""Tests Publisher Serializers."""
from unittest import TestCase
from django.test import RequestFactory
from rest_framework.exceptions import ValidationError
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.serializers import UpdateCourseKeySerializer
from course_discovery.apps.publisher.tests.factories import CourseRunFactory
class UpdateCourseKeySerializerTests(TestCase):
serializer_class = UpdateCourseKeySerializer
def setUp(self):
super(UpdateCourseKeySerializerTests, self).setUp()
self.course_run = CourseRunFactory()
self.request = RequestFactory()
self.user = UserFactory()
self.request.user = self.user
def get_expected_data(self):
return {
'lms_course_id': self.course_run.lms_course_id,
'changed_by': self.user
}
def test_validation(self):
self.course_run.lms_course_id = 'course-v1:edxTest+TC101+2016_Q1'
self.course_run.save() # pylint: disable=no-member
serializer = self.serializer_class(self.course_run, context={'request': self.request})
expected = serializer.validate(serializer.data)
self.assertEqual(self.get_expected_data(), expected)
def test_validation_error(self):
self.course_run.lms_course_id = 'wrong-course-id'
self.course_run.save() # pylint: disable=no-member
serializer = self.serializer_class(self.course_run)
with self.assertRaises(ValidationError):
serializer.validate(serializer.data)
......@@ -10,7 +10,6 @@ from django.db import IntegrityError
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.sites.models import Site
from django.core import mail
from django.core.urlresolvers import reverse
from django.forms import model_to_dict
from django.test import TestCase
......@@ -20,14 +19,13 @@ from testfixtures import LogCapture
from course_discovery.apps.core.models import User
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.constants import (
INTERNAL_USER_GROUP_NAME, ADMIN_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME
)
from course_discovery.apps.publisher.models import Course, CourseRun, Seat, State, OrganizationExtension
from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE
from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.tests.utils import create_non_staff_user_and_login
from course_discovery.apps.publisher.utils import is_email_notification_enabled, get_internal_users
from course_discovery.apps.publisher.views import (
......@@ -1346,89 +1344,6 @@ class ToggleEmailNotificationTests(TestCase):
self.assertEqual(is_email_notification_enabled(user), is_enabled)
class UpdateCourseKeyViewTests(TestCase):
""" Tests for `UpdateCourseKeyView` """
def setUp(self):
super(UpdateCourseKeyViewTests, self).setUp()
self.course_run = factories.CourseRunFactory()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.user.groups.add(Group.objects.get(name=INTERNAL_USER_GROUP_NAME))
self.organization_extension = factories.OrganizationExtensionFactory()
self.course_run.course.organizations.add(self.organization_extension.organization)
self.update_course_key_url = reverse(
'publisher:publisher_course_run_detail', kwargs={'pk': self.course_run.id}
)
# emails send using user course roles
factories.CourseUserRoleFactory(
role=PublisherUserRole.PartnerCoordinator,
course=self.course_run.course,
user=self.user
)
factories.UserAttributeFactory(user=self.user, enable_email_notification=True)
toggle_switch('enable_publisher_email_notifications', True)
self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_update_course_key_with_errors(self):
""" Test that api returns error with invalid course key."""
invalid_course_id = 'invalid-course-key'
response = self.client.patch(
self.update_course_key_url,
data=json.dumps({'lms_course_id': invalid_course_id}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data.get('non_field_errors'), ['Invalid course key [{}]'.format(invalid_course_id)]
)
def test_update_course_key(self):
""" Test that user can update `lms_course_id` for a course run."""
# Verify that `lms_course_id` and `changed_by` are None
self.assert_course_key_and_changed_by()
lms_course_id = 'course-v1:edxTest+TC12+2050Q1'
response = self.client.patch(
self.update_course_key_url,
data=json.dumps({'lms_course_id': lms_course_id}),
content_type=JSON_CONTENT_TYPE
)
self.assertEqual(response.status_code, 200)
# Verify that `lms_course_id` and `changed_by` are not None
self.assert_course_key_and_changed_by(lms_course_id=lms_course_id, changed_by=self.user)
# assert email sent
self.assert_email_sent(
reverse('publisher:publisher_course_run_detail', kwargs={'pk': self.course_run.id}),
'Studio instance created',
'Studio instance created for the following course run'
)
def assert_course_key_and_changed_by(self, lms_course_id=None, changed_by=None):
self.course_run = CourseRun.objects.get(id=self.course_run.id)
self.assertEqual(self.course_run.lms_course_id, lms_course_id)
self.assertEqual(self.course_run.changed_by, changed_by)
def assert_email_sent(self, object_path, subject, expected_body):
""" DRY method to assert sent email data"""
self.assertEqual(len(mail.outbox), 1)
self.assertEqual([settings.PUBLISHER_FROM_EMAIL], mail.outbox[0].to)
self.assertEqual([self.user.email], mail.outbox[0].bcc)
self.assertEqual(str(mail.outbox[0].subject), subject)
body = mail.outbox[0].body.strip()
self.assertIn(expected_body, body)
page_url = 'https://{host}{path}'.format(host=Site.objects.get_current().domain.strip('/'), path=object_path)
self.assertIn(page_url, body)
class CourseListViewTests(TestCase):
""" Tests for `CourseListView` """
......
......@@ -14,7 +14,6 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View, CreateView, UpdateView, DetailView, ListView
from django_fsm import TransitionNotAllowed
from guardian.shortcuts import get_objects_for_user
from rest_framework.generics import UpdateAPIView
from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.choices import PublisherUserRole
......@@ -26,7 +25,6 @@ from course_discovery.apps.publisher import mixins
from course_discovery.apps.publisher.models import (
Course, CourseRun, Seat, State, UserAttributes,
OrganizationExtension, CourseUserRole)
from course_discovery.apps.publisher.serializers import UpdateCourseKeySerializer
from course_discovery.apps.publisher.utils import (
is_internal_user, get_internal_users, is_publisher_admin,
is_partner_coordinator_user
......@@ -149,9 +147,6 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin,
return context
def patch(self, *args, **kwargs):
return UpdateCourseKeyView.as_view()(self.request, *args, **kwargs)
# pylint: disable=attribute-defined-outside-init
class CreateCourseView(mixins.LoginRequiredMixin, CreateView):
......@@ -430,11 +425,6 @@ class ToggleEmailNotification(mixins.LoginRequiredMixin, View):
return JsonResponse({'is_enabled': is_enabled})
class UpdateCourseKeyView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, UpdateAPIView):
queryset = CourseRun.objects.all()
serializer_class = UpdateCourseKeySerializer
class CourseListView(mixins.LoginRequiredMixin, ListView):
""" Course List View."""
template_name = 'publisher/courses.html'
......
......@@ -13,7 +13,7 @@ $(document).ready(function() {
return;
}
var courseRunPageURL = $(this).data('courseRunUrl'),
var updateCourseKeyURL = $(this).data('update-course-key-url'),
courseKeyValue = courseKeyInput.val().trim(),
courseTitleTag = $courseRunParentTag.find("#course-title").html().trim(),
startDateTag = $courseRunParentTag.find("#course-start").html().trim(),
......@@ -29,7 +29,7 @@ $(document).ready(function() {
e.preventDefault();
$.ajax({
url: courseRunPageURL,
url: updateCourseKeyURL,
type: "PATCH",
data: JSON.stringify({lms_course_id: courseKeyValue}),
contentType: "application/json",
......
......@@ -44,7 +44,7 @@
</td>
<td class="form-group">
<input type="text" class="field-input input-text small" aria-labelledby="course-title-{{ course_run.title }} column-title" />
<button data-course-run-url="{{ run_page_url }}" class="btn-inline btn-add-course-key">{% trans "Add" %}</button>
<button data-update-course-key-url="{% url 'publisher:api:update_course_key' course_run.id %}" class="btn-inline btn-add-course-key">{% trans "Add" %}</button>
</td>
</tr>
{% endfor %}
......
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