Merge pull request #16949 from edx/tasawer/learner-3594/update-zendesk-proxy-and-frontend-jsx
zendesk proxy and front end of support form updated
This commit is contained in:
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import FileUpload from './file_upload';
|
||||
|
||||
function LoggedInUser({ userInformation, setErrorState, zendeskApiHost, accessToken, submitForm }) {
|
||||
function LoggedInUser({ userInformation, setErrorState, zendeskApiHost, submitForm }) {
|
||||
let courseElement;
|
||||
if (userInformation.enrollments) {
|
||||
courseElement = (<div>
|
||||
@@ -70,11 +70,12 @@ function LoggedInUser({ userInformation, setErrorState, zendeskApiHost, accessTo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileUpload
|
||||
setErrorState={setErrorState}
|
||||
zendeskApiHost={zendeskApiHost}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
{/*TODO file uploading will be done after initial release*/}
|
||||
{/*<FileUpload*/}
|
||||
{/*setErrorState={setErrorState}*/}
|
||||
{/*zendeskApiHost={zendeskApiHost}*/}
|
||||
{/*accessToken={accessToken}*/}
|
||||
{/*/>*/}
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
@@ -91,8 +92,7 @@ LoggedInUser.propTypes = {
|
||||
setErrorState: PropTypes.func.isRequired,
|
||||
submitForm: PropTypes.func.isRequired,
|
||||
userInformation: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
zendeskApiHost: PropTypes.string.isRequired,
|
||||
accessToken: PropTypes.string.isRequired,
|
||||
zendeskProxyUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LoggedInUser;
|
||||
|
||||
@@ -31,7 +31,7 @@ class RenderForm extends React.Component {
|
||||
}
|
||||
|
||||
submitForm() {
|
||||
const url = `${this.props.context.zendeskApiHost}/api/v2/tickets.json`,
|
||||
const url = this.props.context.zendeskProxyUrl,
|
||||
$userInfo = $('.user-info'),
|
||||
request = new XMLHttpRequest(),
|
||||
$course = $('#course'),
|
||||
@@ -39,14 +39,17 @@ class RenderForm extends React.Component {
|
||||
subject: $('#subject').val(),
|
||||
comment: {
|
||||
body: $('#message').val(),
|
||||
uploads: $.map($('.uploaded-files button'), n => n.id),
|
||||
},
|
||||
tags: this.props.context.zendeskTags,
|
||||
};
|
||||
|
||||
let course;
|
||||
|
||||
data.requester = $userInfo.data('email');
|
||||
data.requester = {
|
||||
email: $userInfo.data('email'),
|
||||
name: $userInfo.data('username')
|
||||
};
|
||||
|
||||
course = $course.find(':selected').val();
|
||||
if (!course) {
|
||||
course = $course.val();
|
||||
@@ -59,12 +62,10 @@ class RenderForm extends React.Component {
|
||||
|
||||
if (this.validateData(data)) {
|
||||
request.open('POST', url, true);
|
||||
request.setRequestHeader('Authorization', `Bearer ${this.props.context.accessToken}`);
|
||||
request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
|
||||
request.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
|
||||
request.setRequestHeader('X-CSRFToken', $.cookie('csrftoken'));
|
||||
|
||||
request.send(JSON.stringify({
|
||||
ticket: data,
|
||||
}));
|
||||
request.send(JSON.stringify(data));
|
||||
|
||||
request.onreadystatechange = function success() {
|
||||
if (request.readyState === 4 && request.status === 201) {
|
||||
@@ -81,16 +82,7 @@ class RenderForm extends React.Component {
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
const errors = [];
|
||||
if (!data.subject) {
|
||||
errors.push(gettext('Enter a subject for your support request.'));
|
||||
$('#subject').closest('.form-group').addClass('has-error');
|
||||
@@ -124,8 +116,7 @@ class RenderForm extends React.Component {
|
||||
if (this.props.context.user) {
|
||||
userElement = (<LoggedInUser
|
||||
userInformation={this.props.context.user}
|
||||
zendeskApiHost={this.props.context.zendeskApiHost}
|
||||
accessToken={this.props.context.accessToken}
|
||||
zendeskProxyUrl={this.props.context.zendeskProxyUrl}
|
||||
setErrorState={this.setErrorState}
|
||||
submitForm={this.submitForm}
|
||||
/>);
|
||||
|
||||
@@ -18,8 +18,6 @@ class ContactUsView(View):
|
||||
def get(self, request):
|
||||
context = {
|
||||
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
|
||||
'zendesk_api_host': settings.ZENDESK_URL,
|
||||
'access_token': 'DUMMY_ACCESS_TOKEN', # LEARNER-3450
|
||||
'custom_fields': settings.ZENDESK_CUSTOM_FIELDS
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string, dump_js_escaped_j
|
||||
'loginQuery': "${login_query() | n, js_escaped_string}",
|
||||
'dashboardUrl': "${reverse('dashboard') | n, js_escaped_string}",
|
||||
'homepageUrl': "${marketing_link('ROOT') | n, js_escaped_string}",
|
||||
'zendeskApiHost': "${zendesk_api_host | n, js_escaped_string}",
|
||||
'accessToken': "${access_token | n, js_escaped_string}",
|
||||
'zendeskProxyUrl': "${reverse('zendesk_proxy_v1') | n, js_escaped_string}",
|
||||
'customFields': ${custom_fields | n, dump_js_escaped_json},
|
||||
'zendeskTags': ${zendesk_tags | n, dump_js_escaped_json},
|
||||
}
|
||||
|
||||
58
openedx/core/djangoapps/zendesk_proxy/tests/test_utils.py
Normal file
58
openedx/core/djangoapps/zendesk_proxy/tests/test_utils.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import ddt
|
||||
from django.test.utils import override_settings
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from openedx.core.djangoapps.zendesk_proxy.utils import create_zendesk_ticket
|
||||
from openedx.core.lib.api.test_utils import ApiTestCase
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(
|
||||
ZENDESK_URL="https://www.superrealurlsthataredefinitelynotfake.com",
|
||||
ZENDESK_OAUTH_ACCESS_TOKEN="abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
)
|
||||
class TestUtils(ApiTestCase):
|
||||
def setUp(self):
|
||||
self.request_data = {
|
||||
'email': 'JohnQStudent@example.com',
|
||||
'name': 'John Q. Student',
|
||||
'subject': 'Python Unit Test Help Request',
|
||||
'body': "Help! I'm trapped in a unit test factory and I can't get out!",
|
||||
}
|
||||
return super(TestUtils, self).setUp()
|
||||
|
||||
@override_settings(
|
||||
ZENDESK_URL=None,
|
||||
ZENDESK_OAUTH_ACCESS_TOKEN=None
|
||||
)
|
||||
def test_missing_settings(self):
|
||||
status_code = create_zendesk_ticket(
|
||||
requester_name=self.request_data['name'],
|
||||
requester_email=self.request_data['email'],
|
||||
subject=self.request_data['subject'],
|
||||
body=self.request_data['body'],
|
||||
)
|
||||
|
||||
self.assertEqual(status_code, 503)
|
||||
|
||||
@ddt.data(201, 400, 401, 403, 404, 500)
|
||||
def test_zendesk_status_codes(self, mock_code):
|
||||
with patch('requests.post', return_value=MagicMock(status_code=mock_code)):
|
||||
status_code = create_zendesk_ticket(
|
||||
requester_name=self.request_data['name'],
|
||||
requester_email=self.request_data['email'],
|
||||
subject=self.request_data['subject'],
|
||||
body=self.request_data['body'],
|
||||
)
|
||||
|
||||
self.assertEqual(status_code, mock_code)
|
||||
|
||||
def test_unexpected_error_pinging_zendesk(self):
|
||||
with patch('requests.post', side_effect=Exception("WHAMMY")):
|
||||
status_code = create_zendesk_ticket(
|
||||
requester_name=self.request_data['name'],
|
||||
requester_email=self.request_data['email'],
|
||||
subject=self.request_data['subject'],
|
||||
body=self.request_data['body'],
|
||||
)
|
||||
self.assertEqual(status_code, 500)
|
||||
@@ -1,14 +1,14 @@
|
||||
"""Tests for zendesk_proxy views."""
|
||||
from copy import deepcopy
|
||||
import ddt
|
||||
import json
|
||||
from mock import MagicMock, patch
|
||||
from copy import deepcopy
|
||||
|
||||
import ddt
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from openedx.core.lib.api.test_utils import ApiTestCase
|
||||
from openedx.core.djangoapps.zendesk_proxy.v0.views import ZENDESK_REQUESTS_PER_HOUR
|
||||
from openedx.core.lib.api.test_utils import ApiTestCase
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -50,7 +50,7 @@ class ZendeskProxyTestCase(ApiTestCase):
|
||||
'content-type': 'application/json',
|
||||
'Authorization': 'Bearer abcdefghijklmnopqrstuvwxyz1234567890'
|
||||
},
|
||||
'data': '{"ticket": {"comment": {"body": "Help! I\'m trapped in a unit test factory and I can\'t get out!"}, "subject": "Python Unit Test Help Request", "tags": ["python_unit_test"], "requester": {"name": "John Q. Student", "email": "JohnQStudent@example.com"}}}' # pylint: disable=line-too-long
|
||||
'data': '{"ticket": {"comment": {"body": "Help! I\'m trapped in a unit test factory and I can\'t get out!", "uploads": null}, "tags": ["python_unit_test"], "subject": "Python Unit Test Help Request", "custom_fields": null, "requester": {"name": "John Q. Student", "email": "JohnQStudent@example.com"}}}' # pylint: disable=line-too-long
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,40 +67,6 @@ class ZendeskProxyTestCase(ApiTestCase):
|
||||
)
|
||||
self.assertHttpBadRequest(response)
|
||||
|
||||
@override_settings(
|
||||
ZENDESK_URL=None,
|
||||
ZENDESK_OAUTH_ACCESS_TOKEN=None
|
||||
)
|
||||
def test_missing_settings(self):
|
||||
response = self.request_without_auth(
|
||||
'post',
|
||||
self.url,
|
||||
data=json.dumps(self.request_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 503)
|
||||
|
||||
@ddt.data(201, 400, 401, 403, 404, 500)
|
||||
def test_zendesk_status_codes(self, mock_code):
|
||||
with patch('requests.post', return_value=MagicMock(status_code=mock_code)):
|
||||
response = self.request_without_auth(
|
||||
'post',
|
||||
self.url,
|
||||
data=json.dumps(self.request_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, mock_code)
|
||||
|
||||
def test_unexpected_error_pinging_zendesk(self):
|
||||
with patch('requests.post', side_effect=Exception("WHAMMY")):
|
||||
response = self.request_without_auth(
|
||||
'post',
|
||||
self.url,
|
||||
data=json.dumps(self.request_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
@override_settings(
|
||||
CACHES={
|
||||
'default': {
|
||||
95
openedx/core/djangoapps/zendesk_proxy/tests/test_v1_views.py
Normal file
95
openedx/core/djangoapps/zendesk_proxy/tests/test_v1_views.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Tests for zendesk_proxy views."""
|
||||
import json
|
||||
from copy import deepcopy
|
||||
|
||||
import ddt
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from openedx.core.djangoapps.zendesk_proxy.v1.views import ZendeskProxyThrottle
|
||||
from openedx.core.lib.api.test_utils import ApiTestCase
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(
|
||||
ZENDESK_URL="https://www.superrealurlsthataredefinitelynotfake.com",
|
||||
ZENDESK_OAUTH_ACCESS_TOKEN="abcdefghijklmnopqrstuvwxyz1234567890"
|
||||
)
|
||||
class ZendeskProxyTestCase(ApiTestCase):
|
||||
"""Tests for zendesk_proxy views."""
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse('zendesk_proxy_v1')
|
||||
self.request_data = {
|
||||
'requester': {
|
||||
'email': 'JohnQStudent@example.com',
|
||||
'name': 'John Q. Student'
|
||||
},
|
||||
'subject': 'Python Unit Test Help Request',
|
||||
'comment': {
|
||||
'body': "Help! I'm trapped in a unit test factory and I can't get out!",
|
||||
},
|
||||
'tags': ['python_unit_test'],
|
||||
'custom_fields': [
|
||||
{
|
||||
'id': '001',
|
||||
'value': 'demo-course'
|
||||
}
|
||||
],
|
||||
}
|
||||
return super(ZendeskProxyTestCase, self).setUp()
|
||||
|
||||
def test_post(self):
|
||||
with patch('requests.post', return_value=MagicMock(status_code=201)) as mock_post:
|
||||
response = self.request_without_auth(
|
||||
'post',
|
||||
self.url,
|
||||
data=json.dumps(self.request_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertHttpCreated(response)
|
||||
(mock_args, mock_kwargs) = mock_post.call_args
|
||||
self.assertEqual(mock_args, ('https://www.superrealurlsthataredefinitelynotfake.com/api/v2/tickets.json',))
|
||||
self.assertEqual(
|
||||
mock_kwargs,
|
||||
{
|
||||
'headers': {
|
||||
'content-type': 'application/json',
|
||||
'Authorization': 'Bearer abcdefghijklmnopqrstuvwxyz1234567890'
|
||||
},
|
||||
'data': '{"ticket": {"comment": {"body": "Help! I\'m trapped in a unit test factory and I can\'t get out!", "uploads": null}, "tags": ["python_unit_test"], "subject": "Python Unit Test Help Request", "custom_fields": [{"id": "001", "value": "demo-course"}], "requester": {"name": "John Q. Student", "email": "JohnQStudent@example.com"}}}' # pylint: disable=line-too-long
|
||||
}
|
||||
)
|
||||
|
||||
@ddt.data('requester', 'tags')
|
||||
def test_bad_request(self, key_to_delete):
|
||||
test_data = deepcopy(self.request_data)
|
||||
_ = test_data.pop(key_to_delete)
|
||||
|
||||
response = self.request_without_auth(
|
||||
'post',
|
||||
self.url,
|
||||
data=json.dumps(test_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.assertHttpBadRequest(response)
|
||||
|
||||
@override_settings(
|
||||
CACHES={
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'zendesk_proxy',
|
||||
}
|
||||
}
|
||||
)
|
||||
def test_rate_limiting(self):
|
||||
"""
|
||||
Confirm rate limits work as expected. Note that drf's rate limiting makes use of the default cache to enforce
|
||||
limits; that's why this test needs a "real" default cache (as opposed to the usual-for-tests DummyCache)
|
||||
"""
|
||||
|
||||
for _ in range(ZendeskProxyThrottle().num_requests):
|
||||
self.request_without_auth('post', self.url)
|
||||
response = self.request_without_auth('post', self.url)
|
||||
self.assertEqual(response.status_code, 429)
|
||||
@@ -5,7 +5,9 @@ Map urls to the relevant view handlers
|
||||
from django.conf.urls import url
|
||||
|
||||
from openedx.core.djangoapps.zendesk_proxy.v0.views import ZendeskPassthroughView as v0_view
|
||||
from openedx.core.djangoapps.zendesk_proxy.v1.views import ZendeskPassthroughView as v1_view
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^v0$', v0_view.as_view(), name='zendesk_proxy_v0'),
|
||||
url(r'^v1$', v1_view.as_view(), name='zendesk_proxy_v1'),
|
||||
]
|
||||
|
||||
@@ -12,7 +12,7 @@ from rest_framework import status
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=None):
|
||||
def create_zendesk_ticket(requester_name, requester_email, subject, body, custom_fields=None, uploads=None, tags=None):
|
||||
"""
|
||||
Create a Zendesk ticket via API.
|
||||
|
||||
@@ -24,8 +24,9 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N
|
||||
"""Internal helper to standardize error message. This allows for simpler splunk alerts."""
|
||||
return 'zendesk_proxy action required\n{}\nNo ticket created for payload {}'.format(details, payload)
|
||||
|
||||
# Remove duplicates from tags list
|
||||
tags = list(set(tags))
|
||||
if tags:
|
||||
# Remove duplicates from tags list
|
||||
tags = list(set(tags))
|
||||
|
||||
data = {
|
||||
'ticket': {
|
||||
@@ -34,7 +35,11 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N
|
||||
'email': requester_email
|
||||
},
|
||||
'subject': subject,
|
||||
'comment': {'body': body},
|
||||
'comment': {
|
||||
'body': body,
|
||||
'uploads': uploads
|
||||
},
|
||||
'custom_fields': custom_fields,
|
||||
'tags': tags
|
||||
}
|
||||
}
|
||||
|
||||
69
openedx/core/djangoapps/zendesk_proxy/v1/views.py
Normal file
69
openedx/core/djangoapps/zendesk_proxy/v1/views.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Define request handlers used by the zendesk_proxy djangoapp
|
||||
"""
|
||||
from rest_framework import status
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from openedx.core.djangoapps.zendesk_proxy.utils import create_zendesk_ticket
|
||||
|
||||
REQUESTS_PER_HOUR = 50
|
||||
|
||||
|
||||
class ZendeskProxyThrottle(UserRateThrottle):
|
||||
"""
|
||||
Custom throttle rates for this particular endpoint's use case.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.rate = '{}/hour'.format(REQUESTS_PER_HOUR)
|
||||
super(ZendeskProxyThrottle, self).__init__()
|
||||
|
||||
|
||||
class ZendeskPassthroughView(APIView):
|
||||
"""
|
||||
An APIView that will take in inputs from an unauthenticated endpoint, and use them to securely create a zendesk
|
||||
ticket.
|
||||
"""
|
||||
throttle_classes = ZendeskProxyThrottle,
|
||||
parser_classes = JSONParser,
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
request body is expected to look like this:
|
||||
{
|
||||
"requester": {
|
||||
"email": "john@example.com",
|
||||
"name": "name"
|
||||
},
|
||||
"subject": "test subject",
|
||||
"comment": {
|
||||
"body": "message details",
|
||||
"uploads": ['file_token'],
|
||||
},
|
||||
"custom_fields": [
|
||||
{
|
||||
"id": '001',
|
||||
"value": 'demo-course'
|
||||
}
|
||||
],
|
||||
"tags": ["LMS"]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
proxy_status = create_zendesk_ticket(
|
||||
requester_name=request.data['requester']['name'],
|
||||
requester_email=request.data['requester']['email'],
|
||||
subject=request.data['subject'],
|
||||
body=request.data['comment']['body'],
|
||||
custom_fields=request.data['custom_fields'],
|
||||
tags=request.data['tags']
|
||||
)
|
||||
except KeyError:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
status=proxy_status
|
||||
)
|
||||
Reference in New Issue
Block a user