Add live validation to password reset
Adds a new React factory for that page to handle the logic. Also cleans up the UI a little (centers it, stops using serif font, etc).
This commit is contained in:
committed by
Michael Terry
parent
32f9902f2e
commit
c19d01a994
@@ -1159,25 +1159,18 @@ def password_reset_confirm_wrapper(request, uidb36=None, token=None):
|
|||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
password = request.POST['new_password1']
|
password = request.POST['new_password1']
|
||||||
valid_link = True # password reset link will be valid if there is no security violation
|
|
||||||
error_message = None
|
|
||||||
try:
|
try:
|
||||||
validate_password(password, user=user)
|
validate_password(password, user=user)
|
||||||
except SecurityPolicyError as err:
|
|
||||||
error_message = err.message
|
|
||||||
valid_link = False
|
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
error_message = err.message
|
|
||||||
|
|
||||||
if error_message:
|
|
||||||
# We have a password reset attempt which violates some security
|
# We have a password reset attempt which violates some security
|
||||||
# policy, or any other validation. Use the existing Django template to communicate that
|
# policy, or any other validation. Use the existing Django template to communicate that
|
||||||
# back to the user.
|
# back to the user.
|
||||||
context = {
|
context = {
|
||||||
'validlink': valid_link,
|
'validlink': True,
|
||||||
'form': None,
|
'form': None,
|
||||||
'title': _('Password reset unsuccessful'),
|
'title': _('Password reset unsuccessful'),
|
||||||
'err_msg': error_message,
|
'err_msg': err.message,
|
||||||
}
|
}
|
||||||
context.update(platform_name)
|
context.update(platform_name)
|
||||||
return TemplateResponse(
|
return TemplateResponse(
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'babel-polyfill';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
// babel-polyfill must be imported last because of https://github.com/facebook/react/issues/8379
|
||||||
|
// which otherwise causes "Objects are not valid as a react child" errors in IE11.
|
||||||
|
import 'babel-polyfill';
|
||||||
|
|
||||||
class ReactRendererException extends Error {
|
class ReactRendererException extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super(`ReactRendererException: ${message}`);
|
super(`ReactRendererException: ${message}`);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class TestPasswordHistory(LoginEnrollmentTestCase):
|
|||||||
history = PasswordHistory()
|
history = PasswordHistory()
|
||||||
history.create(user)
|
history.create(user)
|
||||||
|
|
||||||
def assertPasswordResetError(self, response, error_message, valid_link=False):
|
def assertPasswordResetError(self, response, error_message, valid_link=True):
|
||||||
"""
|
"""
|
||||||
This method is a custom assertion that verifies that a password reset
|
This method is a custom assertion that verifies that a password reset
|
||||||
view returns an error response as expected.
|
view returns an error response as expected.
|
||||||
@@ -363,4 +363,4 @@ class TestPasswordHistory(LoginEnrollmentTestCase):
|
|||||||
'new_password1': password1,
|
'new_password1': password1,
|
||||||
'new_password2': password2,
|
'new_password2': password2,
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
self.assertPasswordResetError(resp, err_msg, valid_link=True)
|
self.assertPasswordResetError(resp, err_msg)
|
||||||
|
|||||||
11
lms/static/js/student_account/components/.eslintrc.js
Normal file
11
lms/static/js/student_account/components/.eslintrc.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: 'eslint-config-edx',
|
||||||
|
root: true,
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
webpack: {
|
||||||
|
config: 'webpack.dev.config.js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/* globals gettext */
|
||||||
|
|
||||||
|
import 'whatwg-fetch';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button, StatusAlert } from '@edx/paragon/static';
|
||||||
|
|
||||||
|
import PasswordResetInput from './PasswordResetInput';
|
||||||
|
|
||||||
|
// NOTE: Use static paragon with this because some internal classes (StatusAlert at least)
|
||||||
|
// conflict with some standard LMS ones ('alert' at least). This means that you need to do
|
||||||
|
// something like the following on any templates that use this class:
|
||||||
|
//
|
||||||
|
// <link type='text/css' rel='stylesheet' href='${STATIC_URL}paragon/static/paragon.min.css'>
|
||||||
|
//
|
||||||
|
|
||||||
|
class PasswordResetConfirmation extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
password: '',
|
||||||
|
passwordConfirmation: '',
|
||||||
|
showMatchError: false,
|
||||||
|
isValid: true,
|
||||||
|
validationMessage: '',
|
||||||
|
};
|
||||||
|
this.onBlurPassword1 = this.onBlurPassword1.bind(this);
|
||||||
|
this.onBlurPassword2 = this.onBlurPassword2.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurPassword1(password) {
|
||||||
|
this.updatePasswordState(password, this.state.passwordConfirmation);
|
||||||
|
this.validatePassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurPassword2(passwordConfirmation) {
|
||||||
|
this.updatePasswordState(this.state.password, passwordConfirmation);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePasswordState(password, passwordConfirmation) {
|
||||||
|
this.setState({
|
||||||
|
password,
|
||||||
|
passwordConfirmation,
|
||||||
|
showMatchError: !!password && !!passwordConfirmation && (password !== passwordConfirmation),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePassword(password) {
|
||||||
|
fetch('/api/user/v1/validation/registration', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((response) => {
|
||||||
|
let validationMessage = '';
|
||||||
|
// Be careful about grabbing this message, since we could have received an HTTP error or the
|
||||||
|
// endpoint didn't give us what we expect. We only care if we get a clear error message.
|
||||||
|
if (response.validation_decisions && response.validation_decisions.password) {
|
||||||
|
validationMessage = response.validation_decisions.password;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
isValid: !validationMessage,
|
||||||
|
validationMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<section id="password-reset-confirm-anchor" className="form-type">
|
||||||
|
<div id="password-reset-confirm-form" className="form-wrapper" aria-live="polite">
|
||||||
|
<StatusAlert
|
||||||
|
alertType="danger"
|
||||||
|
dismissible={false}
|
||||||
|
open={!!this.props.errorMessage}
|
||||||
|
dialog={this.props.errorMessage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form id="passwordreset-form" method="post" action="">
|
||||||
|
<h2 className="section-title lines">
|
||||||
|
<span className="text">
|
||||||
|
{gettext('Reset Your Password')}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="action-label" id="new_password_help_text">
|
||||||
|
{gettext('Enter and confirm your new password.')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<PasswordResetInput
|
||||||
|
name="new_password1"
|
||||||
|
describedBy="new_password_help_text"
|
||||||
|
label={gettext('New Password')}
|
||||||
|
onBlur={this.onBlurPassword1}
|
||||||
|
isValid={this.state.isValid}
|
||||||
|
validationMessage={this.state.validationMessage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordResetInput
|
||||||
|
name="new_password2"
|
||||||
|
describedBy="new_password_help_text"
|
||||||
|
label={gettext('Confirm Password')}
|
||||||
|
onBlur={this.onBlurPassword2}
|
||||||
|
isValid={!this.state.showMatchError}
|
||||||
|
validationMessage={gettext('Passwords do not match.')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
id="csrf_token"
|
||||||
|
name="csrfmiddlewaretoken"
|
||||||
|
value={this.props.csrfToken}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={['action', 'action-primary', 'action-update', 'js-reset']}
|
||||||
|
label={gettext('Reset My Password')}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PasswordResetConfirmation.propTypes = {
|
||||||
|
csrfToken: PropTypes.string.isRequired,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
PasswordResetConfirmation.defaultProps = {
|
||||||
|
errorMessage: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PasswordResetConfirmation }; // eslint-disable-line import/prefer-default-export
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/* globals gettext */
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { InputText } from '@edx/paragon/static';
|
||||||
|
|
||||||
|
function PasswordResetInput(props) {
|
||||||
|
return (
|
||||||
|
<div className="form-field">
|
||||||
|
<InputText
|
||||||
|
id={props.name}
|
||||||
|
type="password"
|
||||||
|
themes={['danger']}
|
||||||
|
dangerIconDescription={gettext('Error: ')}
|
||||||
|
required
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PasswordResetInput.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordResetInput;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/* globals setFixtures */
|
||||||
|
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon'; // eslint-disable-line import/no-extraneous-dependencies
|
||||||
|
import { PasswordResetConfirmation } from '../PasswordResetConfirmation';
|
||||||
|
|
||||||
|
describe('PasswordResetConfirmation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setFixtures('<div id="wrapper"></div>');
|
||||||
|
sinon.stub(window, 'fetch');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.fetch.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
function init(submitError) {
|
||||||
|
ReactDOM.render(
|
||||||
|
React.createElement(PasswordResetConfirmation, {
|
||||||
|
csrfToken: 'csrfToken',
|
||||||
|
errorMessage: submitError,
|
||||||
|
}, null),
|
||||||
|
document.getElementById('wrapper'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerValidation() {
|
||||||
|
$('#new_password1').focus();
|
||||||
|
$('#new_password1').val('a');
|
||||||
|
$('#new_password2').focus();
|
||||||
|
|
||||||
|
expect(window.fetch.calledWithMatch(
|
||||||
|
'/api/user/v1/validation/registration',
|
||||||
|
{ body: JSON.stringify({ password: 'a' }) },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareValidation(validationError, done) {
|
||||||
|
window.fetch.reset();
|
||||||
|
window.fetch.callsFake(() => {
|
||||||
|
done();
|
||||||
|
return Promise.resolve({
|
||||||
|
json: () => ({ validation_decisions: { password: validationError } }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('shows submit error', () => {
|
||||||
|
init('Submit error.');
|
||||||
|
|
||||||
|
expect($('.alert-dialog')).toExist();
|
||||||
|
expect($('.alert-dialog')).not.toBeHidden();
|
||||||
|
expect($('.alert-dialog')).toHaveText('Submit error.');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validation', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
init('');
|
||||||
|
prepareValidation('Validation error.', done);
|
||||||
|
triggerValidation();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows validation error', () => {
|
||||||
|
expect($('#error-new_password1')).toContainText('Validation error.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Password reset template JS.
|
|
||||||
*/
|
|
||||||
$(function() {
|
|
||||||
'use strict';
|
|
||||||
// adding js class for styling with accessibility in mind
|
|
||||||
$('body').addClass('js');
|
|
||||||
|
|
||||||
// form field label styling on focus
|
|
||||||
$('form :input').focus(function() {
|
|
||||||
$("label[for='" + this.id + "']").parent().addClass('is-focused');
|
|
||||||
}).blur(function() {
|
|
||||||
$('label').parent().removeClass('is-focused');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -42,6 +42,7 @@ var options = {
|
|||||||
// Define the Webpack-built spec files first
|
// Define the Webpack-built spec files first
|
||||||
{pattern: 'course_experience/js/**/*_spec.js', webpack: true},
|
{pattern: 'course_experience/js/**/*_spec.js', webpack: true},
|
||||||
{pattern: 'js/learner_dashboard/**/*_spec.js', webpack: true},
|
{pattern: 'js/learner_dashboard/**/*_spec.js', webpack: true},
|
||||||
|
{pattern: 'js/student_account/components/**/*_spec.js', webpack: true},
|
||||||
{pattern: 'completion/js/**/*_spec.js', webpack: true},
|
{pattern: 'completion/js/**/*_spec.js', webpack: true},
|
||||||
|
|
||||||
// Add all remaining spec files to be used without Webpack
|
// Add all remaining spec files to be used without Webpack
|
||||||
|
|||||||
@@ -172,7 +172,7 @@
|
|||||||
&::after {
|
&::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: ($baseline/2);
|
top: 50%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: $gray-l3;
|
background: $gray-l3;
|
||||||
@@ -182,10 +182,11 @@
|
|||||||
|
|
||||||
.text {
|
.text {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -2px; // Aligns center of text with center of line (CR)
|
top: -1px; // Aligns center of text with center of line (CR)
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
padding: 0 $baseline;
|
padding: 0 $baseline;
|
||||||
background: $white;
|
background: $white;
|
||||||
|
font-size: $baseline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,78 +8,46 @@ from openedx.core.djangolib.markup import HTML, Text
|
|||||||
%>
|
%>
|
||||||
|
|
||||||
<%inherit file="../main.html"/>
|
<%inherit file="../main.html"/>
|
||||||
|
<%namespace name='static' file='../static_content.html'/>
|
||||||
|
|
||||||
<%block name="title">
|
<%block name="title">
|
||||||
<title>${_("Reset Your {platform_name} Password").format(platform_name=platform_name)}</title>
|
<title>${_("Reset Your {platform_name} Password").format(platform_name=platform_name)}</title>
|
||||||
</%block>
|
</%block>
|
||||||
|
|
||||||
<%block name="bodyextra">
|
<%block name="head_extra">
|
||||||
<script type="text/javascript" src="${STATIC_URL}js/student_account/password_reset.js"></script>
|
<link type="text/css" rel="stylesheet" href="${STATIC_URL}paragon/static/paragon.min.css">
|
||||||
</%block>
|
</%block>
|
||||||
|
|
||||||
<%block name="bodyclass">view-passwordreset</%block>
|
<%block name="bodyclass">view-passwordreset</%block>
|
||||||
|
|
||||||
<%block name="body">
|
<%block name="body">
|
||||||
<div id="password-reset-confirm-container" class="login-register">
|
<div id="password-reset-confirm-container" class="login-register-content login-register">
|
||||||
<section id="password-reset-confirm-anchor" class="form-type">
|
% if validlink:
|
||||||
<div id="password-reset-confirm-form" class="form-wrapper" aria-live="polite">
|
${static.renderReact(
|
||||||
<div class="status submission-error ${'hidden' if not err_msg else ''}">
|
component="PasswordResetConfirmation",
|
||||||
<h4 class="message-title">${_("Error Resetting Password")}</h4>
|
id="password-reset-confirm-react",
|
||||||
<ul class="message-copy">
|
props={
|
||||||
% if err_msg:
|
'csrfToken': csrf_token,
|
||||||
<li>${err_msg}</li>
|
'errorMessage': err_msg if err_msg else '',
|
||||||
% else:
|
},
|
||||||
<li>${_("You must enter and confirm your new password.")}</li>
|
)}
|
||||||
<li>${_("The text in both password fields must match.")}</li>
|
% else:
|
||||||
% endif
|
<div class="status submission-error">
|
||||||
</ul>
|
<h4 class="message-title">${_("Invalid Password Reset Link")}</h4>
|
||||||
</div>
|
<ul class="message-copy">
|
||||||
|
${Text(_((
|
||||||
% if validlink:
|
"This password reset link is invalid. It may have been used already. "
|
||||||
<form role="form" id="passwordreset-form" method="post" action="">
|
"To reset your password, go to the {start_link}sign-in{end_link} page and "
|
||||||
<div class="section-title lines">
|
"select {start_strong}Forgot password{end_strong}."
|
||||||
<h2>
|
))).format(
|
||||||
<span class="text">
|
start_link=HTML('<a href="/login">'),
|
||||||
${_("Reset Your Password")}
|
end_link=HTML('</a>'),
|
||||||
</span>
|
start_strong=HTML('<strong>'),
|
||||||
</h2>
|
end_strong=HTML('</strong>')
|
||||||
</div>
|
)
|
||||||
|
}
|
||||||
<p class="action-label" id="new_password_help_text">
|
</ul>
|
||||||
${_("Enter and confirm your new password.")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="form-field new_password1-new_password1">
|
|
||||||
<label for="new_password1">${_("New Password")}</label>
|
|
||||||
<input id="new_password1" type="password" name="new_password1" aria-describedby="new_password_help_text" />
|
|
||||||
</div>
|
|
||||||
<div class="form-field new_password2-new_password2">
|
|
||||||
<label for="new_password2">${_("Confirm Password")}</label>
|
|
||||||
<input id="new_password2" type="password" name="new_password2" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
|
|
||||||
|
|
||||||
<button type="submit" class="action action-primary action-update js-reset">${_("Reset My Password")}</button>
|
|
||||||
</form>
|
|
||||||
% else:
|
|
||||||
<div class="status submission-error">
|
|
||||||
<h4 class="message-title">${_("Invalid Password Reset Link")}</h4>
|
|
||||||
<ul class="message-copy">
|
|
||||||
${Text(_((
|
|
||||||
"This password reset link is invalid. It may have been used already. To reset your password, "
|
|
||||||
"go to the {start_link}sign-in{end_link} page and select {start_strong}Forgot password{end_strong}."
|
|
||||||
))).format(
|
|
||||||
start_link=HTML('<a href="/login">'),
|
|
||||||
end_link=HTML('</a>'),
|
|
||||||
start_strong=HTML('<strong>'),
|
|
||||||
end_strong=HTML('</strong>')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
% endif
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</%block>
|
</%block>
|
||||||
|
|||||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -22,19 +22,43 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@edx/paragon": {
|
"@edx/paragon": {
|
||||||
"version": "1.7.1",
|
"version": "2.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-2.5.6.tgz",
|
||||||
"integrity": "sha512-Nzw6IpnMMzvIwXhlmwYz5E9f+aRPAAh94u3epP/GkhSJ2gq+kL3Kj645rEN+17xpzC95u11yfctpyXT6FtAVlA==",
|
"integrity": "sha512-ASKTOWTZvBUo8ev2SJ9apWBa7u7UKc3UNNdGlqgBzLzj2hNVJ2urxrgATaa1LaMazT23gMZkZM5iKCToQE5Pvw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@edx/edx-bootstrap": "0.4.3",
|
"@edx/edx-bootstrap": "1.0.0",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
"classnames": "2.2.5",
|
"classnames": "2.2.5",
|
||||||
|
"email-prop-type": "1.1.5",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
|
"mailto-link": "1.0.0",
|
||||||
"prop-types": "15.6.0",
|
"prop-types": "15.6.0",
|
||||||
"react": "16.1.0",
|
"react": "16.1.0",
|
||||||
"react-dom": "16.1.0",
|
"react-dom": "16.1.0",
|
||||||
"react-element-proptypes": "1.0.0",
|
"react-element-proptypes": "1.0.0",
|
||||||
"react-proptype-conditional-require": "1.0.4"
|
"react-proptype-conditional-require": "1.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@edx/edx-bootstrap": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@edx/edx-bootstrap/-/edx-bootstrap-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZVoGAqWo9NtPKoNRgOgiW+Qr83TyZ+CWiKFpTmqaG3fm0qBgysnYYRooh1pyaJPedgFw2ljGrAAHYpn94R3pfw==",
|
||||||
|
"requires": {
|
||||||
|
"bootstrap": "4.0.0",
|
||||||
|
"jquery": "3.3.1",
|
||||||
|
"popper.js": "1.12.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bootstrap": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-gulJE5dGFo6Q61V/whS6VM4WIyrlydXfCgkE+Gxe5hjrJ8rXLLZlALq7zq2RPhOc45PSwQpJkrTnc2KgD6cvmA=="
|
||||||
|
},
|
||||||
|
"jquery": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@edx/studio-frontend": {
|
"@edx/studio-frontend": {
|
||||||
@@ -7480,7 +7504,7 @@
|
|||||||
},
|
},
|
||||||
"onetime": {
|
"onetime": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
|
||||||
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/edx-bootstrap": "0.4.3",
|
"@edx/edx-bootstrap": "0.4.3",
|
||||||
"@edx/paragon": "1.7.1",
|
"@edx/paragon": "2.5.6",
|
||||||
"@edx/studio-frontend": "1.7.0",
|
"@edx/studio-frontend": "1.7.0",
|
||||||
"babel-core": "6.26.0",
|
"babel-core": "6.26.0",
|
||||||
"babel-loader": "6.4.1",
|
"babel-loader": "6.4.1",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ module.exports = {
|
|||||||
UpsellExperimentModal: './lms/static/common/js/components/UpsellExperimentModal.jsx',
|
UpsellExperimentModal: './lms/static/common/js/components/UpsellExperimentModal.jsx',
|
||||||
PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx',
|
PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx',
|
||||||
EntitlementSupportPage: './lms/djangoapps/support/static/support/jsx/entitlements/index.jsx',
|
EntitlementSupportPage: './lms/djangoapps/support/static/support/jsx/entitlements/index.jsx',
|
||||||
|
PasswordResetConfirmation: './lms/static/js/student_account/components/PasswordResetConfirmation.jsx',
|
||||||
|
|
||||||
// Learner Dashboard
|
// Learner Dashboard
|
||||||
EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js',
|
EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js',
|
||||||
|
|||||||
Reference in New Issue
Block a user