@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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': '<script>alert("XSS")</script>',
|
||||
'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):
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
actionContent: function() {
|
||||
return interpolate(
|
||||
gettext('View %(span_start)s %(team_name)s %(span_end)s'),
|
||||
{span_start: '<span class="sr">', team_name: this.teamModel().get('name'), span_end: '</span>'},
|
||||
{span_start: '<span class="sr">', team_name: _.escape(this.teamModel().get('name')), span_end: '</span>'},
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(","),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -263,7 +263,8 @@
|
||||
@include text-align(center);
|
||||
|
||||
.provider-logo img {
|
||||
width: 100px;
|
||||
max-width: 160px;
|
||||
margin-bottom: $baseline * 0.5;
|
||||
}
|
||||
|
||||
.complete-order {
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
<% if ( isActive ) { %>
|
||||
<div class="payment-buttons nav-wizard is-ready center">
|
||||
<input type="hidden" name="contribution" value="<%- minPrice %>" />
|
||||
<input type="hidden" name="sku" value="<%- sku %>" />
|
||||
<div class="purchase">
|
||||
<p class="product-info"><span class="product-name"></span> <%- gettext( "price" ) %>: <span class="price">$<%- minPrice %> USD</span></p>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@ from verify_student.views import PayAndVerifyView
|
||||
data-course-mode-name='${course_mode.name}'
|
||||
data-course-mode-slug='${course_mode.slug}'
|
||||
data-course-mode-min-price='${course_mode.min_price}'
|
||||
data-course-mode-sku='${course_mode.sku or ''}'
|
||||
data-course-mode-suggested-prices='${course_mode.suggested_prices}'
|
||||
data-course-mode-currency='${course_mode.currency}'
|
||||
data-contribution-amount='${contribution_amount}'
|
||||
|
||||
@@ -162,7 +162,7 @@ def create_credit_request(course_key, provider_id, username):
|
||||
"course_org": "HogwartsX",
|
||||
"course_num": "Potions101",
|
||||
"course_run": "1T2015",
|
||||
"final_grade": 0.95,
|
||||
"final_grade": "0.95",
|
||||
"user_username": "ron",
|
||||
"user_email": "ron@example.com",
|
||||
"user_full_name": "Ron Weasley",
|
||||
@@ -242,13 +242,13 @@ def create_credit_request(course_key, provider_id, username):
|
||||
|
||||
# Retrieve the final grade from the eligibility table
|
||||
try:
|
||||
final_grade = CreditRequirementStatus.objects.get(
|
||||
final_grade = unicode(CreditRequirementStatus.objects.get(
|
||||
username=username,
|
||||
requirement__namespace="grade",
|
||||
requirement__name="grade",
|
||||
requirement__course__course_key=course_key,
|
||||
status="satisfied"
|
||||
).reason["final_grade"]
|
||||
).reason["final_grade"])
|
||||
except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError):
|
||||
log.exception(
|
||||
"Could not retrieve final grade from the credit eligibility table "
|
||||
|
||||
@@ -633,14 +633,10 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
|
||||
self.assertTrue(parsed_date < datetime.datetime.now(pytz.UTC))
|
||||
|
||||
# Validate course information
|
||||
self.assertIn('course_org', parameters)
|
||||
self.assertEqual(parameters['course_org'], self.course_key.org)
|
||||
self.assertIn('course_num', parameters)
|
||||
self.assertEqual(parameters['course_num'], self.course_key.course)
|
||||
self.assertIn('course_run', parameters)
|
||||
self.assertEqual(parameters['course_run'], self.course_key.run)
|
||||
self.assertIn('final_grade', parameters)
|
||||
self.assertEqual(parameters['final_grade'], self.FINAL_GRADE)
|
||||
self.assertEqual(parameters['final_grade'], unicode(self.FINAL_GRADE))
|
||||
|
||||
# Validate user information
|
||||
for key in self.USER_INFO.keys():
|
||||
|
||||
@@ -118,7 +118,7 @@ class CreditProviderViewTests(UrlResetMixin, TestCase):
|
||||
self.assertEqual(content["parameters"]["course_org"], "edX")
|
||||
self.assertEqual(content["parameters"]["course_num"], "DemoX")
|
||||
self.assertEqual(content["parameters"]["course_run"], "Demo_Course")
|
||||
self.assertEqual(content["parameters"]["final_grade"], self.FINAL_GRADE)
|
||||
self.assertEqual(content["parameters"]["final_grade"], unicode(self.FINAL_GRADE))
|
||||
self.assertEqual(content["parameters"]["user_username"], self.USERNAME)
|
||||
self.assertEqual(content["parameters"]["user_full_name"], self.USER_FULL_NAME)
|
||||
self.assertEqual(content["parameters"]["user_mailing_address"], "")
|
||||
|
||||
@@ -111,7 +111,7 @@ def create_credit_request(request, provider_id):
|
||||
course_org: "ASUx"
|
||||
course_num: "DemoX"
|
||||
course_run: "1T2015"
|
||||
final_grade: 0.95,
|
||||
final_grade: "0.95",
|
||||
user_username: "john",
|
||||
user_email: "john@example.com"
|
||||
user_full_name: "John Smith"
|
||||
|
||||
@@ -58,7 +58,7 @@ git+https://github.com/edx/ecommerce-api-client.git@1.1.0#egg=ecommerce-api-clie
|
||||
-e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client
|
||||
-e git+https://github.com/edx/edx-organizations.git@release-2015-08-31#egg=edx-organizations
|
||||
|
||||
git+https://github.com/edx/edx-proctoring.git@0.9.6#egg=edx-proctoring==0.9.6
|
||||
git+https://github.com/edx/edx-proctoring.git@0.9.6b#egg=edx-proctoring==0.9.6b
|
||||
|
||||
# Third Party XBlocks
|
||||
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
|
||||
|
||||
Reference in New Issue
Block a user