Unverified Commit d00697f5 by Tasawer Nawaz Committed by GitHub

Merge pull request #16949 from edx/tasawer/learner-3594/update-zendesk-proxy-and-frontend-jsx

zendesk proxy and front end of support form updated
parents dbc7b894 82051d03
...@@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; ...@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import FileUpload from './file_upload'; import FileUpload from './file_upload';
function LoggedInUser({ userInformation, setErrorState, zendeskApiHost, accessToken, submitForm }) { function LoggedInUser({ userInformation, setErrorState, zendeskApiHost, submitForm }) {
let courseElement; let courseElement;
if (userInformation.enrollments) { if (userInformation.enrollments) {
courseElement = (<div> courseElement = (<div>
...@@ -70,11 +70,12 @@ function LoggedInUser({ userInformation, setErrorState, zendeskApiHost, accessTo ...@@ -70,11 +70,12 @@ function LoggedInUser({ userInformation, setErrorState, zendeskApiHost, accessTo
</div> </div>
</div> </div>
<FileUpload {/*TODO file uploading will be done after initial release*/}
setErrorState={setErrorState} {/*<FileUpload*/}
zendeskApiHost={zendeskApiHost} {/*setErrorState={setErrorState}*/}
accessToken={accessToken} {/*zendeskApiHost={zendeskApiHost}*/}
/> {/*accessToken={accessToken}*/}
{/*/>*/}
<div className="row"> <div className="row">
<div className="col-sm-12"> <div className="col-sm-12">
...@@ -91,8 +92,7 @@ LoggedInUser.propTypes = { ...@@ -91,8 +92,7 @@ LoggedInUser.propTypes = {
setErrorState: PropTypes.func.isRequired, setErrorState: PropTypes.func.isRequired,
submitForm: PropTypes.func.isRequired, submitForm: PropTypes.func.isRequired,
userInformation: PropTypes.arrayOf(PropTypes.object).isRequired, userInformation: PropTypes.arrayOf(PropTypes.object).isRequired,
zendeskApiHost: PropTypes.string.isRequired, zendeskProxyUrl: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
}; };
export default LoggedInUser; export default LoggedInUser;
...@@ -31,7 +31,7 @@ class RenderForm extends React.Component { ...@@ -31,7 +31,7 @@ class RenderForm extends React.Component {
} }
submitForm() { submitForm() {
const url = `${this.props.context.zendeskApiHost}/api/v2/tickets.json`, const url = this.props.context.zendeskProxyUrl,
$userInfo = $('.user-info'), $userInfo = $('.user-info'),
request = new XMLHttpRequest(), request = new XMLHttpRequest(),
$course = $('#course'), $course = $('#course'),
...@@ -39,14 +39,17 @@ class RenderForm extends React.Component { ...@@ -39,14 +39,17 @@ class RenderForm extends React.Component {
subject: $('#subject').val(), subject: $('#subject').val(),
comment: { comment: {
body: $('#message').val(), body: $('#message').val(),
uploads: $.map($('.uploaded-files button'), n => n.id),
}, },
tags: this.props.context.zendeskTags, tags: this.props.context.zendeskTags,
}; };
let course; let course;
data.requester = $userInfo.data('email'); data.requester = {
email: $userInfo.data('email'),
name: $userInfo.data('username')
};
course = $course.find(':selected').val(); course = $course.find(':selected').val();
if (!course) { if (!course) {
course = $course.val(); course = $course.val();
...@@ -59,12 +62,10 @@ class RenderForm extends React.Component { ...@@ -59,12 +62,10 @@ class RenderForm extends React.Component {
if (this.validateData(data)) { if (this.validateData(data)) {
request.open('POST', url, true); 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({ request.send(JSON.stringify(data));
ticket: data,
}));
request.onreadystatechange = function success() { request.onreadystatechange = function success() {
if (request.readyState === 4 && request.status === 201) { if (request.readyState === 4 && request.status === 201) {
...@@ -81,16 +82,7 @@ class RenderForm extends React.Component { ...@@ -81,16 +82,7 @@ class RenderForm extends React.Component {
} }
validateData(data) { validateData(data) {
const errors = [], 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) { if (!data.subject) {
errors.push(gettext('Enter a subject for your support request.')); errors.push(gettext('Enter a subject for your support request.'));
$('#subject').closest('.form-group').addClass('has-error'); $('#subject').closest('.form-group').addClass('has-error');
...@@ -124,8 +116,7 @@ class RenderForm extends React.Component { ...@@ -124,8 +116,7 @@ class RenderForm extends React.Component {
if (this.props.context.user) { if (this.props.context.user) {
userElement = (<LoggedInUser userElement = (<LoggedInUser
userInformation={this.props.context.user} userInformation={this.props.context.user}
zendeskApiHost={this.props.context.zendeskApiHost} zendeskProxyUrl={this.props.context.zendeskProxyUrl}
accessToken={this.props.context.accessToken}
setErrorState={this.setErrorState} setErrorState={this.setErrorState}
submitForm={this.submitForm} submitForm={this.submitForm}
/>); />);
......
...@@ -18,8 +18,6 @@ class ContactUsView(View): ...@@ -18,8 +18,6 @@ class ContactUsView(View):
def get(self, request): def get(self, request):
context = { context = {
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME), '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 'custom_fields': settings.ZENDESK_CUSTOM_FIELDS
} }
......
...@@ -33,8 +33,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string, dump_js_escaped_j ...@@ -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}", 'loginQuery': "${login_query() | n, js_escaped_string}",
'dashboardUrl': "${reverse('dashboard') | n, js_escaped_string}", 'dashboardUrl': "${reverse('dashboard') | n, js_escaped_string}",
'homepageUrl': "${marketing_link('ROOT') | n, js_escaped_string}", 'homepageUrl': "${marketing_link('ROOT') | n, js_escaped_string}",
'zendeskApiHost': "${zendesk_api_host | n, js_escaped_string}", 'zendeskProxyUrl': "${reverse('zendesk_proxy_v1') | n, js_escaped_string}",
'accessToken': "${access_token | n, js_escaped_string}",
'customFields': ${custom_fields | n, dump_js_escaped_json}, 'customFields': ${custom_fields | n, dump_js_escaped_json},
'zendeskTags': ${zendesk_tags | n, dump_js_escaped_json}, 'zendeskTags': ${zendesk_tags | n, dump_js_escaped_json},
} }
......
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)
"""Tests for zendesk_proxy views.""" """Tests for zendesk_proxy views."""
from copy import deepcopy
import ddt
import json import json
from mock import MagicMock, patch from copy import deepcopy
import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.utils import override_settings 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.djangoapps.zendesk_proxy.v0.views import ZENDESK_REQUESTS_PER_HOUR
from openedx.core.lib.api.test_utils import ApiTestCase
@ddt.ddt @ddt.ddt
...@@ -50,7 +50,7 @@ class ZendeskProxyTestCase(ApiTestCase): ...@@ -50,7 +50,7 @@ class ZendeskProxyTestCase(ApiTestCase):
'content-type': 'application/json', 'content-type': 'application/json',
'Authorization': 'Bearer abcdefghijklmnopqrstuvwxyz1234567890' '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
} }
) )
...@@ -68,40 +68,6 @@ class ZendeskProxyTestCase(ApiTestCase): ...@@ -68,40 +68,6 @@ class ZendeskProxyTestCase(ApiTestCase):
self.assertHttpBadRequest(response) self.assertHttpBadRequest(response)
@override_settings( @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={ CACHES={
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
......
"""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 ...@@ -5,7 +5,9 @@ Map urls to the relevant view handlers
from django.conf.urls import url 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.v0.views import ZendeskPassthroughView as v0_view
from openedx.core.djangoapps.zendesk_proxy.v1.views import ZendeskPassthroughView as v1_view
urlpatterns = [ urlpatterns = [
url(r'^v0$', v0_view.as_view(), name='zendesk_proxy_v0'), 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 ...@@ -12,7 +12,7 @@ from rest_framework import status
log = logging.getLogger(__name__) 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. Create a Zendesk ticket via API.
...@@ -24,8 +24,9 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N ...@@ -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.""" """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) return 'zendesk_proxy action required\n{}\nNo ticket created for payload {}'.format(details, payload)
# Remove duplicates from tags list if tags:
tags = list(set(tags)) # Remove duplicates from tags list
tags = list(set(tags))
data = { data = {
'ticket': { 'ticket': {
...@@ -34,7 +35,11 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N ...@@ -34,7 +35,11 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, tags=N
'email': requester_email 'email': requester_email
}, },
'subject': subject, 'subject': subject,
'comment': {'body': body}, 'comment': {
'body': body,
'uploads': uploads
},
'custom_fields': custom_fields,
'tags': tags 'tags': tags
} }
} }
......
"""
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
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment