Commit 81abdbe8 by Kevin Falcone

Merge pull request #9819 from edx/release

Release
parents 22b3c0bc a428385d
"""Tests running the delete_orphan command""" """Tests running the delete_orphan command"""
import ddt
from django.core.management import call_command from django.core.management import call_command
from contentstore.tests.test_orphan import TestOrphanBase from contentstore.tests.test_orphan import TestOrphanBase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore import ModuleStoreEnum
@ddt.ddt
class TestDeleteOrphan(TestOrphanBase): class TestDeleteOrphan(TestOrphanBase):
""" """
Tests for running the delete_orphan management command. Tests for running the delete_orphan management command.
Inherits from TestOrphan in order to use its setUp method. Inherits from TestOrphan in order to use its setUp method.
""" """
def setUp(self): @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
super(TestDeleteOrphan, self).setUp() def test_delete_orphans_no_commit(self, default_store):
self.course_id = self.course.id.to_deprecated_string()
def test_delete_orphans_no_commit(self):
""" """
Tests that running the command without a 'commit' argument Tests that running the command without a 'commit' argument
results in no orphans being deleted results in no orphans being deleted
""" """
call_command('delete_orphans', self.course_id) course = self.create_course_with_orphans(default_store)
self.assertTrue(self.store.has_item(self.course.id.make_usage_key('html', 'multi_parent_html'))) call_command('delete_orphans', unicode(course.id))
self.assertTrue(self.store.has_item(self.course.id.make_usage_key('vertical', 'OrphanVert'))) self.assertTrue(self.store.has_item(course.id.make_usage_key('html', 'multi_parent_html')))
self.assertTrue(self.store.has_item(self.course.id.make_usage_key('chapter', 'OrphanChapter'))) self.assertTrue(self.store.has_item(course.id.make_usage_key('vertical', 'OrphanVert')))
self.assertTrue(self.store.has_item(self.course.id.make_usage_key('html', 'OrphanHtml'))) 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 Tests that running the command WITH the 'commit' argument
results in the orphans being deleted 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 # 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 # 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(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(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('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 @@ ...@@ -2,27 +2,34 @@
Test finding orphans via the view and django config Test finding orphans via the view and django config
""" """
import json import json
import ddt
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from student.models import CourseEnrollment from student.models import CourseEnrollment
from contentstore.utils import reverse_course_url from contentstore.utils import reverse_course_url
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.factories import CourseFactory
class TestOrphanBase(CourseTestCase): class TestOrphanBase(CourseTestCase):
""" """
Base class for Studio tests that require orphaned modules Base class for Studio tests that require orphaned modules
""" """
def setUp(self): def create_course_with_orphans(self, default_store):
super(TestOrphanBase, self).setUp() """
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 # 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) 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) self.store.publish(chapter2.location, self.user.id)
# orphan chapter # 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) self.store.publish(orphan_chapter.location, self.user.id)
# create vertical and add it as child to chapter1 # create vertical and add it as child to chapter1
...@@ -30,7 +37,7 @@ class TestOrphanBase(CourseTestCase): ...@@ -30,7 +37,7 @@ class TestOrphanBase(CourseTestCase):
self.store.publish(vertical1.location, self.user.id) self.store.publish(vertical1.location, self.user.id)
# create orphan vertical # 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) self.store.publish(orphan_vertical.location, self.user.id)
# create component and add it to vertical1 # create component and add it to vertical1
...@@ -45,61 +52,79 @@ class TestOrphanBase(CourseTestCase): ...@@ -45,61 +52,79 @@ class TestOrphanBase(CourseTestCase):
self.store.update_item(orphan_vertical, self.user.id) self.store.update_item(orphan_vertical, self.user.id)
# create an orphaned html module # 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.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, course.location, 'static_tab', "staticuno")
self.store.create_child(self.user.id, self.course.location, 'about', "overview") self.store.create_child(self.user.id, course.location, 'course_info', "updates")
self.store.create_child(self.user.id, self.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): class TestOrphan(TestOrphanBase):
""" """
Test finding orphans via view and django config 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( orphans = json.loads(
self.client.get( self.client.get(
self.orphan_url, orphan_url,
HTTP_ACCEPT='application/json' HTTP_ACCEPT='application/json'
).content ).content
) )
self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans)) 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) 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) 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) 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( 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)) self.assertEqual(len(orphans), 0, "Orphans not deleted {}".format(orphans))
# make sure that any children with one orphan parent and one non-orphan # make sure that any children with one orphan parent and one non-orphan
# parent are not deleted # 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 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() test_user_client, test_user = self.create_non_staff_authed_user_client()
CourseEnrollment.enroll(test_user, self.course.id) CourseEnrollment.enroll(test_user, course.id)
response = test_user_client.get(self.orphan_url) response = test_user_client.get(orphan_url)
self.assertEqual(response.status_code, 403) 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) self.assertEqual(response.status_code, 403)
...@@ -702,10 +702,14 @@ def _delete_orphans(course_usage_key, user_id, commit=False): ...@@ -702,10 +702,14 @@ def _delete_orphans(course_usage_key, user_id, commit=False):
""" """
store = modulestore() store = modulestore()
items = store.get_orphans(course_usage_key) items = store.get_orphans(course_usage_key)
branch = course_usage_key.branch
if commit: if commit:
for itemloc in items: for itemloc in items:
# need to delete all versions revision = ModuleStoreEnum.RevisionOption.all
store.delete_item(itemloc, user_id, 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] return [unicode(item) for item in items]
......
...@@ -557,7 +557,6 @@ PIPELINE_JS_COMPRESSOR = None ...@@ -557,7 +557,6 @@ PIPELINE_JS_COMPRESSOR = None
STATICFILES_IGNORE_PATTERNS = ( STATICFILES_IGNORE_PATTERNS = (
"*.py", "*.py",
"*.pyc", "*.pyc",
"*.html",
# It would be nice if we could do, for example, "**/*.scss", # It would be nice if we could do, for example, "**/*.scss",
# but these strings get passed down to the `fnmatch` module, # but these strings get passed down to the `fnmatch` module,
......
...@@ -126,6 +126,16 @@ class CourseMode(models.Model): ...@@ -126,6 +126,16 @@ class CourseMode(models.Model):
self.currency = self.currency.lower() self.currency = self.currency.lower()
super(CourseMode, self).save(force_insert, force_update, using) 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 @classmethod
def all_modes_for_courses(cls, course_id_list): def all_modes_for_courses(cls, course_id_list):
"""Find all modes for a list of course IDs, including expired modes. """Find all modes for a list of course IDs, including expired modes.
......
...@@ -2416,14 +2416,36 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): ...@@ -2416,14 +2416,36 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
return result return result
@contract(block_key=BlockKey, blocks='dict(BlockKey: BlockData)') @contract(root_block_key=BlockKey, blocks='dict(BlockKey: BlockData)')
def _remove_subtree(self, block_key, blocks): 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
for child in blocks[block_key].fields.get('children', []): any children that may have parents that we don't want to delete.
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): def delete_course(self, course_key, user_id):
""" """
......
...@@ -175,7 +175,7 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli ...@@ -175,7 +175,7 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
self._auto_publish_no_children(parent_usage_key, item.location.category, user_id, **kwargs) self._auto_publish_no_children(parent_usage_key, item.location.category, user_id, **kwargs)
return item 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. Delete the given item from persistence. kwargs allow modulestore specific parameters.
...@@ -217,7 +217,8 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli ...@@ -217,7 +217,8 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
if ( if (
branch == ModuleStoreEnum.BranchName.draft and branch == ModuleStoreEnum.BranchName.draft and
branched_location.block_type in (DIRECT_ONLY_CATEGORIES + ['vertical']) 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 # will publish if its not an orphan
self.publish(parent_loc.version_agnostic(), user_id, blacklist=EXCLUDE_ALL, **kwargs) self.publish(parent_loc.version_agnostic(), user_id, blacklist=EXCLUDE_ALL, **kwargs)
......
...@@ -8,9 +8,10 @@ import time ...@@ -8,9 +8,10 @@ import time
from dateutil.parser import parse from dateutil.parser import parse
import ddt import ddt
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from selenium.common.exceptions import TimeoutException
from uuid import uuid4 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 import LMS_BASE_URL
from ...fixtures.course import CourseFixture from ...fixtures.course import CourseFixture
from ...fixtures.discussion import ( from ...fixtures.discussion import (
...@@ -60,18 +61,23 @@ class TeamsTabBase(EventsTestMixin, UniqueCourseTest): ...@@ -60,18 +61,23 @@ class TeamsTabBase(EventsTestMixin, UniqueCourseTest):
'language': 'aa', 'language': 'aa',
'country': 'AF' 'country': 'AF'
} }
response = self.course_fixture.session.post( teams.append(self.post_team_data(team))
LMS_BASE_URL + '/api/team/v0/teams/',
data=json.dumps(team),
headers=self.course_fixture.headers
)
# Sadly, this sleep is necessary in order to ensure that # Sadly, this sleep is necessary in order to ensure that
# sorting by last_activity_at works correctly when running # sorting by last_activity_at works correctly when running
# in Jenkins. # in Jenkins.
time.sleep(time_between_creation) time.sleep(time_between_creation)
teams.append(json.loads(response.text))
return teams 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): def create_membership(self, username, team_id):
"""Assign `username` to `team_id`.""" """Assign `username` to `team_id`."""
response = self.course_fixture.session.post( response = self.course_fixture.session.post(
...@@ -838,6 +844,26 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase): ...@@ -838,6 +844,26 @@ class BrowseTeamsWithinTopicTest(TeamsTabBase):
with self.assert_events_match_during(self.only_team_events, expected_events=events): with self.assert_events_match_during(self.only_team_events, expected_events=events):
self.browse_teams_page.visit() 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') @attr('shard_5')
class TeamFormActions(TeamsTabBase): class TeamFormActions(TeamsTabBase):
......
...@@ -135,7 +135,7 @@ ...@@ -135,7 +135,7 @@
actionContent: function() { actionContent: function() {
return interpolate( return interpolate(
gettext('View %(span_start)s %(team_name)s %(span_end)s'), 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 true
); );
}, },
......
...@@ -766,13 +766,22 @@ def create_order(request): ...@@ -766,13 +766,22 @@ def create_order(request):
return HttpResponseBadRequest(_("Selected price is not valid number.")) return HttpResponseBadRequest(_("Selected price is not valid number."))
current_mode = None current_mode = None
paid_modes = CourseMode.paid_modes_for_course(course_id) sku = request.POST.get('sku', None)
# 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 sku:
if paid_modes: try:
if len(paid_modes) > 1: current_mode = CourseMode.objects.get(sku=sku)
log.warn(u"Multiple paid course modes found for course '%s' for create order request", course_id) except CourseMode.DoesNotExist:
current_mode = paid_modes[0] 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 # Make sure this course has a paid mode
if not current_mode: if not current_mode:
......
...@@ -1622,7 +1622,6 @@ if os.path.isdir(DATA_DIR): ...@@ -1622,7 +1622,6 @@ if os.path.isdir(DATA_DIR):
STATICFILES_IGNORE_PATTERNS = ( STATICFILES_IGNORE_PATTERNS = (
"*.py", "*.py",
"*.pyc", "*.pyc",
"*.html",
# It would be nice if we could do, for example, "**/*.scss", # It would be nice if we could do, for example, "**/*.scss",
# but these strings get passed down to the `fnmatch` module, # but these strings get passed down to the `fnmatch` module,
......
...@@ -66,7 +66,8 @@ define([ ...@@ -66,7 +66,8 @@ define([
var params = { var params = {
contribution: kwargs.amount || "", contribution: kwargs.amount || "",
course_id: kwargs.courseId || "", course_id: kwargs.courseId || "",
processor: kwargs.processor || "" processor: kwargs.processor || "",
sku: kwargs.sku || ""
}; };
// Click the "go to payment" button // Click the "go to payment" button
......
...@@ -55,6 +55,7 @@ var edx = edx || {}; ...@@ -55,6 +55,7 @@ var edx = edx || {};
), ),
upgrade: el.data('msg-key') === 'upgrade', upgrade: el.data('msg-key') === 'upgrade',
minPrice: el.data('course-mode-min-price'), minPrice: el.data('course-mode-min-price'),
sku: el.data('course-mode-sku'),
contributionAmount: el.data('contribution-amount'), contributionAmount: el.data('contribution-amount'),
suggestedPrices: _.filter( suggestedPrices: _.filter(
(el.data('course-mode-suggested-prices').toString()).split(","), (el.data('course-mode-suggested-prices').toString()).split(","),
......
...@@ -17,6 +17,7 @@ var edx = edx || {}; ...@@ -17,6 +17,7 @@ var edx = edx || {};
isActive: true, isActive: true,
suggestedPrices: [], suggestedPrices: [],
minPrice: 0, minPrice: 0,
sku: '',
currency: 'usd', currency: 'usd',
upgrade: false, upgrade: false,
verificationDeadline: '', verificationDeadline: '',
...@@ -133,7 +134,8 @@ var edx = edx || {}; ...@@ -133,7 +134,8 @@ var edx = edx || {};
postData = { postData = {
'processor': event.target.id, 'processor': event.target.id,
'contribution': paymentAmount, 'contribution': paymentAmount,
'course_id': this.stepData.courseKey 'course_id': this.stepData.courseKey,
'sku': this.templateContext().sku
}; };
// Disable the payment button to prevent multiple submissions // Disable the payment button to prevent multiple submissions
......
...@@ -263,7 +263,8 @@ ...@@ -263,7 +263,8 @@
@include text-align(center); @include text-align(center);
.provider-logo img { .provider-logo img {
width: 100px; max-width: 160px;
margin-bottom: $baseline * 0.5;
} }
.complete-order { .complete-order {
......
...@@ -100,6 +100,7 @@ ...@@ -100,6 +100,7 @@
<% if ( isActive ) { %> <% if ( isActive ) { %>
<div class="payment-buttons nav-wizard is-ready center"> <div class="payment-buttons nav-wizard is-ready center">
<input type="hidden" name="contribution" value="<%- minPrice %>" /> <input type="hidden" name="contribution" value="<%- minPrice %>" />
<input type="hidden" name="sku" value="<%- sku %>" />
<div class="purchase"> <div class="purchase">
<p class="product-info"><span class="product-name"></span> <%- gettext( "price" ) %>: <span class="price">$<%- minPrice %> USD</span></p> <p class="product-info"><span class="product-name"></span> <%- gettext( "price" ) %>: <span class="price">$<%- minPrice %> USD</span></p>
</div> </div>
......
...@@ -62,6 +62,7 @@ from verify_student.views import PayAndVerifyView ...@@ -62,6 +62,7 @@ from verify_student.views import PayAndVerifyView
data-course-mode-name='${course_mode.name}' data-course-mode-name='${course_mode.name}'
data-course-mode-slug='${course_mode.slug}' data-course-mode-slug='${course_mode.slug}'
data-course-mode-min-price='${course_mode.min_price}' 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-suggested-prices='${course_mode.suggested_prices}'
data-course-mode-currency='${course_mode.currency}' data-course-mode-currency='${course_mode.currency}'
data-contribution-amount='${contribution_amount}' data-contribution-amount='${contribution_amount}'
......
...@@ -162,7 +162,7 @@ def create_credit_request(course_key, provider_id, username): ...@@ -162,7 +162,7 @@ def create_credit_request(course_key, provider_id, username):
"course_org": "HogwartsX", "course_org": "HogwartsX",
"course_num": "Potions101", "course_num": "Potions101",
"course_run": "1T2015", "course_run": "1T2015",
"final_grade": 0.95, "final_grade": "0.95",
"user_username": "ron", "user_username": "ron",
"user_email": "ron@example.com", "user_email": "ron@example.com",
"user_full_name": "Ron Weasley", "user_full_name": "Ron Weasley",
...@@ -242,13 +242,13 @@ def create_credit_request(course_key, provider_id, username): ...@@ -242,13 +242,13 @@ def create_credit_request(course_key, provider_id, username):
# Retrieve the final grade from the eligibility table # Retrieve the final grade from the eligibility table
try: try:
final_grade = CreditRequirementStatus.objects.get( final_grade = unicode(CreditRequirementStatus.objects.get(
username=username, username=username,
requirement__namespace="grade", requirement__namespace="grade",
requirement__name="grade", requirement__name="grade",
requirement__course__course_key=course_key, requirement__course__course_key=course_key,
status="satisfied" status="satisfied"
).reason["final_grade"] ).reason["final_grade"])
except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError): except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError):
log.exception( log.exception(
"Could not retrieve final grade from the credit eligibility table " "Could not retrieve final grade from the credit eligibility table "
......
...@@ -633,14 +633,10 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase): ...@@ -633,14 +633,10 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
self.assertTrue(parsed_date < datetime.datetime.now(pytz.UTC)) self.assertTrue(parsed_date < datetime.datetime.now(pytz.UTC))
# Validate course information # Validate course information
self.assertIn('course_org', parameters)
self.assertEqual(parameters['course_org'], self.course_key.org) self.assertEqual(parameters['course_org'], self.course_key.org)
self.assertIn('course_num', parameters)
self.assertEqual(parameters['course_num'], self.course_key.course) self.assertEqual(parameters['course_num'], self.course_key.course)
self.assertIn('course_run', parameters)
self.assertEqual(parameters['course_run'], self.course_key.run) self.assertEqual(parameters['course_run'], self.course_key.run)
self.assertIn('final_grade', parameters) self.assertEqual(parameters['final_grade'], unicode(self.FINAL_GRADE))
self.assertEqual(parameters['final_grade'], self.FINAL_GRADE)
# Validate user information # Validate user information
for key in self.USER_INFO.keys(): for key in self.USER_INFO.keys():
......
...@@ -118,7 +118,7 @@ class CreditProviderViewTests(UrlResetMixin, TestCase): ...@@ -118,7 +118,7 @@ class CreditProviderViewTests(UrlResetMixin, TestCase):
self.assertEqual(content["parameters"]["course_org"], "edX") self.assertEqual(content["parameters"]["course_org"], "edX")
self.assertEqual(content["parameters"]["course_num"], "DemoX") self.assertEqual(content["parameters"]["course_num"], "DemoX")
self.assertEqual(content["parameters"]["course_run"], "Demo_Course") 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_username"], self.USERNAME)
self.assertEqual(content["parameters"]["user_full_name"], self.USER_FULL_NAME) self.assertEqual(content["parameters"]["user_full_name"], self.USER_FULL_NAME)
self.assertEqual(content["parameters"]["user_mailing_address"], "") self.assertEqual(content["parameters"]["user_mailing_address"], "")
......
...@@ -111,7 +111,7 @@ def create_credit_request(request, provider_id): ...@@ -111,7 +111,7 @@ def create_credit_request(request, provider_id):
course_org: "ASUx" course_org: "ASUx"
course_num: "DemoX" course_num: "DemoX"
course_run: "1T2015" course_run: "1T2015"
final_grade: 0.95, final_grade: "0.95",
user_username: "john", user_username: "john",
user_email: "john@example.com" user_email: "john@example.com"
user_full_name: "John Smith" 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 ...@@ -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-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 -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 # Third Party XBlocks
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga -e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
......
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