diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_delete_orphans.py b/cms/djangoapps/contentstore/management/commands/tests/test_delete_orphans.py index 649c4e7fb0..9f38a85438 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_delete_orphans.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_delete_orphans.py @@ -1,40 +1,111 @@ """Tests running the delete_orphan command""" +import ddt from django.core.management import call_command from contentstore.tests.test_orphan import TestOrphanBase +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore import ModuleStoreEnum + +@ddt.ddt class TestDeleteOrphan(TestOrphanBase): """ Tests for running the delete_orphan management command. Inherits from TestOrphan in order to use its setUp method. """ - def setUp(self): - super(TestDeleteOrphan, self).setUp() - self.course_id = self.course.id.to_deprecated_string() - - def test_delete_orphans_no_commit(self): + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) + def test_delete_orphans_no_commit(self, default_store): """ Tests that running the command without a 'commit' argument results in no orphans being deleted """ - call_command('delete_orphans', self.course_id) - self.assertTrue(self.store.has_item(self.course.id.make_usage_key('html', 'multi_parent_html'))) - self.assertTrue(self.store.has_item(self.course.id.make_usage_key('vertical', 'OrphanVert'))) - self.assertTrue(self.store.has_item(self.course.id.make_usage_key('chapter', 'OrphanChapter'))) - self.assertTrue(self.store.has_item(self.course.id.make_usage_key('html', 'OrphanHtml'))) + course = self.create_course_with_orphans(default_store) + call_command('delete_orphans', unicode(course.id)) + self.assertTrue(self.store.has_item(course.id.make_usage_key('html', 'multi_parent_html'))) + self.assertTrue(self.store.has_item(course.id.make_usage_key('vertical', 'OrphanVert'))) + self.assertTrue(self.store.has_item(course.id.make_usage_key('chapter', 'OrphanChapter'))) + self.assertTrue(self.store.has_item(course.id.make_usage_key('html', 'OrphanHtml'))) - def test_delete_orphans_commit(self): + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) + def test_delete_orphans_commit(self, default_store): """ Tests that running the command WITH the 'commit' argument results in the orphans being deleted """ - call_command('delete_orphans', self.course_id, 'commit') + course = self.create_course_with_orphans(default_store) + + call_command('delete_orphans', unicode(course.id), 'commit') # make sure this module wasn't deleted - self.assertTrue(self.store.has_item(self.course.id.make_usage_key('html', 'multi_parent_html'))) + self.assertTrue(self.store.has_item(course.id.make_usage_key('html', 'multi_parent_html'))) # and make sure that these were - self.assertFalse(self.store.has_item(self.course.id.make_usage_key('vertical', 'OrphanVert'))) - self.assertFalse(self.store.has_item(self.course.id.make_usage_key('chapter', 'OrphanChapter'))) - self.assertFalse(self.store.has_item(self.course.id.make_usage_key('html', 'OrphanHtml'))) + self.assertFalse(self.store.has_item(course.id.make_usage_key('vertical', 'OrphanVert'))) + self.assertFalse(self.store.has_item(course.id.make_usage_key('chapter', 'OrphanChapter'))) + self.assertFalse(self.store.has_item(course.id.make_usage_key('html', 'OrphanHtml'))) + + def test_delete_orphans_published_branch_split(self): + """ + Tests that if there are orphans only on the published branch, + running delete orphans with a course key that specifies + the published branch will delete the published orphan + """ + course, orphan = self.create_split_course_with_published_orphan() + published_branch = course.id.for_branch(ModuleStoreEnum.BranchName.published) + + items_in_published = self.store.get_items(published_branch) + items_in_draft_preferred = self.store.get_items(course.id) + + # call delete orphans, specifying the published branch + # of the course + call_command('delete_orphans', unicode(published_branch), 'commit') + + # now all orphans should be deleted + self.assertOrphanCount(course.id, 0) + self.assertOrphanCount(published_branch, 0) + self.assertNotIn(orphan, self.store.get_items(published_branch)) + # we should have one fewer item in the published branch of the course + self.assertEqual( + len(items_in_published) - 1, + len(self.store.get_items(published_branch)), + ) + # and the same amount of items in the draft branch of the course + self.assertEqual( + len(items_in_draft_preferred), + len(self.store.get_items(course.id)), + ) + + def create_split_course_with_published_orphan(self): + """ + Helper to create a split course with a published orphan + """ + course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) + # create an orphan + orphan = self.store.create_item( + self.user.id, course.id, 'html', "PublishedOnlyOrphan" + ) + self.store.publish(orphan.location, self.user.id) + + # grab the published branch of the course + published_branch = course.id.for_branch( + ModuleStoreEnum.BranchName.published + ) + + # assert that this orphan is present in both branches + self.assertOrphanCount(course.id, 1) + self.assertOrphanCount(published_branch, 1) + + # delete this orphan from the draft branch without + # auto-publishing this change to the published branch + self.store.delete_item( + orphan.location, self.user.id, skip_auto_publish=True + ) + + # now there should be no orphans in the draft branch, but + # there should be one in published + self.assertOrphanCount(course.id, 0) + self.assertOrphanCount(published_branch, 1) + self.assertIn(orphan, self.store.get_items(published_branch)) + + return course, orphan diff --git a/cms/djangoapps/contentstore/tests/test_orphan.py b/cms/djangoapps/contentstore/tests/test_orphan.py index befcec8bab..2a99156d9c 100644 --- a/cms/djangoapps/contentstore/tests/test_orphan.py +++ b/cms/djangoapps/contentstore/tests/test_orphan.py @@ -2,27 +2,34 @@ Test finding orphans via the view and django config """ import json +import ddt from contentstore.tests.utils import CourseTestCase from student.models import CourseEnrollment from contentstore.utils import reverse_course_url +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.tests.factories import CourseFactory class TestOrphanBase(CourseTestCase): """ Base class for Studio tests that require orphaned modules """ - def setUp(self): - super(TestOrphanBase, self).setUp() + def create_course_with_orphans(self, default_store): + """ + Creates a course with 3 orphan modules, one of which + has a child that's also in the course tree. + """ + course = CourseFactory.create(default_store=default_store) # create chapters and add them to course tree - chapter1 = self.store.create_child(self.user.id, self.course.location, 'chapter', "Chapter1") + chapter1 = self.store.create_child(self.user.id, course.location, 'chapter', "Chapter1") self.store.publish(chapter1.location, self.user.id) - chapter2 = self.store.create_child(self.user.id, self.course.location, 'chapter', "Chapter2") + chapter2 = self.store.create_child(self.user.id, course.location, 'chapter', "Chapter2") self.store.publish(chapter2.location, self.user.id) # orphan chapter - orphan_chapter = self.store.create_item(self.user.id, self.course.id, 'chapter', "OrphanChapter") + orphan_chapter = self.store.create_item(self.user.id, course.id, 'chapter', "OrphanChapter") self.store.publish(orphan_chapter.location, self.user.id) # create vertical and add it as child to chapter1 @@ -30,7 +37,7 @@ class TestOrphanBase(CourseTestCase): self.store.publish(vertical1.location, self.user.id) # create orphan vertical - orphan_vertical = self.store.create_item(self.user.id, self.course.id, 'vertical', "OrphanVert") + orphan_vertical = self.store.create_item(self.user.id, course.id, 'vertical', "OrphanVert") self.store.publish(orphan_vertical.location, self.user.id) # create component and add it to vertical1 @@ -45,61 +52,79 @@ class TestOrphanBase(CourseTestCase): self.store.update_item(orphan_vertical, self.user.id) # create an orphaned html module - orphan_html = self.store.create_item(self.user.id, self.course.id, 'html', "OrphanHtml") + orphan_html = self.store.create_item(self.user.id, course.id, 'html', "OrphanHtml") self.store.publish(orphan_html.location, self.user.id) - self.store.create_child(self.user.id, self.course.location, 'static_tab', "staticuno") - self.store.create_child(self.user.id, self.course.location, 'about', "overview") - self.store.create_child(self.user.id, self.course.location, 'course_info', "updates") + self.store.create_child(self.user.id, course.location, 'static_tab', "staticuno") + self.store.create_child(self.user.id, course.location, 'course_info', "updates") + + return course + + def assertOrphanCount(self, course_key, number): + """ + Asserts that we have the expected count of orphans + for a given course_key + """ + self.assertEqual(len(self.store.get_orphans(course_key)), number) +@ddt.ddt class TestOrphan(TestOrphanBase): """ Test finding orphans via view and django config """ - def setUp(self): - super(TestOrphan, self).setUp() - self.orphan_url = reverse_course_url('orphan_handler', self.course.id) - def test_mongo_orphan(self): + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) + def test_get_orphans(self, default_store): """ - Test that old mongo finds the orphans + Test that the orphan handler finds the orphans """ + course = self.create_course_with_orphans(default_store) + orphan_url = reverse_course_url('orphan_handler', course.id) + orphans = json.loads( self.client.get( - self.orphan_url, + orphan_url, HTTP_ACCEPT='application/json' ).content ) self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans)) - location = self.course.location.replace(category='chapter', name='OrphanChapter') + location = course.location.replace(category='chapter', name='OrphanChapter') self.assertIn(location.to_deprecated_string(), orphans) - location = self.course.location.replace(category='vertical', name='OrphanVert') + location = course.location.replace(category='vertical', name='OrphanVert') self.assertIn(location.to_deprecated_string(), orphans) - location = self.course.location.replace(category='html', name='OrphanHtml') + location = course.location.replace(category='html', name='OrphanHtml') self.assertIn(location.to_deprecated_string(), orphans) - def test_mongo_orphan_delete(self): + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) + def test_delete_orphans(self, default_store): """ - Test that old mongo deletes the orphans + Test that the orphan handler deletes the orphans """ - self.client.delete(self.orphan_url) + course = self.create_course_with_orphans(default_store) + orphan_url = reverse_course_url('orphan_handler', course.id) + + self.client.delete(orphan_url) orphans = json.loads( - self.client.get(self.orphan_url, HTTP_ACCEPT='application/json').content + self.client.get(orphan_url, HTTP_ACCEPT='application/json').content ) self.assertEqual(len(orphans), 0, "Orphans not deleted {}".format(orphans)) # make sure that any children with one orphan parent and one non-orphan # parent are not deleted - self.assertTrue(self.store.has_item(self.course.id.make_usage_key('html', "multi_parent_html"))) + self.assertTrue(self.store.has_item(course.id.make_usage_key('html', "multi_parent_html"))) - def test_not_permitted(self): + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) + def test_not_permitted(self, default_store): """ Test that auth restricts get and delete appropriately """ + course = self.create_course_with_orphans(default_store) + orphan_url = reverse_course_url('orphan_handler', course.id) + test_user_client, test_user = self.create_non_staff_authed_user_client() - CourseEnrollment.enroll(test_user, self.course.id) - response = test_user_client.get(self.orphan_url) + CourseEnrollment.enroll(test_user, course.id) + response = test_user_client.get(orphan_url) self.assertEqual(response.status_code, 403) - response = test_user_client.delete(self.orphan_url) + response = test_user_client.delete(orphan_url) self.assertEqual(response.status_code, 403) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index debc327ab5..d1283ece1b 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -702,10 +702,14 @@ def _delete_orphans(course_usage_key, user_id, commit=False): """ store = modulestore() items = store.get_orphans(course_usage_key) + branch = course_usage_key.branch if commit: for itemloc in items: - # need to delete all versions - store.delete_item(itemloc, user_id, revision=ModuleStoreEnum.RevisionOption.all) + revision = ModuleStoreEnum.RevisionOption.all + # specify branches when deleting orphans + if branch == ModuleStoreEnum.BranchName.published: + revision = ModuleStoreEnum.RevisionOption.published_only + store.delete_item(itemloc, user_id, revision=revision) return [unicode(item) for item in items] diff --git a/cms/envs/common.py b/cms/envs/common.py index f1d34953a1..304ac08b1a 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -557,7 +557,6 @@ PIPELINE_JS_COMPRESSOR = None STATICFILES_IGNORE_PATTERNS = ( "*.py", "*.pyc", - "*.html", # It would be nice if we could do, for example, "**/*.scss", # but these strings get passed down to the `fnmatch` module, diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index d76823e9bb..e92657f956 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -126,6 +126,16 @@ class CourseMode(models.Model): self.currency = self.currency.lower() super(CourseMode, self).save(force_insert, force_update, using) + @property + def slug(self): + """ + Returns mode_slug + + NOTE (CCB): This is a silly hack needed because all of the class methods use tuples + with a property named slug instead of mode_slug. + """ + return self.mode_slug + @classmethod def all_modes_for_courses(cls, course_id_list): """Find all modes for a list of course IDs, including expired modes. diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 379f5f04ba..9fed0c0f9e 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -2416,14 +2416,36 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): return result - @contract(block_key=BlockKey, blocks='dict(BlockKey: BlockData)') - def _remove_subtree(self, block_key, blocks): + @contract(root_block_key=BlockKey, blocks='dict(BlockKey: BlockData)') + def _remove_subtree(self, root_block_key, blocks): """ - Remove the subtree rooted at block_key + Remove the subtree rooted at root_block_key + We do this breadth-first to make sure that we don't remove + any children that may have parents that we don't want to delete. """ - for child in blocks[block_key].fields.get('children', []): - self._remove_subtree(BlockKey(*child), blocks) - del blocks[block_key] + # create mapping from each child's key to its parents' keys + child_parent_map = defaultdict(set) + for block_key, block_data in blocks.iteritems(): + for child in block_data.fields.get('children', []): + child_parent_map[BlockKey(*child)].add(block_key) + + to_delete = {root_block_key} + tier = {root_block_key} + while tier: + next_tier = set() + for block_key in tier: + for child in blocks[block_key].fields.get('children', []): + child_block_key = BlockKey(*child) + parents = child_parent_map[child_block_key] + # Make sure we want to delete all of the child's parents + # before slating it for deletion + if parents.issubset(to_delete): + next_tier.add(child_block_key) + tier = next_tier + to_delete.update(tier) + + for block_key in to_delete: + del blocks[block_key] def delete_course(self, course_key, user_id): """ diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py index 11a50a717e..4dcb0f4cff 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py @@ -175,7 +175,7 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli self._auto_publish_no_children(parent_usage_key, item.location.category, user_id, **kwargs) return item - def delete_item(self, location, user_id, revision=None, **kwargs): + def delete_item(self, location, user_id, revision=None, skip_auto_publish=False, **kwargs): """ Delete the given item from persistence. kwargs allow modulestore specific parameters. @@ -217,7 +217,8 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli if ( branch == ModuleStoreEnum.BranchName.draft and branched_location.block_type in (DIRECT_ONLY_CATEGORIES + ['vertical']) and - parent_loc + parent_loc and + not skip_auto_publish ): # will publish if its not an orphan self.publish(parent_loc.version_agnostic(), user_id, blacklist=EXCLUDE_ALL, **kwargs) diff --git a/common/test/acceptance/tests/lms/test_teams.py b/common/test/acceptance/tests/lms/test_teams.py index db7ad3de81..40d6dc963d 100644 --- a/common/test/acceptance/tests/lms/test_teams.py +++ b/common/test/acceptance/tests/lms/test_teams.py @@ -8,9 +8,10 @@ import time from dateutil.parser import parse import ddt from nose.plugins.attrib import attr +from selenium.common.exceptions import TimeoutException from uuid import uuid4 -from ..helpers import EventsTestMixin, UniqueCourseTest +from ..helpers import get_modal_alert, EventsTestMixin, UniqueCourseTest from ...fixtures import LMS_BASE_URL from ...fixtures.course import CourseFixture from ...fixtures.discussion import ( @@ -60,18 +61,23 @@ class TeamsTabBase(EventsTestMixin, UniqueCourseTest): 'language': 'aa', 'country': 'AF' } - response = self.course_fixture.session.post( - LMS_BASE_URL + '/api/team/v0/teams/', - data=json.dumps(team), - headers=self.course_fixture.headers - ) + teams.append(self.post_team_data(team)) # Sadly, this sleep is necessary in order to ensure that # sorting by last_activity_at works correctly when running # in Jenkins. time.sleep(time_between_creation) - teams.append(json.loads(response.text)) return teams + def post_team_data(self, team_data): + """Given a JSON representation of a team, post it to the server.""" + response = self.course_fixture.session.post( + LMS_BASE_URL + '/api/team/v0/teams/', + data=json.dumps(team_data), + headers=self.course_fixture.headers + ) + self.assertEqual(response.status_code, 200) + return json.loads(response.text) + def create_membership(self, username, team_id): """Assign `username` to `team_id`.""" response = self.course_fixture.session.post( @@ -838,6 +844,26 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): with self.assert_events_match_during(self.only_team_events, expected_events=events): self.browse_teams_page.visit() + def test_team_name_xss(self): + """ + Scenario: Team names should be HTML-escaped on the teams page + Given I am enrolled in a course with teams enabled + When I visit the Teams page for a topic, with a team name containing JS code + Then I should not see any alerts + """ + self.post_team_data({ + 'course_id': self.course_id, + 'topic_id': self.topic['id'], + 'name': '', + 'description': 'Description', + 'language': 'aa', + 'country': 'AF' + }) + with self.assertRaises(TimeoutException): + self.browser.get(self.browse_teams_page.url) + alert = get_modal_alert(self.browser) + alert.accept() + @attr('shard_5') class TeamFormActions(TeamsTabBase): diff --git a/lms/djangoapps/teams/static/teams/js/views/team_card.js b/lms/djangoapps/teams/static/teams/js/views/team_card.js index b9561e0bef..770a112814 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_card.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_card.js @@ -135,7 +135,7 @@ actionContent: function() { return interpolate( gettext('View %(span_start)s %(team_name)s %(span_end)s'), - {span_start: '', team_name: this.teamModel().get('name'), span_end: ''}, + {span_start: '', team_name: _.escape(this.teamModel().get('name')), span_end: ''}, true ); }, diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index ea89f8828c..819d7ecac5 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -766,13 +766,22 @@ def create_order(request): return HttpResponseBadRequest(_("Selected price is not valid number.")) current_mode = None - paid_modes = CourseMode.paid_modes_for_course(course_id) - # Check if there are more than 1 paid(mode with min_price>0 e.g verified/professional/no-id-professional) modes - # for course exist then choose the first one - if paid_modes: - if len(paid_modes) > 1: - log.warn(u"Multiple paid course modes found for course '%s' for create order request", course_id) - current_mode = paid_modes[0] + sku = request.POST.get('sku', None) + + if sku: + try: + current_mode = CourseMode.objects.get(sku=sku) + except CourseMode.DoesNotExist: + log.exception(u'Failed to find CourseMode with SKU [%s].', sku) + + if not current_mode: + # Check if there are more than 1 paid(mode with min_price>0 e.g verified/professional/no-id-professional) modes + # for course exist then choose the first one + paid_modes = CourseMode.paid_modes_for_course(course_id) + if paid_modes: + if len(paid_modes) > 1: + log.warn(u"Multiple paid course modes found for course '%s' for create order request", course_id) + current_mode = paid_modes[0] # Make sure this course has a paid mode if not current_mode: diff --git a/lms/envs/common.py b/lms/envs/common.py index 41924c2217..5d36d87ff5 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1622,7 +1622,6 @@ if os.path.isdir(DATA_DIR): STATICFILES_IGNORE_PATTERNS = ( "*.py", "*.pyc", - "*.html", # It would be nice if we could do, for example, "**/*.scss", # but these strings get passed down to the `fnmatch` module, diff --git a/lms/static/js/spec/verify_student/make_payment_step_view_spec.js b/lms/static/js/spec/verify_student/make_payment_step_view_spec.js index c63915c780..7e59684588 100644 --- a/lms/static/js/spec/verify_student/make_payment_step_view_spec.js +++ b/lms/static/js/spec/verify_student/make_payment_step_view_spec.js @@ -66,7 +66,8 @@ define([ var params = { contribution: kwargs.amount || "", course_id: kwargs.courseId || "", - processor: kwargs.processor || "" + processor: kwargs.processor || "", + sku: kwargs.sku || "" }; // Click the "go to payment" button diff --git a/lms/static/js/verify_student/pay_and_verify.js b/lms/static/js/verify_student/pay_and_verify.js index 3503afc7db..3e989f80f9 100644 --- a/lms/static/js/verify_student/pay_and_verify.js +++ b/lms/static/js/verify_student/pay_and_verify.js @@ -55,6 +55,7 @@ var edx = edx || {}; ), upgrade: el.data('msg-key') === 'upgrade', minPrice: el.data('course-mode-min-price'), + sku: el.data('course-mode-sku'), contributionAmount: el.data('contribution-amount'), suggestedPrices: _.filter( (el.data('course-mode-suggested-prices').toString()).split(","), diff --git a/lms/static/js/verify_student/views/make_payment_step_view.js b/lms/static/js/verify_student/views/make_payment_step_view.js index 20f56c03a9..9aee49a7b4 100644 --- a/lms/static/js/verify_student/views/make_payment_step_view.js +++ b/lms/static/js/verify_student/views/make_payment_step_view.js @@ -17,6 +17,7 @@ var edx = edx || {}; isActive: true, suggestedPrices: [], minPrice: 0, + sku: '', currency: 'usd', upgrade: false, verificationDeadline: '', @@ -133,7 +134,8 @@ var edx = edx || {}; postData = { 'processor': event.target.id, 'contribution': paymentAmount, - 'course_id': this.stepData.courseKey + 'course_id': this.stepData.courseKey, + 'sku': this.templateContext().sku }; // Disable the payment button to prevent multiple submissions diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 979c5cf956..bb4c32f847 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -263,7 +263,8 @@ @include text-align(center); .provider-logo img { - width: 100px; + max-width: 160px; + margin-bottom: $baseline * 0.5; } .complete-order { diff --git a/lms/templates/verify_student/make_payment_step.underscore b/lms/templates/verify_student/make_payment_step.underscore index 06094a2997..4f572426c1 100644 --- a/lms/templates/verify_student/make_payment_step.underscore +++ b/lms/templates/verify_student/make_payment_step.underscore @@ -100,6 +100,7 @@ <% if ( isActive ) { %>