Merge branch 'master' into release-mergeback-to-master

This commit is contained in:
Feanil Patel
2019-10-10 13:30:33 -04:00
committed by GitHub
41 changed files with 594 additions and 113 deletions

View File

@@ -39,7 +39,7 @@ class RequireJSPathOverridesTest(TestCase):
def test_requirejs_path_overrides(self):
result = render_require_js_path_overrides(self.OVERRIDES)
# To make the string comparision easy remove the whitespaces
self.assertEqual(list(map(str.strip, result.splitlines())), self.OVERRIDES_JS)
self.assertCountEqual(list(map(str.strip, result.splitlines())), self.OVERRIDES_JS)
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')

View File

@@ -32,7 +32,7 @@ class Command(BaseCommand):
csv_path = options['csv_path']
if csv_path:
with open(csv_path) as csv_file:
with open(csv_path, 'rb') as csv_file:
self.unenroll_users(csv_file)
else:
csv_file = BulkUnenrollConfiguration.current().csv_file

View File

@@ -96,7 +96,7 @@ class BulkUnenrollTests(SharedModuleStoreTestCase):
lines += str(enrollment.user.id) + "," + enrollment.user.username + "," + \
enrollment.user.email + "," + str(enrollment.course.id) + "\n"
csv_file = SimpleUploadedFile(name='test.csv', content=lines, content_type='text/csv')
csv_file = SimpleUploadedFile(name='test.csv', content=lines.encode('utf-8'), content_type='text/csv')
BulkUnenrollConfiguration.objects.create(enabled=True, csv_file=csv_file)
call_command("bulk_unenroll")
@@ -110,14 +110,14 @@ class BulkUnenrollTests(SharedModuleStoreTestCase):
for enrollment in self.enrollments:
username = enrollment.user.username
if username in users_unenrolled:
users_unenrolled[username].append(str(enrollment.course.id))
users_unenrolled[username].append(str(enrollment.course.id).encode('utf-8'))
else:
users_unenrolled[username] = [str(enrollment.course.id)]
users_unenrolled[username] = [str(enrollment.course.id).encode('utf-8')]
lines += str(enrollment.user.id) + "," + username + "," + \
enrollment.user.email + "," + str(enrollment.course.id) + "\n"
csv_file = SimpleUploadedFile(name='test.csv', content=lines, content_type='text/csv')
csv_file = SimpleUploadedFile(name='test.csv', content=lines.encode('utf-8'), content_type='text/csv')
BulkUnenrollConfiguration.objects.create(enabled=True, csv_file=csv_file)
with LogCapture(LOGGER_NAME) as log:

View File

@@ -16,7 +16,6 @@ import six.moves.urllib.request # pylint: disable=import-error
from lazy import lazy
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer # pylint: disable=import-error
from six.moves.socketserver import ThreadingMixIn # pylint: disable=import-error
LOGGER = getLogger(__name__)
@@ -105,13 +104,13 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
Retrieve the request POST parameters from the client as a dictionary.
If no POST parameters can be interpreted, return an empty dict.
"""
contents = self.request_content
contents = self.request_content.decode()
# The POST dict will contain a list of values for each key.
# None of our parameters are lists, however, so we map [val] --> val
# If the list contains multiple entries, we pick the first one
try:
post_dict = six.moves.urllib.parse.parse_qs(contents, keep_blank_values=True)
post_dict = six.moves.urllib.parse.parse_qs(contents.decode('utf-8'), keep_blank_values=True)
return {
key: list_val[0]
for key, list_val in post_dict.items()
@@ -159,13 +158,6 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
if len(self.post_dict) > 0:
for key, value in six.iteritems(self.post_dict):
# Decode the params as UTF-8
try:
key = six.text_type(key, 'utf-8')
value = six.text_type(value, 'utf-8')
except UnicodeDecodeError:
self.log_message("Could not decode request params as UTF-8")
self.log_message(u"Set config '{0}' to '{1}'".format(key, value))
try:
@@ -209,6 +201,8 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object):
self.end_headers()
if content is not None:
if not six.PY2 and isinstance(content, six.text_type):
content = content.encode('utf-8')
self.wfile.write(content)
def send_json_response(self, content):

View File

@@ -266,7 +266,7 @@ class StubLtiHandler(StubHttpRequestHandler):
# Calculate and encode body hash. See http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
sha1 = hashlib.sha1()
sha1.update(body)
sha1.update(body.encode('utf-8'))
oauth_body_hash = six.text_type(base64.b64encode(sha1.digest()))
mock_request = mock.Mock(
uri=six.text_type(six.moves.urllib.parse.unquote(url)),

View File

@@ -49,7 +49,7 @@ class StubLtiServiceTest(unittest.TestCase):
"""
self.launch_uri = self.uri + 'wrong_lti_endpoint'
response = requests.post(self.launch_uri, data=self.payload)
self.assertIn('Invalid request URL', response.content)
self.assertIn(b'Invalid request URL', response.content)
def test_wrong_signature(self):
"""
@@ -57,7 +57,7 @@ class StubLtiServiceTest(unittest.TestCase):
path and responses with incorrect signature.
"""
response = requests.post(self.launch_uri, data=self.payload)
self.assertIn('Wrong LTI signature', response.content)
self.assertIn(b'Wrong LTI signature', response.content)
@patch('terrain.stubs.lti.signature.verify_hmac_sha1', return_value=True)
def test_success_response_launch_lti(self, check_oauth):
@@ -65,34 +65,34 @@ class StubLtiServiceTest(unittest.TestCase):
Success lti launch.
"""
response = requests.post(self.launch_uri, data=self.payload)
self.assertIn('This is LTI tool. Success.', response.content)
self.assertIn(b'This is LTI tool. Success.', response.content)
@patch('terrain.stubs.lti.signature.verify_hmac_sha1', return_value=True)
def test_send_graded_result(self, verify_hmac): # pylint: disable=unused-argument
response = requests.post(self.launch_uri, data=self.payload)
self.assertIn('This is LTI tool. Success.', response.content)
self.assertIn(b'This is LTI tool. Success.', response.content)
grade_uri = self.uri + 'grade'
with patch('terrain.stubs.lti.requests.post') as mocked_post:
mocked_post.return_value = Mock(content='Test response', status_code=200)
response = six.moves.urllib.request.urlopen(grade_uri, data='')
self.assertIn('Test response', response.read())
response = six.moves.urllib.request.urlopen(grade_uri, data=b'')
self.assertIn(b'Test response', response.read())
@patch('terrain.stubs.lti.signature.verify_hmac_sha1', return_value=True)
def test_lti20_outcomes_put(self, verify_hmac): # pylint: disable=unused-argument
response = requests.post(self.launch_uri, data=self.payload)
self.assertIn('This is LTI tool. Success.', response.content)
self.assertIn(b'This is LTI tool. Success.', response.content)
grade_uri = self.uri + 'lti2_outcome'
with patch('terrain.stubs.lti.requests.put') as mocked_put:
mocked_put.return_value = Mock(status_code=200)
response = six.moves.urllib.request.urlopen(grade_uri, data='')
self.assertIn('LTI consumer (edX) responded with HTTP 200', response.read())
response = six.moves.urllib.request.urlopen(grade_uri, data=b'')
self.assertIn(b'LTI consumer (edX) responded with HTTP 200', response.read())
@patch('terrain.stubs.lti.signature.verify_hmac_sha1', return_value=True)
def test_lti20_outcomes_put_like_delete(self, verify_hmac): # pylint: disable=unused-argument
response = requests.post(self.launch_uri, data=self.payload)
self.assertIn('This is LTI tool. Success.', response.content)
self.assertIn(b'This is LTI tool. Success.', response.content)
grade_uri = self.uri + 'lti2_delete'
with patch('terrain.stubs.lti.requests.put') as mocked_put:
mocked_put.return_value = Mock(status_code=200)
response = six.moves.urllib.request.urlopen(grade_uri, data='')
self.assertIn('LTI consumer (edX) responded with HTTP 200', response.read())
response = six.moves.urllib.request.urlopen(grade_uri, data=b'')
self.assertIn(b'LTI consumer (edX) responded with HTTP 200', response.read())

View File

@@ -100,7 +100,7 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
problem.student_answers = {'1_2_1': 'choice_3'}
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
without_new_lines = the_html.replace("\\n", "").replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback3\" role=\"group\" aria-describedby=\"1_2_1-legend\">\s*<span class=\"sr\">Incorrect</span>.*3rd WRONG solution")
self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback2|feedbackC")
@@ -114,7 +114,7 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
problem.student_answers = {'1_2_1': 'choice_0'}
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
without_new_lines = the_html.replace("\\n", "").replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\" role=\"group\" aria-describedby=\"1_2_1-legend\">\s*<span class=\"sr\">Incorrect</span>.*1st WRONG solution")
self.assertRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
@@ -127,7 +127,7 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
problem.student_answers = {'1_2_1': 'choice_2'}
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
without_new_lines = the_html.replace("\\n", "").replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines,
r"<targetedfeedback explanation-id=\"feedbackC\" role=\"group\" aria-describedby=\"1_2_1-legend\">\s*<span class=\"sr\">Correct</span>.*Feedback on your correct solution...")

View File

@@ -1553,7 +1553,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
next_versions = [struct for struct in next_entries]
for course_structure in next_versions:
result.setdefault(course_structure['previous_version'], []).append(
CourseLocator(version_guid=struct['_id']))
CourseLocator(version_guid=next_entries[-1]['_id']))
return VersionTree(course_locator, result)
def get_block_generations(self, block_locator):

View File

@@ -44,7 +44,7 @@ class RandomizeModule(RandomizeFields, XModule):
# NOTE: calling self.get_children() doesn't work until we've picked a choice
num_choices = len(self.descriptor.get_children())
if self.choice > num_choices:
if self.choice is not None and self.choice > num_choices:
# Oops. Children changed. Reset.
self.choice = None

View File

@@ -4,6 +4,7 @@ Tests for sequence module.
# pylint: disable=no-member
from __future__ import absolute_import
import ast
import json
from datetime import timedelta
@@ -195,10 +196,10 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
extra_context=dict(specific_masquerade=True),
)
self.assertIn("seq_module.html", html)
self.assertIn(
"'banner_text': u'Because the due date has passed, "
"this assignment is hidden from the learner.'",
html
html = self.get_context_dict_from_string(html)
self.assertEqual(
'Because the due date has passed, this assignment is hidden from the learner.',
html['banner_text']
)
def test_hidden_content_self_paced_past_due_before_end(self):
@@ -223,31 +224,35 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
Assert sequence content is gated
"""
self.assertIn("seq_module.html", html)
self.assertIn("'banner_text': None", html)
self.assertIn("'items': []", html)
self.assertIn("'gated': True", html)
self.assertIn("'prereq_url': 'PrereqUrl'", html)
self.assertIn("'prereq_section_name': 'PrereqSectionName'", html)
self.assertIn("'gated_section_name': u'{}'".format(six.text_type(sequence.display_name)), html)
self.assertIn("'next_url': 'NextSequential'", html)
self.assertIn("'prev_url': 'PrevSequential'", html)
html = self.get_context_dict_from_string(html)
self.assertIsNone(html['banner_text'])
self.assertEqual([], html['items'])
self.assertTrue(html['gated_content']['gated'])
self.assertEqual('PrereqUrl', html['gated_content']['prereq_url'])
self.assertEqual('PrereqSectionName', html['gated_content']['prereq_section_name'])
self.assertIn(
six.text_type(sequence.display_name),
html['gated_content']['gated_section_name']
)
self.assertEqual('NextSequential', html['next_url'])
self.assertEqual('PrevSequential', html['prev_url'])
def _assert_prereq(self, html, sequence):
"""
Assert sequence is a prerequisite with unfulfilled gates
"""
self.assertIn("seq_module.html", html)
self.assertIn(
"'banner_text': u'This section is a prerequisite. "
"You must complete this section in order to unlock additional content.'",
html
html = self.get_context_dict_from_string(html)
self.assertEqual(
"This section is a prerequisite. You must complete this section in order to unlock additional content.",
html['banner_text']
)
self.assertIn("'gated': False", html)
self.assertIn(six.text_type(sequence.location), html)
self.assertIn("'prereq_url': None", html)
self.assertIn("'prereq_section_name': None", html)
self.assertIn("'next_url': 'NextSequential'", html)
self.assertIn("'prev_url': 'PrevSequential'", html)
self.assertFalse(html['gated_content']['gated'])
self.assertEqual(six.text_type(sequence.location), html['item_id'])
self.assertIsNone(html['gated_content']['prereq_url'])
self.assertIsNone(html['gated_content']['prereq_section_name'])
self.assertEqual('NextSequential', html['next_url'])
self.assertEqual('PrevSequential', html['prev_url'])
def _assert_ungated(self, html, sequence):
"""
@@ -295,7 +300,6 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
self.sequence_1_2,
extra_context=dict(next_url='NextSequential', prev_url='PrevSequential'),
)
# assert that content and preq banner is shown
self._assert_prereq(html, self.sequence_1_2)
@@ -338,3 +342,11 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
{'usage_key': usage_key}
)
self.assertIs(completion_return, None)
def get_context_dict_from_string(self, data):
"""
Retrieve dictionary from string.
"""
# Replace tuple and un-necessary info from inside string and get the dictionary.
cleaned_data = data.replace("(('seq_module.html',\n", '').replace("),\n {})", '').strip()
return ast.literal_eval(cleaned_data)

View File

@@ -827,7 +827,7 @@ class VideoExportTestCase(VideoBlockTestBase):
self.descriptor.transcripts = None
xml = self.descriptor.definition_to_xml(self.file_system)
expected = '<video url_name="SampleProblem"/>\n'
self.assertEquals(expected, etree.tostring(xml, pretty_print=True))
self.assertEquals(expected, etree.tostring(xml, pretty_print=True).decode('utf-8'))
@patch('xmodule.video_module.video_module.edxval_api', None)
def test_export_to_xml_invalid_characters_in_attributes(self):
@@ -1171,7 +1171,9 @@ class VideoBlockIndexingTestCase(unittest.TestCase):
self.assertFalse(validation.empty) # Validation contains some warning/message
self.assertTrue(validation.summary)
self.assertEqual(StudioValidationMessage.WARNING, validation.summary.type)
self.assertIn(expected_msg, validation.summary.text)
self.assertIn(
expected_msg, validation.summary.text.replace('Urdu, Esperanto', 'Esperanto, Urdu')
)
@ddt.data(
(

View File

@@ -416,7 +416,7 @@ class TestTranscriptAvailableTranslationsBumperDispatch(TestVideo):
request = Request.blank('/' + self.dispatch)
response = self.item.transcript(request=request, dispatch=self.dispatch)
self.assertEqual(json.loads(response.body), [lang])
self.assertEqual(json.loads(response.body.decode('utf-8')), [lang])
@patch('xmodule.video_module.transcripts_utils.get_available_transcript_languages')
def test_multiple_available_translations(self, mock_get_transcript_languages):
@@ -441,7 +441,7 @@ class TestTranscriptAvailableTranslationsBumperDispatch(TestVideo):
request = Request.blank('/' + self.dispatch)
response = self.item.transcript(request=request, dispatch=self.dispatch)
# Assert that bumper only get its own translations.
self.assertEqual(json.loads(response.body.decode('utf-8')), ['en', 'uk'])
self.assertEqual(sorted(json.loads(response.body.decode('utf-8'))), sorted(['en', 'uk']))
@ddt.ddt

View File

@@ -0,0 +1,11 @@
module.exports = {
extends: 'eslint-config-edx',
root: true,
settings: {
'import/resolver': {
webpack: {
config: 'webpack.dev.config.js',
},
},
},
};

View File

@@ -0,0 +1,16 @@
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
import { NewRelicLoggingService } from '@edx/frontend-logging';
const apiClient = getAuthenticatedAPIClient({
appBaseUrl: process.env.LMS_ROOT_URL,
authBaseUrl: process.env.LMS_ROOT_URL,
loginUrl: `${process.env.LMS_ROOT_URL}/login`,
logoutUrl: `${process.env.LMS_ROOT_URL}/logout`,
csrfTokenApiPath: '/csrf/api/v1/token',
refreshAccessTokenEndpoint: `${process.env.LMS_ROOT_URL}/login_refresh`,
accessTokenCookieName: process.env.JWT_AUTH_COOKIE_HEADER_PAYLOAD,
userInfoCookieName: process.env.EDXMKTG_USER_INFO_COOKIE_NAME,
loggingService: NewRelicLoggingService,
});
export default apiClient;

View File

@@ -0,0 +1,11 @@
module.exports = {
extends: 'eslint-config-edx',
root: true,
settings: {
'import/resolver': {
webpack: {
config: 'webpack.dev.config.js',
},
},
},
};

View File

@@ -0,0 +1,20 @@
import { getLearnerPortalLinks } from '@edx/frontend-enterprise';
import apiClient from '../apiClient';
function CustomUserMenuLinks() {
// Inject enterprise learner portal links
getLearnerPortalLinks(apiClient).then((learnerPortalLinks) => {
const $dashboardLink = $('#user-menu .dashboard');
const classNames = 'mobile-nav-item dropdown-item dropdown-nav-item';
for (let i = 0; i < learnerPortalLinks.length; i += 1) {
const link = learnerPortalLinks[i];
$dashboardLink.after( // xss-lint: disable=javascript-jquery-insertion
`<div class="${classNames}"><a href="${link.url}" role="menuitem">${link.title} Dashboard</a></div>`,
);
}
});
}
export { CustomUserMenuLinks }; // eslint-disable-line import/prefer-default-export

View File

@@ -0,0 +1,78 @@
import React, { Component } from 'react';
import { getLearnerPortalLinks } from '@edx/frontend-enterprise';
import { StatusAlert } from '@edx/paragon';
import apiClient from '../apiClient';
const LOCAL_STORAGE_KEY = 'has-viewed-enterprise-learner-portal-banner';
function getAlertHtml(learnerPortalLinks) {
let html = '';
for (let i = 0; i < learnerPortalLinks.length; i += 1) {
const link = learnerPortalLinks[i];
html += `<div>
${link.title} has a dedicated page where you can see all of your sponsored courses.
Go to <a href="${link.url}">your learner portal</a>.
</div>`;
}
return html;
}
function setViewedBanner() {
window.localStorage.setItem(LOCAL_STORAGE_KEY, true);
}
function hasViewedBanner() {
window.localStorage.getItem(LOCAL_STORAGE_KEY);
}
class EnterpriseLearnerPortalBanner extends Component {
constructor(props) {
super(props);
this.onClose = this.onClose.bind(this);
this.state = {
open: false,
alertHtml: '',
};
}
componentDidMount() {
if (!hasViewedBanner()) {
getLearnerPortalLinks(apiClient).then((learnerPortalLinks) => {
this.setState({
open: true,
alertHtml: getAlertHtml(learnerPortalLinks),
});
});
}
}
onClose() {
this.setState({ open: false });
setViewedBanner();
}
render() {
const { alertHtml, open } = this.state;
if (open) {
return (
<div className="edx-enterprise-learner-portal-banner-wrapper">
<StatusAlert
className={['edx-enterprise-learner-portal-banner']}
open={open}
// eslint-disable-next-line react/no-danger
dialog={(<span dangerouslySetInnerHTML={{ __html: alertHtml }} />)}
onClose={this.onClose}
/>
</div>
);
}
return null;
}
}
export { EnterpriseLearnerPortalBanner }; // eslint-disable-line import/prefer-default-export

View File

@@ -73,6 +73,7 @@
@import 'features/_unsupported-browser-alert';
@import 'features/content-type-gating';
@import 'features/course-duration-limits';
@import 'features/enterprise-learner-portal-banner';
// search
@import 'search/search';

View File

@@ -33,6 +33,7 @@
@import 'features/course-sock';
@import 'features/course-upgrade-message';
@import 'features/content-type-gating';
@import 'features/enterprise-learner-portal-banner';
// Responsive Design

View File

@@ -25,6 +25,7 @@ $static-path: '../..';
@import 'features/course-sock';
@import 'features/course-upgrade-message';
@import 'features/course-duration-limits';
@import 'features/enterprise-learner-portal-banner';
// Individual Pages

View File

@@ -0,0 +1,89 @@
$enterprise-learner-portal-banner-background-color: #d9edf7 !default;
$enterprise-learner-portal-banner-text-color: #4e4e4e !default;
$enterprise-learner-portal-banner-cta-base: #0075b4 !default;
$enterprise-learner-portal-banner-cta-hover: #075683 !default;
.edx-enterprise-learner-portal-banner-wrapper {
background: $enterprise-learner-portal-banner-background-color;
box-sizing: border-box;
/** Base Styles - start **/
text-align: left;
line-height: 1.5;
font: {
family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif;
size: 1rem;
weight: 400;
}
.alert {
position: relative;
padding: 0.75rem 1.25rem;
}
.alert-dismissible {
.close {
position: absolute;
top: 0;
right: 0;
padding: 0.75rem 1.25rem;
background: transparent;
border: 0;
text-shadow: 0 1px 0 #fff;
opacity: 0.5;
float: right;
line-height: 1;
font: {
size: 1.5rem;
weight: 700;
}
}
.btn {
display: inline-block;
text-align: center;
white-space: nowrap;
vertical-align: middle;
box-shadow: none;
}
}
/** Base Styles - end **/
.edx-enterprise-learner-portal-banner {
box-sizing: border-box;
display: flex;
justify-content: space-between;
max-width: 1200px;
min-width: 0;
margin: 0 auto;
background: inherit;
border: none;
.policy-link {
color: $enterprise-learner-portal-banner-cta-base;
text-decoration: underline;
&:focus,
&:hover {
color: $enterprise-learner-portal-banner-cta-hover;
border: none;
}
}
.alert-dialog {
margin-right: 30px;
color: $enterprise-learner-portal-banner-text-color;
}
.btn.close {
color: $enterprise-learner-portal-banner-cta-base;
&:focus,
&:hover {
color: $enterprise-learner-portal-banner-cta-hover;
cursor: pointer;
}
}
}
}

View File

@@ -1,6 +1,6 @@
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='static_content.html'/>
<%namespace name='static' file='../static_content.html'/>
<%!
from django.conf import settings
@@ -22,6 +22,12 @@ resume_block = retrieve_last_sitewide_block_completed(self.real_user)
displayname = get_enterprise_learner_generic_name(request) or username
%>
<%static:webpack entry="CustomUserMenuLinks">
$(document).ready(function() {
CustomUserMenuLinks();
});
</%static:webpack>
<div class="nav-item hidden-mobile">
<a href="${reverse('dashboard')}" class="menu-title">
<img class="user-image-frame" src="${profile_image_url}" alt="">
@@ -37,7 +43,7 @@ displayname = get_enterprise_learner_generic_name(request) or username
% if resume_block:
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${resume_block}" role="menuitem">${_("Resume your last course")}</a></div>
% endif
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('dashboard')}" role="menuitem">${_("Dashboard")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item dashboard"><a href="${reverse('dashboard')}" role="menuitem">${_("Dashboard")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('learner_profile', kwargs={'username': username})}" role="menuitem">${_("Profile")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('account_settings')}" role="menuitem">${_("Account")}</a></div>
% if should_redirect_to_order_history_microfrontend():

View File

@@ -74,8 +74,8 @@ class ProfileImageUrlTestCase(TestCase):
"""
self.user.profile.profile_image_uploaded_at = TEST_PROFILE_IMAGE_UPLOAD_DT
self.user.profile.save()
expected_name = hashlib.md5(
'secret' + text_type(self.user.username).encode('utf-8')).hexdigest()
expected_name = hashlib.md5((
'secret' + text_type(self.user.username)).encode('utf-8')).hexdigest()
actual_urls = get_profile_image_urls_for_user(self.user)
self.verify_urls(actual_urls, expected_name, is_default=False)

View File

@@ -126,7 +126,7 @@ class UserAPITestCase(APITestCase):
template = '{root}/{filename}_{{size}}.{extension}'
if has_profile_image:
url_root = 'http://example-storage.com/profile-images'
filename = hashlib.md5('secret' + self.user.username.encode('utf-8')).hexdigest()
filename = hashlib.md5(('secret' + self.user.username).encode('utf-8')).hexdigest()
file_extension = 'jpg'
template += '?v={}'.format(TEST_PROFILE_IMAGE_UPLOADED_AT.strftime("%s"))
else:
@@ -821,6 +821,9 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
Verify we handle error cases when patching the language_proficiencies
field.
"""
if six.PY3:
expected_error_message = six.text_type(expected_error_message).replace('unicode', 'str')
client = self.login_client("client", "user")
response = self.send_patch(client, {"language_proficiencies": patch_value}, expected_status=400)
self.assertEqual(

View File

@@ -4,6 +4,7 @@
Tests for js_utils.py
"""
from __future__ import absolute_import
import re
import six.moves.html_parser # pylint: disable=import-error
import json
from unittest import TestCase
@@ -97,14 +98,14 @@ class TestJSUtils(TestCase):
parsed from json where applicable.
"""
test_dict = {
'test_string': u'test-=&\\;\'"<>☃'.encode(encoding='utf-8'),
'test_string': u'test-=&\\;\'"<>☃',
'test_tuple': (1, 2, 3),
'test_number': 3.5,
'test_bool': False,
}
template = Template(
"""
u"""
<%!
import json
from openedx.core.djangolib.js_utils import (
@@ -140,25 +141,28 @@ class TestJSUtils(TestCase):
r"&#34;test_tuple&#34;: [1, 2, 3], &#34;test_string&#34;: "
r"&#34;test-=&amp;\\;&#39;\&#34;&lt;&gt;\u2603&#34;}"
)
expected_attr_json_for_html = "data-test-dict='" + expected_json_for_html + "'"
self._validate_expectation_of_json_for_html(test_dict, expected_json_for_html)
self.assertIn(expected_attr_json_for_html, out)
self.assertIn("&#34;test_tuple&#34;: [1, 2, 3]", out)
self.assertIn("&#34;test_number&#34;: 3.5", out)
self.assertIn("&#34;test_bool&#34;: false", out)
self.assertIn("&#34;test_string&#34;: &#34;test-=&amp;\\\\;&#39;\\&#34;&lt;&gt;\\u2603&#34", out)
self.assertIn(u"data-test-string='test-=&amp;\\;&#39;&#34;&lt;&gt;☃'", out)
self.assertIn("data-test-tuple='[1, 2, 3]'", out)
self.assertIn("data-test-number='3.5'", out)
self.assertIn("data-test-bool='false'", out)
expected_string_for_js_in_dict = r'''test-=\u0026\\;'\"\u003c\u003e\u2603'''
self._validate_expectation_of_string_for_js(test_dict['test_string'], expected_string_for_js_in_dict)
self.assertIn(
(
'var test_dict = {"test_bool": false, "test_number": 3.5, '
'"test_tuple": [1, 2, 3], "test_string": "' + expected_string_for_js_in_dict + '"}'
), out)
expected_string_for_js = r"test\u002D\u003D\u0026\u005C\u003B\u0027\u0022\u003C\u003E☃"
location_of_dict_in_out = re.search("var test_dict.*}", out)
var_dict_in_out = out[location_of_dict_in_out.span()[0]:location_of_dict_in_out.span()[1]]
self.assertIn('"test_number": 3.5', var_dict_in_out)
self.assertIn('"test_string": "test-=\\u0026\\\\;\'\\"\\u003c\\u003e\\u2603"', var_dict_in_out)
self.assertIn('"test_tuple": [1, 2, 3]', var_dict_in_out)
self.assertIn('"test_bool": false', var_dict_in_out)
expected_string_for_js = u"test\\u002D\\u003D\\u0026\\u005C\\u003B\\u0027\\u0022\\u003C\\u003E☃"
self._validate_expectation_of_string_for_js(test_dict['test_string'], expected_string_for_js)
self.assertIn(
"var test_string = '" + expected_string_for_js.decode(encoding='utf-8') + "'",
out)
self.assertIn("var test_string = '" + expected_string_for_js + "'", out)
self.assertIn("var test_none_string = ''", out)
self.assertIn("var test_tuple = [1, 2, 3]", out)
self.assertIn("var test_number = 3.5", out)
@@ -188,7 +192,7 @@ class TestJSUtils(TestCase):
# tuples become arrays in json, so it is parsed to a list that is
# switched back to a tuple before comparing
parsed_expected_dict['test_tuple'] = tuple(parsed_expected_dict['test_tuple'])
self.assertEqual(test_dict['test_string'].decode(encoding='utf-8'), parsed_expected_dict['test_string'])
self.assertEqual(test_dict['test_string'], parsed_expected_dict['test_string'])
self.assertEqual(test_dict['test_tuple'], parsed_expected_dict['test_tuple'])
self.assertEqual(test_dict['test_number'], parsed_expected_dict['test_number'])
self.assertEqual(test_dict['test_bool'], parsed_expected_dict['test_bool'])
@@ -209,4 +213,4 @@ class TestJSUtils(TestCase):
"""
parsed_expected_string = json.loads('"' + expected_string_for_js + '"')
self.assertEqual(test_string.decode(encoding='utf-8'), parsed_expected_string)
self.assertEqual(test_string, parsed_expected_string)

View File

@@ -23,7 +23,7 @@ class ApiTestCase(TestCase):
"""
Returns a dictionary containing the http auth header with encoded username+password
"""
return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))}
return {'HTTP_AUTHORIZATION': b'Basic ' + base64.b64encode(b'%s:%s' % (username.encode(), password.encode()))}
def request_with_auth(self, method, *args, **kwargs):
"""Issue a request to the given URI with the API key header"""

View File

@@ -8,6 +8,8 @@ import mock
import ddt
import httpretty
from six.moves.urllib.parse import parse_qs # pylint: disable=import-error
from consent.models import DataSharingConsent
from django.conf import settings
from django.contrib.auth.models import User
@@ -395,15 +397,16 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, CacheIsolationTestCase):
course_id = 'course-v1:edX+DemoX+Demo_Course'
return_to = 'info'
expected_url = (
'/enterprise/grant_data_sharing_permissions?course_id=course-v1%3AedX%2BDemoX%2BDemo_'
'Course&failure_url=http%3A%2F%2Flocalhost%3A8000%2Fdashboard%3Fconsent_failed%3Dcou'
'rse-v1%253AedX%252BDemoX%252BDemo_Course&enterprise_customer_uuid=cf246b88-d5f6-4908'
'-a522-fc307e0b0c59&next=http%3A%2F%2Flocalhost%3A8000%2Fcourses%2Fcourse-v1%3AedX%2B'
'DemoX%2BDemo_Course%2Finfo'
)
expected_url_args = {
'course_id': ['course-v1:edX+DemoX+Demo_Course'],
'failure_url': ['http://localhost:8000/dashboard?consent_failed=course-v1%3AedX%2BDemoX%2BDemo_Course'],
'enterprise_customer_uuid': ['cf246b88-d5f6-4908-a522-fc307e0b0c59'],
'next': ['http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info']
}
actual_url = get_enterprise_consent_url(request_mock, course_id, return_to=return_to)
self.assertEqual(actual_url, expected_url)
actual_url_args = parse_qs(actual_url.split('/enterprise/grant_data_sharing_permissions?')[1])
self.assertEqual(actual_url_args, expected_url_args)
@ddt.data(
(False, {'real': 'enterprise', 'uuid': ''}, 'course', [], [], "", ""),

174
package-lock.json generated
View File

@@ -66,6 +66,59 @@
"resolved": "https://registry.npmjs.org/@edx/edx-proctoring/-/edx-proctoring-1.5.0.tgz",
"integrity": "sha512-RiNjAgh8ZMX0D5gfN2R09a0RBs/R/Blfs/DiqhLmvCSvyCoeMDGANrDDQXv1w5blxxSJbz8a2awSZkwpv6gWNQ=="
},
"@edx/frontend-auth": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-6.0.2.tgz",
"integrity": "sha512-37qMrdzwe02PzLQMF4q2ov7POGM3laWtouHK8BiLL7Q2DhFBxe0IULWAZiEJGvSVBEpMDFfcG+J1s33NiIpUfA==",
"requires": {
"@edx/frontend-logging": "2.1.0",
"axios": "0.18.1",
"camelcase-keys": "5.2.0",
"jwt-decode": "2.2.0",
"pubsub-js": "1.7.0",
"snakecase-keys": "2.1.0",
"universal-cookie": "3.1.0",
"url-parse": "1.4.7"
},
"dependencies": {
"@edx/frontend-logging": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-logging/-/frontend-logging-2.1.0.tgz",
"integrity": "sha512-IN0Bgh0/1Ax3TMPfZztqzdJchW4B5Px9PT4V9uu6TMj2Cj8el1CV3jrSA4Idg8C3CAkFZ/EHjmaFVCxgJ9aXVA=="
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"camelcase-keys": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-5.2.0.tgz",
"integrity": "sha512-mSM/OQKD1HS5Ll2AXxeaHSdqCGC/QQ8IrgTbKYA/rxnC36thBKysfIr9+OVBWuW17jyZF4swHkjtglawgBmVFg==",
"requires": {
"camelcase": "5.3.1",
"map-obj": "3.1.0",
"quick-lru": "1.1.0"
}
},
"map-obj": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-3.1.0.tgz",
"integrity": "sha512-Xg1iyYz/+iIW6YoMldux47H/e5QZyDSB41Kb0ev+YYHh3FJnyyzY0vTk/WbVeWcCvdXd70cOriUBmhP8alUFBA=="
},
"universal-cookie": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-3.1.0.tgz",
"integrity": "sha512-sP6WuFgqIUro7ikgI2ndrsw9Ro+YvVBe5O9cQfWnjTicpLaSMUEUUDjQF8m8utzWF2ONl7tRkcZd7v4n6NnzjQ==",
"requires": {
"@types/cookie": "0.3.3",
"@types/object-assign": "4.0.30",
"cookie": "0.3.1",
"object-assign": "4.1.1"
}
}
}
},
"@edx/frontend-component-cookie-policy-banner": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-cookie-policy-banner/-/frontend-component-cookie-policy-banner-1.0.0.tgz",
@@ -137,6 +190,16 @@
}
}
},
"@edx/frontend-enterprise": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-enterprise/-/frontend-enterprise-1.0.2.tgz",
"integrity": "sha512-Xv5R8qpAmg4qr2B4EJpCJrBbgNISPoQbGjK31cS9wWz8u28lhAmsgltKaRKqTD92DPi+uGam3feJ2QhAwLqJqQ=="
},
"@edx/frontend-logging": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-logging/-/frontend-logging-3.0.1.tgz",
"integrity": "sha512-kRDsPbTUxNfZdnC4KN5HratS/7bkCYv/gyvUnBcuPbiONXwSuriNIVAKCepldvhg1DTwLqQMXh+Qw6vo2r048A=="
},
"@edx/mockprock": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@edx/mockprock/-/mockprock-1.0.2.tgz",
@@ -272,12 +335,22 @@
"resolved": "https://registry.npmjs.org/@sambego/storybook-styles/-/storybook-styles-1.0.0.tgz",
"integrity": "sha512-n0SqZwDewUDRaStEcoNMiYy9qovaLVStsh4Gb2dc2LLiG3IIK0UXdeR1N7puVuRihJq/192uOyGPCjZ/NAteuA=="
},
"@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"@types/node": {
"version": "10.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.2.tgz",
"integrity": "sha512-m9zXmifkZsMHZBOyxZWilMwmTlpC8x5Ty360JKTiXvlXZfBWYpsg9ZZvP/Ye+iZUh+Q+MxDLjItVTWIsfwz+8Q==",
"dev": true
},
"@types/object-assign": {
"version": "4.0.30",
"resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI="
},
"abab": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@@ -905,6 +978,22 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
},
"axios": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "2.0.4"
},
"dependencies": {
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
}
}
},
"axobject-query": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz",
@@ -5203,6 +5292,24 @@
"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz",
"integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I="
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
@@ -7837,6 +7944,11 @@
"integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=",
"dev": true
},
"jwt-decode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
},
"karma": {
"version": "0.13.22",
"resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz",
@@ -10491,6 +10603,11 @@
"randombytes": "2.0.6"
}
},
"pubsub-js": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.7.0.tgz",
"integrity": "sha512-Pb68P9qFZxnvDipHMuj9oT1FoIgBcXJ9C9eWdHCLZAnulaUoJ3+Y87RhGMYilWpun6DMWVmvK70T4RP4drZMSA=="
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
@@ -10525,11 +10642,15 @@
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
},
"querystringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
},
"quick-lru": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz",
"integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=",
"dev": true
"integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g="
},
"raf": {
"version": "3.4.0",
@@ -11295,8 +11416,7 @@
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"reselect": {
"version": "3.0.1",
@@ -12185,6 +12305,22 @@
"resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
"integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA=="
},
"snakecase-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-2.1.0.tgz",
"integrity": "sha512-oQSiCIgNCwixBf8Kxgv0SPo67zQSutIEymAk/dkgcdZEOMPvGMGPua/WwYGPG4LLHArGGews3CB3zEEfqlMk2g==",
"requires": {
"map-obj": "3.0.0",
"to-snake-case": "1.0.0"
},
"dependencies": {
"map-obj": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-3.0.0.tgz",
"integrity": "sha512-Ot+2wruG8WqTbJngDxz0Ifm03y2pO4iL+brq/l+yEkGjUza03BnMQqX2XT//Jls8MOOl2VTHviAoLX+/nq/HXw=="
}
}
},
"snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -13893,6 +14029,11 @@
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
"integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc="
},
"to-no-case": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz",
"integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo="
},
"to-object-path": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
@@ -13935,6 +14076,22 @@
}
}
},
"to-snake-case": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/to-snake-case/-/to-snake-case-1.0.0.tgz",
"integrity": "sha1-znRpE4l5RgGah+Yu366upMYIq4w=",
"requires": {
"to-space-case": "1.0.0"
}
},
"to-space-case": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz",
"integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=",
"requires": {
"to-no-case": "1.0.2"
}
},
"toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
@@ -14356,6 +14513,15 @@
}
}
},
"url-parse": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
"requires": {
"querystringify": "2.1.1",
"requires-port": "1.0.0"
}
},
"url-toolkit": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.1.4.tgz",

View File

@@ -5,6 +5,9 @@
"@edx/frontend-component-cookie-policy-banner": "1.0.0",
"@edx/edx-bootstrap": "1.0.4",
"@edx/edx-proctoring": "^1.5.0",
"@edx/frontend-auth": "^6.0.2",
"@edx/frontend-enterprise": "^1.0.2",
"@edx/frontend-logging": "^3.0.1",
"@edx/paragon": "2.6.4",
"@edx/studio-frontend": "1.16.17",
"babel-core": "6.26.0",

View File

@@ -767,12 +767,28 @@ def webpack(options):
settings = getattr(options, 'settings', Env.DEVSTACK_SETTINGS)
static_root_lms = Env.get_django_setting("STATIC_ROOT", "lms", settings=settings)
static_root_cms = Env.get_django_setting("STATIC_ROOT", "cms", settings=settings)
config_path = Env.get_django_setting("WEBPACK_CONFIG_PATH", "lms", settings=settings)
environment = u'NODE_ENV={node_env} STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms}'.format(
node_env="development" if config_path == 'webpack.dev.config.js' else "production",
static_root_lms=static_root_lms,
static_root_cms=static_root_cms
lms_root_url = Env.get_django_setting("LMS_ROOT_URL", "lms", settings=settings)
jwt_auth_cookie_header_payload_name = Env.get_nested_django_setting(
"JWT_AUTH",
"JWT_AUTH_COOKIE_HEADER_PAYLOAD",
"lms",
settings=settings,
)
user_info_cookie_name = Env.get_django_setting("EDXMKTG_USER_INFO_COOKIE_NAME", "lms", settings=settings)
config_path = Env.get_django_setting("WEBPACK_CONFIG_PATH", "lms", settings=settings)
environment = u"NODE_ENV={node_env} " \
u"STATIC_ROOT_LMS={static_root_lms} " \
u"STATIC_ROOT_CMS={static_root_cms} " \
u"LMS_ROOT_URL={lms_root_url} " \
u"JWT_AUTH_COOKIE_HEADER_PAYLOAD={jwt_auth_cookie_header_payload_name} " \
u"EDXMKTG_USER_INFO_COOKIE_NAME={user_info_cookie_name}".format(
node_env="development" if config_path == 'webpack.dev.config.js' else "production",
static_root_lms=static_root_lms,
static_root_cms=static_root_cms,
lms_root_url=lms_root_url,
jwt_auth_cookie_header_payload_name=jwt_auth_cookie_header_payload_name,
user_info_cookie_name=user_info_cookie_name,
)
sh(
cmd(
u'{environment} $(npm bin)/webpack --config={config_path}'.format(

View File

@@ -46,10 +46,15 @@ EXPECTED_INDEX_COURSE_COMMAND = (
EXPECTED_PRINT_SETTINGS_COMMAND = [
u"python manage.py lms --settings={settings} print_setting STATIC_ROOT 2>{log_file}",
u"python manage.py cms --settings={settings} print_setting STATIC_ROOT 2>{log_file}",
u"python manage.py lms --settings={settings} print_setting LMS_ROOT_URL 2>{log_file}",
u"python manage.py lms --settings={settings} print_setting JWT_AUTH 2>{log_file}",
u"python manage.py lms --settings={settings} print_setting EDXMKTG_USER_INFO_COOKIE_NAME 2>{log_file}",
u"python manage.py lms --settings={settings} print_setting WEBPACK_CONFIG_PATH 2>{log_file}"
]
EXPECTED_WEBPACK_COMMAND = (
u"NODE_ENV={node_env} STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} "
u"LMS_ROOT_URL={lms_root_url} JWT_AUTH_COOKIE_HEADER_PAYLOAD={jwt_auth_cookie_header_payload_name} "
u"EDXMKTG_USER_INFO_COOKIE_NAME={user_info_cookie_name} "
u"$(npm bin)/webpack --config={webpack_config_path}"
)
@@ -252,6 +257,9 @@ class TestPaverServerTasks(PaverTestCase):
node_env="production",
static_root_lms=None,
static_root_cms=None,
lms_root_url=None,
jwt_auth_cookie_header_payload_name=None,
user_info_cookie_name=None,
webpack_config_path=None
))
expected_messages.extend(self.expected_sass_commands(system=system, asset_settings=expected_asset_settings))
@@ -298,6 +306,9 @@ class TestPaverServerTasks(PaverTestCase):
node_env="production",
static_root_lms=None,
static_root_cms=None,
lms_root_url=None,
jwt_auth_cookie_header_payload_name=None,
user_info_cookie_name=None,
webpack_config_path=None
))
expected_messages.extend(self.expected_sass_commands(asset_settings=expected_asset_settings))

View File

@@ -6,6 +6,7 @@ from __future__ import absolute_import, print_function
import io
import json
import os
import re
import sys
from time import sleep
@@ -268,6 +269,25 @@ class Env(object):
print(f.read())
sys.exit(1)
@classmethod
def get_nested_django_setting(cls, django_setting, nested_django_setting, system, settings=None):
"""
Interrogate Django environment for specific nested settings values
:param django_setting: the root django setting to get
:param nested_django_setting: the nested django setting to get
:param system: the django app to use when asking for the setting (lms | cms)
:param settings: the settings file to use when asking for the value
:return: unicode value of the django setting
"""
django_setting_value = cls.get_django_setting(django_setting, system, settings)
pattern = re.compile(
u"[\"']{setting}[\"']: [\"'](?P<setting_value>.*)[\"']".format(setting=nested_django_setting)
)
match = pattern.search(django_setting_value)
if match:
return match.group('setting_value')
return None
@classmethod
def covered_modules(cls):
"""

View File

@@ -129,7 +129,7 @@ feedparser==5.1.3
fs-s3fs==0.1.8
fs==2.0.18
funcsigs==1.0.2
future==0.17.1 # via edx-celeryutils, edx-enterprise, pyjwkest
future==0.18.0 # via edx-celeryutils, edx-enterprise, pyjwkest
futures==3.3.0 ; python_version == "2.7" # via django-pipeline, python-swiftclient, s3transfer, xblock-utils
geoip2==2.9.0
glob2==0.7
@@ -152,7 +152,7 @@ lazy==1.1
lepl==5.1.3 # via rfc6266-parser
libsass==0.10.0
loremipsum==1.0.5
git+https://github.com/edx/xblock-lti-consumer.git@v1.1.8#egg=lti_consumer-xblock==1.1.8
git+https://github.com/edx/xblock-lti-consumer.git@v1.2.1#egg=lti_consumer-xblock==1.2.1
lxml==3.8.0
mailsnake==1.6.4
mako==1.0.2
@@ -236,7 +236,7 @@ soupsieve==1.9.4 # via beautifulsoup4
sqlparse==0.3.0
staff-graded-xblock==0.5
stevedore==1.31.0
super-csv==0.9.4
super-csv==0.9.5
sympy==1.4
testfixtures==6.10.0 # via edx-enterprise
text-unidecode==1.3 # via python-slugify

View File

@@ -164,7 +164,7 @@ fs-s3fs==0.1.8
fs==2.0.18
funcsigs==1.0.2
functools32==3.2.3.post2 ; python_version == "2.7"
future==0.17.1
future==0.18.0
futures==3.3.0 ; python_version == "2.7"
geoip2==2.9.0
glob2==0.7
@@ -172,7 +172,7 @@ gunicorn==19.9.0
help-tokens==1.0.4
html5lib==1.0.1
httplib2==0.14.0
httpretty==0.9.6
httpretty==0.9.7
idna==2.8
imagesize==1.1.0 # via sphinx
importlib-metadata==0.23
@@ -195,7 +195,7 @@ lazy==1.1
lepl==5.1.3
libsass==0.10.0
loremipsum==1.0.5
git+https://github.com/edx/xblock-lti-consumer.git@v1.1.8#egg=lti_consumer-xblock==1.1.8
git+https://github.com/edx/xblock-lti-consumer.git@v1.2.1#egg=lti_consumer-xblock==1.2.1
lxml==3.8.0
mailsnake==1.6.4
mako==1.0.2
@@ -314,7 +314,7 @@ sphinxcontrib-websupport==1.1.2 # via sphinx
sqlparse==0.3.0
staff-graded-xblock==0.5
stevedore==1.31.0
super-csv==0.9.4
super-csv==0.9.5
sympy==1.4
testfixtures==6.10.0
text-unidecode==1.3

View File

@@ -86,7 +86,7 @@ git+https://github.com/edx/crowdsourcehinter.git@a7ffc85b134b7d8909bf1fefd23dbdb
-e git+https://github.com/edx/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
-e git+https://github.com/edx/DoneXBlock.git@01a14f3bd80ae47dd08cdbbe2f88f3eb88d00fba#egg=done-xblock
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
git+https://github.com/edx/xblock-lti-consumer.git@v1.1.8#egg=lti_consumer-xblock==1.1.8
git+https://github.com/edx/xblock-lti-consumer.git@v1.2.1#egg=lti_consumer-xblock==1.2.1
# Third Party XBlocks

View File

@@ -159,7 +159,7 @@ fs-s3fs==0.1.8
fs==2.0.18
funcsigs==1.0.2
functools32==3.2.3.post2 ; python_version == "2.7" # via flake8
future==0.17.1
future==0.18.0
futures==3.3.0 ; python_version == "2.7"
geoip2==2.9.0
glob2==0.7
@@ -167,7 +167,7 @@ gunicorn==19.9.0
help-tokens==1.0.4
html5lib==1.0.1
httplib2==0.14.0
httpretty==0.9.6
httpretty==0.9.7
idna==2.8
importlib-metadata==0.23 # via pluggy, tox
inflect==2.1.0
@@ -189,7 +189,7 @@ lazy==1.1
lepl==5.1.3
libsass==0.10.0
loremipsum==1.0.5
git+https://github.com/edx/xblock-lti-consumer.git@v1.1.8#egg=lti_consumer-xblock==1.1.8
git+https://github.com/edx/xblock-lti-consumer.git@v1.2.1#egg=lti_consumer-xblock==1.2.1
lxml==3.8.0
mailsnake==1.6.4
mako==1.0.2
@@ -301,7 +301,7 @@ soupsieve==1.9.4
sqlparse==0.3.0
staff-graded-xblock==0.5
stevedore==1.31.0
super-csv==0.9.4
super-csv==0.9.5
sympy==1.4
testfixtures==6.10.0
text-unidecode==1.3

View File

@@ -137,6 +137,11 @@ from student.models import CourseEnrollment
</div>
%endif
${static.renderReact(
component="EnterpriseLearnerPortalBanner",
id="enterprise-learner-portal-banner",
props={}
)}
</div>
<section class="dashboard" id="dashboard-main">

View File

@@ -91,6 +91,8 @@ module.exports = Merge.smart({
StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx',
StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js',
ProblemBrowser: './lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx',
CustomUserMenuLinks: './lms/static/js/custom_user_menu_links/CustomUserMenuLinks.js',
EnterpriseLearnerPortalBanner: './lms/static/js/learner_dashboard/EnterpriseLearnerPortalBanner.jsx',
// Learner Dashboard
EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js',

View File

@@ -20,7 +20,10 @@ module.exports = _.values(Merge.smart(commonConfig, {
debug: true
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
'process.env.NODE_ENV': JSON.stringify('development'),
'process.env.LMS_ROOT_URL': JSON.stringify('https://localhost:18000'),
'process.env.JWT_AUTH_COOKIE_HEADER_PAYLOAD': JSON.stringify('edx-jwt-cookie-header-payload'),
'process.env.EDXMKTG_USER_INFO_COOKIE_NAME': JSON.stringify('edx-user-info')
})
],
module: {

View File

@@ -17,7 +17,10 @@ var optimizedConfig = Merge.smart(commonConfig, {
devtool: false,
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.LMS_ROOT_URL': JSON.stringify(process.env.LMS_ROOT_URL),
'process.env.JWT_AUTH_COOKIE_HEADER_PAYLOAD': JSON.stringify(process.env.JWT_AUTH_COOKIE_HEADER_PAYLOAD),
'process.env.EDXMKTG_USER_INFO_COOKIE_NAME': JSON.stringify(process.env.EDXMKTG_USER_INFO_COOKIE_NAME)
}),
new webpack.LoaderOptionsPlugin({ // This may not be needed; legacy option for loaders written for webpack 1
minimize: true