Commit 9d3a782b by Waheed Ahmed

Fixed/Moved UpdateCourseKeyView to API directory.

ECOM-6692
parent 17b1f519
"""Publisher API Serializers""" """Publisher API Serializers"""
import waffle
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import serializers 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.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): class CourseUserRoleSerializer(serializers.ModelSerializer):
...@@ -30,3 +33,36 @@ class GroupUserSerializer(serializers.ModelSerializer): ...@@ -30,3 +33,36 @@ class GroupUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ('id', 'full_name', ) 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 @@ ...@@ -2,10 +2,13 @@
from unittest import TestCase from unittest import TestCase
from django.test import RequestFactory from django.test import RequestFactory
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 CourseUserRoleSerializer, GroupUserSerializer from course_discovery.apps.publisher.api.serializers import (
from course_discovery.apps.publisher.tests.factories import CourseUserRoleFactory CourseUserRoleSerializer, GroupUserSerializer, UpdateCourseKeySerializer
)
from course_discovery.apps.publisher.tests.factories import CourseUserRoleFactory, CourseRunFactory
class CourseUserRoleSerializerTests(TestCase): class CourseUserRoleSerializerTests(TestCase):
...@@ -39,3 +42,34 @@ class GroupUserSerializerTests(TestCase): ...@@ -39,3 +42,34 @@ class GroupUserSerializerTests(TestCase):
serializer = GroupUserSerializer(user) serializer = GroupUserSerializer(user)
self.assertDictEqual(serializer.data, {'id': user.id, 'full_name': user.full_name}) 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 @@ ...@@ -2,13 +2,18 @@
import json import json
import ddt import ddt
from django.conf import settings
from django.contrib.auth.models import Group 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.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
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.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
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
from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE from course_discovery.apps.publisher.tests import factories, JSON_CONTENT_TYPE
...@@ -137,3 +142,90 @@ class OrganizationGroupUserViewTests(TestCase): ...@@ -137,3 +142,90 @@ class OrganizationGroupUserViewTests(TestCase):
return reverse( return reverse(
'publisher:api:organization_group_users', kwargs={'pk': org_id} '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. """ """ Publisher API URLs. """
from django.conf.urls import url 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 = [ urlpatterns = [
url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'), 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(), url(r'^admins/organizations/(?P<pk>\d+)/users/$', OrganizationGroupUserView.as_view(),
name='organization_group_users'), 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 ...@@ -2,9 +2,11 @@ from rest_framework.generics import UpdateAPIView, ListAPIView, get_object_or_40
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from course_discovery.apps.core.models import User 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.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): class CourseRoleAssignmentView(UpdateAPIView):
...@@ -21,3 +23,9 @@ class OrganizationGroupUserView(ListAPIView): ...@@ -21,3 +23,9 @@ class OrganizationGroupUserView(ListAPIView):
org_extension = get_object_or_404(OrganizationExtension, organization=self.kwargs.get('pk')) org_extension = get_object_or_404(OrganizationExtension, organization=self.kwargs.get('pk'))
queryset = User.objects.filter(groups__name=org_extension.group) queryset = User.objects.filter(groups__name=org_extension.group)
return queryset 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 ...@@ -10,7 +10,6 @@ from django.db import IntegrityError
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import mail
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.forms import model_to_dict from django.forms import model_to_dict
from django.test import TestCase from django.test import TestCase
...@@ -20,14 +19,13 @@ from testfixtures import LogCapture ...@@ -20,14 +19,13 @@ from testfixtures import LogCapture
from course_discovery.apps.core.models import User 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.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.helpers import make_image_file 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.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.constants import ( from course_discovery.apps.publisher.constants import (
INTERNAL_USER_GROUP_NAME, ADMIN_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME 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.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.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.utils import is_email_notification_enabled, get_internal_users
from course_discovery.apps.publisher.views import ( from course_discovery.apps.publisher.views import (
...@@ -1346,89 +1344,6 @@ class ToggleEmailNotificationTests(TestCase): ...@@ -1346,89 +1344,6 @@ class ToggleEmailNotificationTests(TestCase):
self.assertEqual(is_email_notification_enabled(user), is_enabled) 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): class CourseListViewTests(TestCase):
""" Tests for `CourseListView` """ """ Tests for `CourseListView` """
......
...@@ -14,7 +14,6 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -14,7 +14,6 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View, CreateView, UpdateView, DetailView, ListView from django.views.generic import View, CreateView, UpdateView, DetailView, ListView
from django_fsm import TransitionNotAllowed from django_fsm import TransitionNotAllowed
from guardian.shortcuts import get_objects_for_user 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.core.models import User
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
...@@ -26,7 +25,6 @@ from course_discovery.apps.publisher import mixins ...@@ -26,7 +25,6 @@ from course_discovery.apps.publisher import mixins
from course_discovery.apps.publisher.models import ( from course_discovery.apps.publisher.models import (
Course, CourseRun, Seat, State, UserAttributes, Course, CourseRun, Seat, State, UserAttributes,
OrganizationExtension, CourseUserRole) OrganizationExtension, CourseUserRole)
from course_discovery.apps.publisher.serializers import UpdateCourseKeySerializer
from course_discovery.apps.publisher.utils import ( from course_discovery.apps.publisher.utils import (
is_internal_user, get_internal_users, is_publisher_admin, is_internal_user, get_internal_users, is_publisher_admin,
is_partner_coordinator_user is_partner_coordinator_user
...@@ -149,9 +147,6 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin, ...@@ -149,9 +147,6 @@ class CourseRunDetailView(mixins.LoginRequiredMixin, mixins.ViewPermissionMixin,
return context return context
def patch(self, *args, **kwargs):
return UpdateCourseKeyView.as_view()(self.request, *args, **kwargs)
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
class CreateCourseView(mixins.LoginRequiredMixin, CreateView): class CreateCourseView(mixins.LoginRequiredMixin, CreateView):
...@@ -430,11 +425,6 @@ class ToggleEmailNotification(mixins.LoginRequiredMixin, View): ...@@ -430,11 +425,6 @@ class ToggleEmailNotification(mixins.LoginRequiredMixin, View):
return JsonResponse({'is_enabled': is_enabled}) 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): class CourseListView(mixins.LoginRequiredMixin, ListView):
""" Course List View.""" """ Course List View."""
template_name = 'publisher/courses.html' template_name = 'publisher/courses.html'
......
...@@ -13,7 +13,7 @@ $(document).ready(function() { ...@@ -13,7 +13,7 @@ $(document).ready(function() {
return; return;
} }
var courseRunPageURL = $(this).data('courseRunUrl'), var updateCourseKeyURL = $(this).data('update-course-key-url'),
courseKeyValue = courseKeyInput.val().trim(), courseKeyValue = courseKeyInput.val().trim(),
courseTitleTag = $courseRunParentTag.find("#course-title").html().trim(), courseTitleTag = $courseRunParentTag.find("#course-title").html().trim(),
startDateTag = $courseRunParentTag.find("#course-start").html().trim(), startDateTag = $courseRunParentTag.find("#course-start").html().trim(),
...@@ -29,7 +29,7 @@ $(document).ready(function() { ...@@ -29,7 +29,7 @@ $(document).ready(function() {
e.preventDefault(); e.preventDefault();
$.ajax({ $.ajax({
url: courseRunPageURL, url: updateCourseKeyURL,
type: "PATCH", type: "PATCH",
data: JSON.stringify({lms_course_id: courseKeyValue}), data: JSON.stringify({lms_course_id: courseKeyValue}),
contentType: "application/json", contentType: "application/json",
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
</td> </td>
<td class="form-group"> <td class="form-group">
<input type="text" class="field-input input-text small" aria-labelledby="course-title-{{ course_run.title }} column-title" /> <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> </td>
</tr> </tr>
{% endfor %} {% 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