diff --git a/lms/djangoapps/support/static/support/jsx/.eslintrc.js b/lms/djangoapps/support/static/support/jsx/.eslintrc.js
new file mode 100644
index 0000000000..2c5f18759e
--- /dev/null
+++ b/lms/djangoapps/support/static/support/jsx/.eslintrc.js
@@ -0,0 +1,10 @@
+module.exports = {
+ extends: 'eslint-config-edx',
+ root: true,
+ settings: {
+ 'import/resolver': 'webpack',
+ },
+ rules: {
+ 'import/prefer-default-export': 'off',
+ },
+};
diff --git a/lms/djangoapps/support/static/support/jsx/errors_list.jsx b/lms/djangoapps/support/static/support/jsx/errors_list.jsx
new file mode 100644
index 0000000000..f55dad0077
--- /dev/null
+++ b/lms/djangoapps/support/static/support/jsx/errors_list.jsx
@@ -0,0 +1,28 @@
+/* eslint react/no-array-index-key: 0 */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+class ShowErrors extends React.Component {
+
+ render() {
+ window.scrollTo(0, 0);
+ return this.props.errorList.length > 0 &&
+
+
+
{gettext('Please fix the following errors:')}
+
+ {this.props.errorList.map(error =>
+ - {error}
,
+ )}
+
+
+
;
+ }
+}
+
+ShowErrors.propTypes = {
+ errorList: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
+
+export default ShowErrors;
diff --git a/lms/djangoapps/support/static/support/jsx/file_upload.jsx b/lms/djangoapps/support/static/support/jsx/file_upload.jsx
new file mode 100644
index 0000000000..f2160f70e9
--- /dev/null
+++ b/lms/djangoapps/support/static/support/jsx/file_upload.jsx
@@ -0,0 +1,170 @@
+/* global gettext */
+/* eslint one-var: ["error", "always"] */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import ShowProgress from './upload_progress';
+
+
+class FileUpload extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.uploadFile = this.uploadFile.bind(this);
+ this.removeFile = this.removeFile.bind(this);
+ this.state = {
+ fileList: [],
+ fileInProgress: null,
+ };
+ }
+
+ removeFile(e) {
+ e.preventDefault();
+ const fileToken = e.target.id,
+ $this = this,
+ url = `https://arbisoft.zendesk.com/api/v2/uploads/${fileToken}.json`,
+ accessToken = 'd6ed06821334b6584dd9607d04007c281007324ed07e087879c9c44835c684da',
+ request = new XMLHttpRequest();
+
+ request.open('DELETE', url, true);
+ request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
+ request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
+
+ request.send();
+
+ request.onreadystatechange = function removeFile() {
+ if (request.readyState === 4 && request.status === 204) {
+ $this.setState({
+ fileList: $this.state.fileList.filter(file => file.fileToken !== fileToken),
+ });
+ }
+ };
+ }
+
+ uploadFile(e) {
+ const url = 'https://arbisoft.zendesk.com/api/v2/uploads.json?filename=',
+ fileReader = new FileReader(),
+ request = new XMLHttpRequest(),
+ errorList = [],
+ $this = this,
+ file = e.target.files[0],
+ accessToken = 'd6ed06821334b6584dd9607d04007c281007324ed07e087879c9c44835c684da',
+ maxFileSize = 5000000, // 5mb is max limit
+ allowedFileTypes = ['gif', 'png', 'jpg', 'jpeg', 'pdf'];
+
+ // remove file from input and upload it to zendesk after validation
+ $(e.target).val('');
+
+ if (file.size > maxFileSize) {
+ errorList.push(gettext('Files that you upload must be smaller than 5MB in size.'));
+ } else if ($.inArray(file.name.split('.').pop().toLowerCase(), allowedFileTypes) === -1) {
+ errorList.push(gettext('Files that you upload must be PDFs or image files in .gif, .jpg, .jpeg, or .png format.'));
+ }
+
+ this.props.setErrorState(errorList);
+ if (errorList.length > 0) {
+ return;
+ }
+
+ request.open('POST', (url + file.name), true);
+ request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
+ request.setRequestHeader('Content-Type', 'application/binary');
+
+ fileReader.readAsArrayBuffer(file);
+
+ fileReader.onloadend = function success() {
+ $this.setState({
+ fileInProgress: file.name,
+ currentRequest: request,
+ });
+ request.send(fileReader.result);
+ };
+
+ request.upload.onprogress = function renderProgress(event) {
+ if (event.lengthComputable) {
+ const percentComplete = (event.loaded / event.total) * 100;
+ $('.progress-bar-striped').css({ width: `${percentComplete}%` });
+ }
+ };
+
+ request.onreadystatechange = function success() {
+ if (request.readyState === 4 && request.status === 201) {
+ const uploadedFile = {
+ fileName: file.name,
+ fileToken: JSON.parse(request.response).upload.token,
+ };
+
+ $this.setState(
+ {
+ fileList: $this.state.fileList.concat(uploadedFile),
+ fileInProgress: null,
+ },
+ );
+ }
+ };
+
+ request.onerror = function error() {
+ $this.setState({
+ fileInProgress: null,
+ errorList: [gettext('Something went wrong. Please try again later.')],
+ });
+ };
+
+ request.onabort = function abortUpload() {
+ $this.setState({
+ fileInProgress: null,
+ });
+ };
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {this.state.fileInProgress &&
+
+ }
+
+
+ {
+ this.state.fileList.map(file =>
+ (
+
+ {file.fileName}
+
+
+
+
+
),
+ )
+ }
+
+
+ );
+ }
+}
+
+FileUpload.propTypes = {
+ setErrorState: PropTypes.func.isRequired,
+};
+export default FileUpload;
diff --git a/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx b/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx
new file mode 100644
index 0000000000..7cd223476c
--- /dev/null
+++ b/lms/djangoapps/support/static/support/jsx/logged_in_user.jsx
@@ -0,0 +1,49 @@
+/* global gettext */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function LoggedInUser({ userInformation }) {
+ return (
+
+
+
{gettext(`What can we help you with, ${userInformation.username}?`)}
+
+
+
+
+
+
+ {userInformation.enrollments.length === 0 &&
+
+
+
+
+ }
+ {userInformation.enrollments.length > 0 &&
+
+
+
+
+ }
+
+
+
+
);
+}
+
+LoggedInUser.propTypes = {
+ userInformation: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
+
+export default LoggedInUser;
diff --git a/lms/djangoapps/support/static/support/jsx/logged_out_user.jsx b/lms/djangoapps/support/static/support/jsx/logged_out_user.jsx
new file mode 100644
index 0000000000..84cce2fa71
--- /dev/null
+++ b/lms/djangoapps/support/static/support/jsx/logged_out_user.jsx
@@ -0,0 +1,48 @@
+/* global gettext */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function LoggedOutUser({ loginUrl }) {
+ return (
+
+
+
+
{gettext('Sign in to edX so we can help you better.')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+LoggedOutUser.propTypes = {
+ loginUrl: PropTypes.string.isRequired,
+};
+
+export default LoggedOutUser;
diff --git a/lms/djangoapps/support/static/support/jsx/single_support_form.jsx b/lms/djangoapps/support/static/support/jsx/single_support_form.jsx
new file mode 100644
index 0000000000..a738eb9310
--- /dev/null
+++ b/lms/djangoapps/support/static/support/jsx/single_support_form.jsx
@@ -0,0 +1,211 @@
+/* global gettext */
+/* eslint one-var: ["error", "always"] */
+/* eslint no-alert: "error" */
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import FileUpload from './file_upload';
+import ShowErrors from './errors_list';
+import LoggedInUser from './logged_in_user';
+import LoggedOutUser from './logged_out_user';
+
+// TODO
+// edx zendesk APIs
+// access token
+// custom fields ids
+// https://openedx.atlassian.net/browse/LEARNER-2736
+// https://openedx.atlassian.net/browse/LEARNER-2735
+
+class RenderForm extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ currentRequest: null,
+ errorList: [],
+ };
+ this.submitForm = this.submitForm.bind(this);
+ this.setErrorState = this.setErrorState.bind(this);
+ }
+
+ setErrorState(errors) {
+ this.setState({
+ errorList: errors,
+ });
+ }
+
+ submitForm() {
+ const url = 'https://arbisoft.zendesk.com/api/v2/tickets.json',
+ $userInfo = $('.user-info'),
+ request = new XMLHttpRequest(),
+ $course = $('#course'),
+ accessToken = 'd6ed06821334b6584dd9607d04007c281007324ed07e087879c9c44835c684da',
+ data = {
+ subject: $('#subject').val(),
+ comment: {
+ body: $('#message').val(),
+ uploads: $.map($('.uploaded-files button'), n => n.id),
+ },
+ };
+
+ let course;
+
+ if ($userInfo.length) {
+ data.requester = $userInfo.data('email');
+ course = $course.find(':selected').text();
+ if (!course.length) {
+ course = $course.val();
+ }
+ } else {
+ data.requester = $('#email').val();
+ course = $course.val();
+ }
+
+ data.custom_fields = [{
+ id: '114099484092',
+ value: course,
+ }];
+
+ if (this.validateData(data)) {
+ request.open('POST', url, true);
+ request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
+ request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
+
+ request.send(JSON.stringify({
+ ticket: data,
+ }));
+
+ request.onreadystatechange = function success() {
+ if (request.readyState === 4 && request.status === 201) {
+ // TODO needs to remove after implementing success page
+ const alert = 'Request submitted successfully.';
+ alert();
+ }
+ };
+
+ request.onerror = function error() {
+ this.setErrorState([gettext('Something went wrong. Please try again later.')]);
+ }.bind(this);
+ }
+ }
+
+ validateData(data) {
+ const errors = [],
+ regex = /^([a-zA-Z0-9_.+-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
+
+ if (!data.requester) {
+ errors.push(gettext('Enter a valid email address.'));
+ $('#email').closest('.form-group').addClass('has-error');
+ } else if (!regex.test(data.requester)) {
+ errors.push(gettext('Enter a valid email address.'));
+ $('#email').closest('.form-group').addClass('has-error');
+ }
+ if (!data.subject) {
+ errors.push(gettext('Enter a subject for your support request.'));
+ $('#subject').closest('.form-group').addClass('has-error');
+ }
+ if (!data.comment.body) {
+ errors.push(gettext('Enter some details for your support request.'));
+ $('#message').closest('.form-group').addClass('has-error');
+ }
+
+ if (!errors.length) {
+ return true;
+ }
+
+ this.setErrorState(errors);
+ return false;
+ }
+
+ render() {
+ let userElement;
+ if (this.props.context.user) {
+ userElement = ;
+ } else {
+ userElement = ;
+ }
+
+ return (
+
+
+
+
+
{gettext('Contact Us')}
+
+
+
+
+
+
+
+
+
+
{gettext('Your question might have already been answered.')}
+
+
+
+
+
+ {userElement}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{gettext('The more you tell us, the more quickly and helpfully we can respond!')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+RenderForm.propTypes = {
+ context: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
+
+export class SingleSupportForm {
+ constructor(context) {
+ ReactDOM.render(
+ ,
+ document.getElementById('root'),
+ );
+ }
+}
diff --git a/lms/djangoapps/support/static/support/jsx/upload_progress.jsx b/lms/djangoapps/support/static/support/jsx/upload_progress.jsx
new file mode 100644
index 0000000000..4792d628a5
--- /dev/null
+++ b/lms/djangoapps/support/static/support/jsx/upload_progress.jsx
@@ -0,0 +1,41 @@
+/* global gettext */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+class ShowProgress extends React.Component {
+ constructor(props) {
+ super(props);
+ this.abortRequest = this.abortRequest.bind(this);
+ }
+
+ abortRequest(e) {
+ e.preventDefault();
+ this.props.request.abort();
+ }
+
+ render() {
+ return (
+
+
+
+
{this.props.fileName}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+ShowProgress.propTypes = {
+ fileName: PropTypes.string.isRequired,
+ request: PropTypes.objectOf(XMLHttpRequest).isRequired,
+};
+
+export default ShowProgress;
diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py
index 2e639f670e..64dea2f333 100644
--- a/lms/djangoapps/support/tests/test_views.py
+++ b/lms/djangoapps/support/tests/test_views.py
@@ -380,46 +380,3 @@ class SupportViewEnrollmentsTests(SharedModuleStoreTestCase, SupportViewTestCase
)
verified_mode.expiration_datetime = datetime(year=1970, month=1, day=9, tzinfo=UTC)
verified_mode.save()
-
-
-class ContactUsViewTests(ModuleStoreTestCase):
-
- url = reverse('support:contact_us')
-
- def setUp(self):
- super(ContactUsViewTests, self).setUp()
- self.user = UserFactory()
- self.client.login(username=self.user.username, password='test')
- self.user_enrollment = CourseEnrollmentFactory.create(
- user=self.user,
- )
-
- def test_get_with_logged_in_user(self):
- """ Verify that logged in users will see courses dropdown."""
- response = self.client.get(self.url)
- expected = '