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';
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},
}
......
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."""
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
}
)
......@@ -68,40 +68,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': {
'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
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,6 +24,7 @@ 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)
if tags:
# Remove duplicates from tags list
tags = list(set(tags))
......@@ -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
}
}
......
"""
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