Merge pull request #17945 from edx/alasdair/LEARNER-4880-gdpr-modal-display

LEARNER-4880 added confirmation modal for account deletion
This commit is contained in:
AlasdairSwan
2018-04-24 16:40:07 -04:00
committed by GitHub
10 changed files with 428 additions and 13 deletions

View File

@@ -0,0 +1,27 @@
/* eslint-disable no-new */
import { ReactRenderer } from 'ReactRenderer';
import { StudentAccountDeletion } from './components/StudentAccountDeletion';
const maxWait = 60000;
const interval = 50;
const accountDeletionWrapperId = 'account-deletion-container';
let currentWait = 0;
const wrapperRendered = setInterval(() => {
const wrapper = document.getElementById(accountDeletionWrapperId);
if (wrapper) {
clearInterval(wrapperRendered);
new ReactRenderer({
component: StudentAccountDeletion,
selector: `#${accountDeletionWrapperId}`,
componentName: 'StudentAccountDeletion',
});
}
currentWait += interval;
if (currentWait >= maxWait) {
clearInterval(wrapperRendered);
}
}, interval);

View File

@@ -0,0 +1,56 @@
/* globals gettext */
/* eslint-disable react/no-danger, import/prefer-default-export */
import React from 'react';
import { Button } from '@edx/paragon/static';
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
import StudentAccountDeletionModal from './StudentAccountDeletionModal';
export class StudentAccountDeletion extends React.Component {
constructor(props) {
super(props);
this.closeDeletionModal = this.closeDeletionModal.bind(this);
this.loadDeletionModal = this.loadDeletionModal.bind(this);
this.state = { deletionModalOpen: false };
}
closeDeletionModal() {
this.setState({ deletionModalOpen: false });
this.modalTrigger.focus();
}
loadDeletionModal() {
this.setState({ deletionModalOpen: true });
}
render() {
const { deletionModalOpen } = this.state;
const loseAccessText = StringUtils.interpolate(
gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a certificate{htmlEnd}.'),
{
htmlStart: '<a href="http://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate" target="_blank">',
htmlEnd: '</a>',
},
);
return (
<div className="account-deletion-details">
<p className="account-settings-header-subtitle">{ gettext('Were sorry to see you go!') }</p>
<p className="account-settings-header-subtitle">{ gettext('Please note: Deletion of your account and personal data is permanent and cannot be undone. EdX will not be able to recover your account or the data that is deleted.') }</p>
<p className="account-settings-header-subtitle">{ gettext('Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.') }</p>
<p
className="account-settings-header-subtitle"
dangerouslySetInnerHTML={{ __html: loseAccessText }}
/>
<Button
id="delete-account-btn"
className={['btn-outline-primary']}
label={gettext('Delete My Account')}
inputRef={(input) => { this.modalTrigger = input; }}
onClick={this.loadDeletionModal}
/>
{deletionModalOpen && <StudentAccountDeletionModal onClose={this.closeDeletionModal} />}
</div>
);
}
}

View File

@@ -0,0 +1,216 @@
/* globals gettext */
/* eslint-disable react/no-danger */
import React from 'react';
import 'whatwg-fetch';
import PropTypes from 'prop-types';
import Cookies from 'js-cookie';
import { Button, Modal, Icon, InputText, StatusAlert } from '@edx/paragon/static';
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
class StudentAccountDeletionConfirmationModal extends React.Component {
constructor(props) {
super(props);
this.deleteAccount = this.deleteAccount.bind(this);
this.handlePasswordInputChange = this.handlePasswordInputChange.bind(this);
this.passwordFieldValidation = this.passwordFieldValidation.bind(this);
this.state = {
password: '',
passwordSubmitted: false,
passwordValid: true,
validationMessage: '',
validationErrorDetails: '',
accountQueuedForDeletion: false,
responseError: false,
};
}
addUserToDeletionQueue() {
// TODO: Add API call to add user to account deletion queue
this.setState({
accountQueuedForDeletion: true,
responseError: false,
passwordSubmitted: false,
validationMessage: '',
validationErrorDetails: '',
});
}
deleteAccount() {
const { password } = this.state;
this.setState({ passwordSubmitted: true });
fetch('/accounts/verify_password', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': Cookies.get('csrftoken'),
},
body: JSON.stringify({ password }),
}).then((response) => {
if (response.ok) {
return this.addUserToDeletionQueue();
}
return this.failedSubmission(response);
}).catch(error => this.failedSubmission(error));
}
failedSubmission(error) {
const { status } = error;
const title = status === 403 ? gettext('Password is incorrect') : gettext('Unable to delete account');
const body = status === 403 ? gettext('Please re-enter your password.') : gettext('Sorry, there was an error trying to process your request. Please try again later.');
this.setState({
passwordSubmitted: false,
responseError: true,
passwordValid: false,
validationMessage: title,
validationErrorDetails: body,
});
}
handlePasswordInputChange(value) {
this.setState({ password: value });
}
passwordFieldValidation(value) {
let feedback = { passwordValid: true };
if (value.length < 1) {
feedback = {
passwordValid: false,
validationMessage: gettext('A Password is required'),
validationErrorDetails: '',
};
}
this.setState(feedback);
}
renderConfirmationModal() {
const {
passwordValid,
password,
passwordSubmitted,
responseError,
validationErrorDetails,
validationMessage,
} = this.state;
const { onClose } = this.props;
const loseAccessText = StringUtils.interpolate(
gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a certificate{htmlEnd}.'),
{
htmlStart: '<a href="http://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate" target="_blank">',
htmlEnd: '</a>',
},
);
return (
<div className="delete-confirmation-wrapper">
<Modal
title={gettext('Are you sure?')}
renderHeaderCloseButton={false}
onClose={onClose}
aria-live="polite"
open
body={(
<div>
{responseError &&
<StatusAlert
dialog={(
<div className="modal-alert">
<div className="icon-wrapper">
<Icon id="delete-confirmation-body-error-icon" className={['fa', 'fa-exclamation-circle']} />
</div>
<div className="alert-content">
<h3 className="alert-title">{ validationMessage }</h3>
<p>{ validationErrorDetails }</p>
</div>
</div>
)}
alertType="danger"
dismissible={false}
open
/>
}
<StatusAlert
dialog={(
<div className="modal-alert">
<div className="icon-wrapper">
<Icon id="delete-confirmation-body-warning-icon" className={['fa', 'fa-exclamation-triangle']} />
</div>
<div className="alert-content">
<h3 className="alert-title">{ gettext('You have selected “Delete my account.” Deletion of your account and personal data is permanent and cannot be undone. EdX will not be able to recover your account or the data that is deleted.') }</h3>
<p>{ gettext('If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.') }</p>
<p dangerouslySetInnerHTML={{ __html: loseAccessText }} />
</div>
</div>
)}
dismissible={false}
open
/>
<p className="next-steps">{ gettext('If you still wish to continue and delete your account, please enter your account password:') }</p>
<InputText
name="confirm-password"
label="Password"
type="password"
className={['confirm-password-input']}
onBlur={this.passwordFieldValidation}
isValid={passwordValid}
validationMessage={validationMessage}
onChange={this.handlePasswordInputChange}
autoComplete="new-password"
themes={['danger']}
/>
</div>
)}
closeText={gettext('Cancel')}
buttons={[
<Button
label={gettext('Yes, Delete')}
onClick={this.deleteAccount}
disabled={password.length === 0 || passwordSubmitted}
/>,
]}
/>
</div>
);
}
renderSuccessModal() {
const { onClose } = this.props;
return (
<div className="delete-success-wrapper">
<Modal
title={gettext('We\'re sorry to see you go! Your account will be deleted shortly.')}
renderHeaderCloseButton={false}
body={gettext('Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.')}
onClose={onClose}
aria-live="polite"
open
/>
</div>
);
}
render() {
const { accountQueuedForDeletion } = this.state;
return accountQueuedForDeletion ? this.renderSuccessModal() : this.renderConfirmationModal();
}
}
StudentAccountDeletionConfirmationModal.propTypes = {
onClose: PropTypes.func,
};
StudentAccountDeletionConfirmationModal.defaultProps = {
onClose: () => {},
};
export default StudentAccountDeletionConfirmationModal;

View File

@@ -22,17 +22,17 @@
contactEmail,
allowEmailChange,
socialPlatforms,
syncLearnerProfileData,
enterpriseName,
enterpriseReadonlyAccountFields,
edxSupportUrl,
extendedProfileFields
extendedProfileFields,
enableGDPRFlag
) {
var $accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData,
accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage,
showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField,
emailFieldView, socialFields, platformData,
emailFieldView, socialFields, accountDeletionFields, platformData,
aboutSectionMessageType, aboutSectionMessage, fullnameFieldView, countryFieldView,
fullNameFieldData, emailFieldData, countryFieldData, additionalFields, fieldItem;
@@ -291,6 +291,18 @@
}
aboutSectionsData.push(socialFields);
// Add account deletion fields
if (enableGDPRFlag) {
accountDeletionFields = {
title: gettext('Delete My Account'),
fields: [],
// Used so content can be rendered external to Backbone
domHookId: 'account-deletion-container'
};
aboutSectionsData.push(accountDeletionFields);
}
// set TimeZoneField to listen to CountryField
getUserField = function(list, search) {
return _.find(list, function(field) {

View File

@@ -535,6 +535,98 @@
}
}
.account-deletion-details {
.btn-outline-primary {
@extend %ui-clear-button;
// set styles
@extend %btn-pl-default-base;
@include font-size(18);
border: 1px solid $blue;
color: $blue;
padding: 11px 14px;
line-height: normal;
margin: 20px 0;
}
.paragon__modal-open {
overflow-y: scroll;
color: $dark-gray;
.paragon__modal-title {
font-weight: $font-semibold;
}
.paragon__modal-body {
line-height: 1.5;
.alert-title {
line-height: 1.5;
}
}
.paragon__alert-warning {
color: $dark-gray;
}
.next-steps {
margin-bottom: 10px;
font-weight: $font-semibold;
}
.confirm-password-input {
width: 50%;
}
.paragon__btn:not(.cancel-btn) {
@extend %btn-primary-blue;
}
}
.modal-alert {
display: flex;
.icon-wrapper {
padding-right: 15px;
}
.alert-content {
.alert-title {
color: $dark-gray;
margin-bottom: 10px;
font: {
size: 1rem;
weight: $font-semibold;
}
}
a {
color: $blue-u1;
}
}
}
.delete-confirmation-wrapper {
.paragon__modal-footer {
.paragon__btn-outline-primary {
@extend %ui-clear-button;
// set styles
@extend %btn-pl-default-base;
@include margin-left(25px);
border-color: $blue;
color: $blue;
padding: 11px 14px;
line-height: normal;
}
}
}
}
&:last-child {
border-bottom: none;
}

View File

@@ -8,9 +8,10 @@ from django.conf import settings
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
%>
<!--<%namespace name='static' file='/static_content.html'/>-->
# GDPR Flag
from openedx.features.course_experience import ENABLE_GDPR_COMPAT_FLAG
%>
<%inherit file="/main.html" />
<%def name="online_help_token()"><% return "learneraccountsettings" %></%def>
@@ -25,9 +26,9 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
% endif
<div class="wrapper-account-settings"></div>
<%block name="headextra">
<%static:css group='style-course'/>
<link type="text/css" rel="stylesheet" href="${STATIC_URL}paragon/static/paragon.min.css">
</%block>
<%block name="js_extra">
@@ -44,7 +45,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
enterpriseName = '${ enterprise_name | n, js_escaped_string }',
enterpriseReadonlyAccountFields = ${ enterprise_readonly_account_fields | n, dump_js_escaped_json },
edxSupportUrl = '${ edx_support_url | n, js_escaped_string }',
extendedProfileFields = ${ extended_profile_fields | n, dump_js_escaped_json };
extendedProfileFields = ${ extended_profile_fields | n, dump_js_escaped_json },
enableGDPRFlag = ${ ENABLE_GDPR_COMPAT_FLAG.is_enabled_without_course_context() | n, dump_js_escaped_json };
AccountSettingsFactory(
fieldsData,
@@ -63,7 +65,11 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
enterpriseName,
enterpriseReadonlyAccountFields,
edxSupportUrl,
extendedProfileFields
extendedProfileFields,
enableGDPRFlag
);
</%static:require_module>
% if ENABLE_GDPR_COMPAT_FLAG.is_enabled_without_course_context():
<%static:webpack entry="StudentAccountDeletionInitializer"></%static:webpack>
% endif
</%block>

View File

@@ -17,6 +17,10 @@
</div>
<% } %>
<% if (section.domHookId) { %>
<div id="<%- section.domHookId %>"></div>
<% } %>
<div class="account-settings-section-body <%- tabName %>-section-body">
<div class="ui-loading-error is-hidden">
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>

8
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"integrity": "sha512-MIrtCbJj7CZdW+FQ1hLaNcbqbgcoalxIRobBfgYiLa3oaBnmMh3IkwsYUsYz4hjvzXrUQvL+FsJqq/RB8SAZlg==",
"requires": {
"@edx/edx-bootstrap": "0.4.3",
"@edx/paragon": "2.5.6",
"@edx/paragon": "2.6.4",
"classnames": "2.2.5",
"prop-types": "15.6.1",
"universal-cookie": "2.1.2"
@@ -46,9 +46,9 @@
}
},
"@edx/paragon": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-2.5.6.tgz",
"integrity": "sha512-ASKTOWTZvBUo8ev2SJ9apWBa7u7UKc3UNNdGlqgBzLzj2hNVJ2urxrgATaa1LaMazT23gMZkZM5iKCToQE5Pvw==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-2.6.4.tgz",
"integrity": "sha512-E3n4fzTv2pzhCwhbTfmc7roJ7lEaEqetIRls7xK4U1dVvWW+yra4cyQioLkU30fNc8p8aA3Li3P9zstkqBnq6w==",
"requires": {
"@edx/edx-bootstrap": "1.0.0",
"babel-polyfill": "6.26.0",

View File

@@ -4,7 +4,7 @@
"dependencies": {
"@edx/cookie-policy-banner": "^1.1.3",
"@edx/edx-bootstrap": "0.4.3",
"@edx/paragon": "2.5.6",
"@edx/paragon": "2.6.4",
"@edx/studio-frontend": "1.7.3",
"babel-core": "6.26.0",
"babel-loader": "6.4.1",

View File

@@ -31,6 +31,8 @@ module.exports = {
PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx',
EntitlementSupportPage: './lms/djangoapps/support/static/support/jsx/entitlements/index.jsx',
PasswordResetConfirmation: './lms/static/js/student_account/components/PasswordResetConfirmation.jsx',
StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx',
StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js',
// Learner Dashboard
EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js',