Commit 2d85fe4a by Anthony Mangano Committed by GitHub

Merge pull request #14279 from…

Merge pull request #14279 from edx/ECOM-5936-send-course-id-and-enrollment-track-to-zendesk-custom-fields

Ecom 5936 enable users to select course when making support requests
parents 6572b1df 38d8017d
...@@ -11,8 +11,19 @@ from util import views ...@@ -11,8 +11,19 @@ from util import views
from zendesk import ZendeskError from zendesk import ZendeskError
import json import json
import mock import mock
from ddt import ddt, data, unpack
from student.tests.test_configuration_overrides import fake_get_value from student.tests.test_configuration_overrides import fake_get_value
from student.tests.factories import CourseEnrollmentFactory
TEST_SUPPORT_EMAIL = "support@example.com"
TEST_ZENDESK_CUSTOM_FIELD_CONFIG = {"course_id": 1234, "enrollment_mode": 5678}
TEST_REQUEST_HEADERS = {
"HTTP_REFERER": "test_referer",
"HTTP_USER_AGENT": "test_user_agent",
"REMOTE_ADDR": "1.2.3.4",
"SERVER_NAME": "test_server",
}
def fake_support_backend_values(name, default=None): # pylint: disable=unused-argument def fake_support_backend_values(name, default=None): # pylint: disable=unused-argument
...@@ -21,13 +32,20 @@ def fake_support_backend_values(name, default=None): # pylint: disable=unused-a ...@@ -21,13 +32,20 @@ def fake_support_backend_values(name, default=None): # pylint: disable=unused-a
""" """
config_dict = { config_dict = {
"CONTACT_FORM_SUBMISSION_BACKEND": "email", "CONTACT_FORM_SUBMISSION_BACKEND": "email",
"email_from_address": "support_from@example.com", "email_from_address": TEST_SUPPORT_EMAIL,
} }
return config_dict[name] return config_dict[name]
@ddt
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_FEEDBACK_SUBMISSION": True}) @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_FEEDBACK_SUBMISSION": True})
@override_settings(ZENDESK_URL="dummy", ZENDESK_USER="dummy", ZENDESK_API_KEY="dummy") @override_settings(
DEFAULT_FROM_EMAIL=TEST_SUPPORT_EMAIL,
ZENDESK_URL="dummy",
ZENDESK_USER="dummy",
ZENDESK_API_KEY="dummy",
ZENDESK_CUSTOM_FIELDS={}
)
@mock.patch("util.views.dog_stats_api") @mock.patch("util.views.dog_stats_api")
@mock.patch("util.views._ZendeskApi", autospec=True) @mock.patch("util.views._ZendeskApi", autospec=True)
class SubmitFeedbackTest(TestCase): class SubmitFeedbackTest(TestCase):
...@@ -44,14 +62,12 @@ class SubmitFeedbackTest(TestCase): ...@@ -44,14 +62,12 @@ class SubmitFeedbackTest(TestCase):
username="test", username="test",
profile__name="Test User" profile__name="Test User"
) )
# This contains issue_type and course_id to ensure that tags are submitted correctly
self._anon_fields = { self._anon_fields = {
"email": "test@edx.org", "email": "test@edx.org",
"name": "Test User", "name": "Test User",
"subject": "a subject", "subject": "a subject",
"details": "some details", "details": "some details",
"issue_type": "test_issue", "issue_type": "test_issue"
"course_id": "test_course"
} }
# This does not contain issue_type nor course_id to ensure that they are optional # This does not contain issue_type nor course_id to ensure that they are optional
self._auth_fields = {"subject": "a subject", "details": "some details"} self._auth_fields = {"subject": "a subject", "details": "some details"}
...@@ -66,10 +82,10 @@ class SubmitFeedbackTest(TestCase): ...@@ -66,10 +82,10 @@ class SubmitFeedbackTest(TestCase):
req = self._request_factory.post( req = self._request_factory.post(
"/submit_feedback", "/submit_feedback",
data=fields, data=fields,
HTTP_REFERER="test_referer", HTTP_REFERER=TEST_REQUEST_HEADERS["HTTP_REFERER"],
HTTP_USER_AGENT="test_user_agent", HTTP_USER_AGENT=TEST_REQUEST_HEADERS["HTTP_USER_AGENT"],
REMOTE_ADDR="1.2.3.4", REMOTE_ADDR=TEST_REQUEST_HEADERS["REMOTE_ADDR"],
SERVER_NAME="test_server", SERVER_NAME=TEST_REQUEST_HEADERS["SERVER_NAME"],
) )
req.user = user req.user = user
return views.submit_feedback(req) return views.submit_feedback(req)
...@@ -130,13 +146,58 @@ class SubmitFeedbackTest(TestCase): ...@@ -130,13 +146,58 @@ class SubmitFeedbackTest(TestCase):
resp = self._build_and_run_request(user, fields) resp = self._build_and_run_request(user, fields)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
def _assert_datadog_called(self, datadog_mock, with_tags): def _build_zendesk_ticket(self, recipient, name, email, subject, details, tags, custom_fields=None):
expected_datadog_calls = [ """
mock.call.increment( Build a Zendesk ticket that can be used in assertions to verify that the correct
views.DATADOG_FEEDBACK_METRIC, data was submitted to create a Zendesk ticket.
tags=(["course_id:test_course", "issue_type:test_issue"] if with_tags else []) """
) ticket = {
"ticket": {
"recipient": recipient,
"requester": {"name": name, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": tags
}
}
if custom_fields is not None:
ticket["ticket"]["custom_fields"] = custom_fields
return ticket
def _build_zendesk_ticket_update(self, request_headers, username=None):
"""
Build a Zendesk ticket update that can be used in assertions to verify that the correct
data was submitted to update a Zendesk ticket.
"""
body = []
if username:
body.append("username: {}".format(username))
# FIXME the tests rely on the body string being built in this specific order, which doesn't seem
# reliable given that the view builds the string by iterating over a dictionary.
header_text_mapping = [
("Client IP", "REMOTE_ADDR"),
("Host", "SERVER_NAME"),
("Page", "HTTP_REFERER"),
("Browser", "HTTP_USER_AGENT")
] ]
for text, header in header_text_mapping:
body.append("{}: {}".format(text, request_headers[header]))
body = "Additional information:\n\n" + "\n".join(body)
return {"ticket": {"comment": {"public": False, "body": body}}}
def _assert_zendesk_called(self, zendesk_mock, ticket_id, ticket, ticket_update):
"""Assert that Zendesk was called with the correct ticket and ticket_update."""
expected_zendesk_calls = [mock.call.create_ticket(ticket), mock.call.update_ticket(ticket_id, ticket_update)]
self.assertEqual(zendesk_mock.mock_calls, expected_zendesk_calls)
def _assert_datadog_called(self, datadog_mock, tags):
"""Assert that datadog was called with the correct tags."""
expected_datadog_calls = [mock.call.increment(views.DATADOG_FEEDBACK_METRIC, tags=tags)]
self.assertEqual(datadog_mock.mock_calls, expected_datadog_calls) self.assertEqual(datadog_mock.mock_calls, expected_datadog_calls)
def test_bad_request_anon_user_no_name(self, zendesk_mock_class, datadog_mock): def test_bad_request_anon_user_no_name(self, zendesk_mock_class, datadog_mock):
...@@ -174,39 +235,26 @@ class SubmitFeedbackTest(TestCase): ...@@ -174,39 +235,26 @@ class SubmitFeedbackTest(TestCase):
the given information should have been submitted via the Zendesk API. the given information should have been submitted via the Zendesk API.
""" """
zendesk_mock_instance = zendesk_mock_class.return_value zendesk_mock_instance = zendesk_mock_class.return_value
zendesk_mock_instance.create_ticket.return_value = 42 user = self._anon_user
self._test_success(self._anon_user, self._anon_fields) fields = self._anon_fields
expected_zendesk_calls = [
mock.call.create_ticket( ticket_id = 42
{ zendesk_mock_instance.create_ticket.return_value = ticket_id
"ticket": {
"recipient": "registration@example.com", ticket = self._build_zendesk_ticket(
"requester": {"name": "Test User", "email": "test@edx.org"}, recipient=TEST_SUPPORT_EMAIL,
"subject": "a subject", name=fields["name"],
"comment": {"body": "some details"}, email=fields["email"],
"tags": ["test_course", "test_issue", "LMS"] subject=fields["subject"],
} details=fields["details"],
} tags=[fields["issue_type"], "LMS"]
), )
mock.call.update_ticket(
42, ticket_update = self._build_zendesk_ticket_update(TEST_REQUEST_HEADERS)
{
"ticket": { self._test_success(user, fields)
"comment": { self._assert_zendesk_called(zendesk_mock_instance, ticket_id, ticket, ticket_update)
"public": False, self._assert_datadog_called(datadog_mock, ["issue_type:{}".format(fields["issue_type"])])
"body":
"Additional information:\n\n"
"Client IP: 1.2.3.4\n"
"Host: test_server\n"
"Page: test_referer\n"
"Browser: test_user_agent"
}
}
}
)
]
self.assertEqual(zendesk_mock_instance.mock_calls, expected_zendesk_calls)
self._assert_datadog_called(datadog_mock, with_tags=True)
@mock.patch("openedx.core.djangoapps.site_configuration.helpers.get_value", fake_get_value) @mock.patch("openedx.core.djangoapps.site_configuration.helpers.get_value", fake_get_value)
def test_valid_request_anon_user_configuration_override(self, zendesk_mock_class, datadog_mock): def test_valid_request_anon_user_configuration_override(self, zendesk_mock_class, datadog_mock):
...@@ -218,39 +266,75 @@ class SubmitFeedbackTest(TestCase): ...@@ -218,39 +266,75 @@ class SubmitFeedbackTest(TestCase):
tag that will come from site configuration override. tag that will come from site configuration override.
""" """
zendesk_mock_instance = zendesk_mock_class.return_value zendesk_mock_instance = zendesk_mock_class.return_value
zendesk_mock_instance.create_ticket.return_value = 42 user = self._anon_user
self._test_success(self._anon_user, self._anon_fields) fields = self._anon_fields
expected_zendesk_calls = [
mock.call.create_ticket( ticket_id = 42
{ zendesk_mock_instance.create_ticket.return_value = ticket_id
"ticket": {
"recipient": "no-reply@fakeuniversity.com", ticket = self._build_zendesk_ticket(
"requester": {"name": "Test User", "email": "test@edx.org"}, recipient=fake_get_value("email_from_address"),
"subject": "a subject", name=fields["name"],
"comment": {"body": "some details"}, email=fields["email"],
"tags": ["test_course", "test_issue", "LMS", "whitelabel_fakeorg"] subject=fields["subject"],
} details=fields["details"],
} tags=[fields["issue_type"], "LMS", "whitelabel_{}".format(fake_get_value("course_org_filter"))]
), )
mock.call.update_ticket(
42, ticket_update = self._build_zendesk_ticket_update(TEST_REQUEST_HEADERS)
{
"ticket": { self._test_success(user, fields)
"comment": { self._assert_zendesk_called(zendesk_mock_instance, ticket_id, ticket, ticket_update)
"public": False, self._assert_datadog_called(datadog_mock, ["issue_type:{}".format(fields["issue_type"])])
"body":
"Additional information:\n\n" @data("course-v1:testOrg+testCourseNumber+testCourseRun", "", None)
"Client IP: 1.2.3.4\n" @override_settings(ZENDESK_CUSTOM_FIELDS=TEST_ZENDESK_CUSTOM_FIELD_CONFIG)
"Host: test_server\n" def test_valid_request_anon_user_with_custom_fields(self, course_id, zendesk_mock_class, datadog_mock):
"Page: test_referer\n" """
"Browser: test_user_agent" Test a valid request from an anonymous user when configured to use Zendesk Custom Fields.
}
} The response should have a 200 (success) status code, and a ticket with
} the given information should have been submitted via the Zendesk API. When course_id is
) present, it should be sent to Zendesk via a custom field. When course_id is blank or missing,
] the request should still be processed successfully.
self.assertEqual(zendesk_mock_instance.mock_calls, expected_zendesk_calls) """
self._assert_datadog_called(datadog_mock, with_tags=True) zendesk_mock_instance = zendesk_mock_class.return_value
user = self._anon_user
fields = self._anon_fields.copy()
if course_id is not None:
fields["course_id"] = course_id
ticket_id = 42
zendesk_mock_instance.create_ticket.return_value = ticket_id
zendesk_tags = [fields["issue_type"], "LMS"]
datadog_tags = ["issue_type:{}".format(fields["issue_type"])]
zendesk_custom_fields = None
if course_id:
# FIXME the tests rely on the tags being in this specific order, which doesn't seem
# reliable given that the view builds the list by iterating over a dictionary.
zendesk_tags.insert(0, course_id)
datadog_tags.insert(0, "course_id:{}".format(course_id))
zendesk_custom_fields = [
{"id": TEST_ZENDESK_CUSTOM_FIELD_CONFIG["course_id"], "value": course_id}
]
ticket = self._build_zendesk_ticket(
recipient=TEST_SUPPORT_EMAIL,
name=fields["name"],
email=fields["email"],
subject=fields["subject"],
details=fields["details"],
tags=zendesk_tags,
custom_fields=zendesk_custom_fields
)
ticket_update = self._build_zendesk_ticket_update(TEST_REQUEST_HEADERS)
self._test_success(user, fields)
self._assert_zendesk_called(zendesk_mock_instance, ticket_id, ticket, ticket_update)
self._assert_datadog_called(datadog_mock, datadog_tags)
def test_bad_request_auth_user_no_subject(self, zendesk_mock_class, datadog_mock): def test_bad_request_auth_user_no_subject(self, zendesk_mock_class, datadog_mock):
"""Test a request from an authenticated user not specifying `subject`.""" """Test a request from an authenticated user not specifying `subject`."""
...@@ -270,40 +354,92 @@ class SubmitFeedbackTest(TestCase): ...@@ -270,40 +354,92 @@ class SubmitFeedbackTest(TestCase):
the given information should have been submitted via the Zendesk API. the given information should have been submitted via the Zendesk API.
""" """
zendesk_mock_instance = zendesk_mock_class.return_value zendesk_mock_instance = zendesk_mock_class.return_value
zendesk_mock_instance.create_ticket.return_value = 42 user = self._auth_user
self._test_success(self._auth_user, self._auth_fields) fields = self._auth_fields
expected_zendesk_calls = [
mock.call.create_ticket( ticket_id = 42
{ zendesk_mock_instance.create_ticket.return_value = ticket_id
"ticket": {
"recipient": "registration@example.com", ticket = self._build_zendesk_ticket(
"requester": {"name": "Test User", "email": "test@edx.org"}, recipient=TEST_SUPPORT_EMAIL,
"subject": "a subject", name=user.profile.name,
"comment": {"body": "some details"}, email=user.email,
"tags": ["LMS"] subject=fields["subject"],
} details=fields["details"],
} tags=["LMS"]
), )
mock.call.update_ticket(
42, ticket_update = self._build_zendesk_ticket_update(TEST_REQUEST_HEADERS, user.username)
{
"ticket": { self._test_success(user, fields)
"comment": { self._assert_zendesk_called(zendesk_mock_instance, ticket_id, ticket, ticket_update)
"public": False, self._assert_datadog_called(datadog_mock, [])
"body":
"Additional information:\n\n" @data(
"username: test\n" ("course-v1:testOrg+testCourseNumber+testCourseRun", True),
"Client IP: 1.2.3.4\n" ("course-v1:testOrg+testCourseNumber+testCourseRun", False),
"Host: test_server\n" ("", None),
"Page: test_referer\n" (None, None)
"Browser: test_user_agent" )
} @unpack
} @override_settings(ZENDESK_CUSTOM_FIELDS=TEST_ZENDESK_CUSTOM_FIELD_CONFIG)
} def test_valid_request_auth_user_with_custom_fields(self, course_id, enrolled, zendesk_mock_class, datadog_mock):
) """
] Test a valid request from an authenticated user when configured to use Zendesk Custom Fields.
self.assertEqual(zendesk_mock_instance.mock_calls, expected_zendesk_calls)
self._assert_datadog_called(datadog_mock, with_tags=False) The response should have a 200 (success) status code, and a ticket with
the given information should have been submitted via the Zendesk API. When course_id is
present, it should be sent to Zendesk via a custom field, along with the enrollment mode
if the user has an active enrollment for that course. When course_id is blank or missing,
the request should still be processed successfully.
"""
zendesk_mock_instance = zendesk_mock_class.return_value
user = self._auth_user
fields = self._auth_fields.copy()
if course_id is not None:
fields["course_id"] = course_id
ticket_id = 42
zendesk_mock_instance.create_ticket.return_value = ticket_id
zendesk_tags = ["LMS"]
datadog_tags = []
zendesk_custom_fields = None
if course_id:
# FIXME the tests rely on the tags being in this specific order, which doesn't seem
# reliable given that the view builds the list by iterating over a dictionary.
zendesk_tags.insert(0, course_id)
datadog_tags.insert(0, "course_id:{}".format(course_id))
zendesk_custom_fields = [
{"id": TEST_ZENDESK_CUSTOM_FIELD_CONFIG["course_id"], "value": course_id}
]
if enrolled is not None:
enrollment = CourseEnrollmentFactory.create(
user=user,
course_id=course_id,
is_active=enrolled
)
if enrollment.is_active:
zendesk_custom_fields.append(
{"id": TEST_ZENDESK_CUSTOM_FIELD_CONFIG["enrollment_mode"], "value": enrollment.mode}
)
ticket = self._build_zendesk_ticket(
recipient=TEST_SUPPORT_EMAIL,
name=user.profile.name,
email=user.email,
subject=fields["subject"],
details=fields["details"],
tags=zendesk_tags,
custom_fields=zendesk_custom_fields
)
ticket_update = self._build_zendesk_ticket_update(TEST_REQUEST_HEADERS, user.username)
self._test_success(user, fields)
self._assert_zendesk_called(zendesk_mock_instance, ticket_id, ticket, ticket_update)
self._assert_datadog_called(datadog_mock, datadog_tags)
def test_get_request(self, zendesk_mock_class, datadog_mock): def test_get_request(self, zendesk_mock_class, datadog_mock):
"""Test that a GET results in a 405 even with all required fields""" """Test that a GET results in a 405 even with all required fields"""
...@@ -329,7 +465,7 @@ class SubmitFeedbackTest(TestCase): ...@@ -329,7 +465,7 @@ class SubmitFeedbackTest(TestCase):
resp = self._build_and_run_request(self._anon_user, self._anon_fields) resp = self._build_and_run_request(self._anon_user, self._anon_fields)
self.assertEqual(resp.status_code, 500) self.assertEqual(resp.status_code, 500)
self.assertFalse(resp.content) self.assertFalse(resp.content)
self._assert_datadog_called(datadog_mock, with_tags=True) self._assert_datadog_called(datadog_mock, ["issue_type:{}".format(self._anon_fields["issue_type"])])
def test_zendesk_error_on_update(self, zendesk_mock_class, datadog_mock): def test_zendesk_error_on_update(self, zendesk_mock_class, datadog_mock):
""" """
...@@ -344,7 +480,7 @@ class SubmitFeedbackTest(TestCase): ...@@ -344,7 +480,7 @@ class SubmitFeedbackTest(TestCase):
zendesk_mock_instance.update_ticket.side_effect = err zendesk_mock_instance.update_ticket.side_effect = err
resp = self._build_and_run_request(self._anon_user, self._anon_fields) resp = self._build_and_run_request(self._anon_user, self._anon_fields)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self._assert_datadog_called(datadog_mock, with_tags=True) self._assert_datadog_called(datadog_mock, ["issue_type:{}".format(self._anon_fields["issue_type"])])
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_FEEDBACK_SUBMISSION": False}) @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_FEEDBACK_SUBMISSION": False})
def test_not_enabled(self, zendesk_mock_class, datadog_mock): def test_not_enabled(self, zendesk_mock_class, datadog_mock):
......
...@@ -25,6 +25,7 @@ from edxmako.shortcuts import render_to_response, render_to_string ...@@ -25,6 +25,7 @@ from edxmako.shortcuts import render_to_response, render_to_string
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
import track.views import track.views
from student.roles import GlobalStaff from student.roles import GlobalStaff
from student.models import CourseEnrollment
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -222,6 +223,40 @@ class _ZendeskApi(object): ...@@ -222,6 +223,40 @@ class _ZendeskApi(object):
return None return None
def _get_zendesk_custom_field_context(request):
"""
Construct a dictionary of data that can be stored in Zendesk custom fields.
"""
context = {}
course_id = request.POST.get("course_id")
if not course_id:
return context
context["course_id"] = course_id
if not request.user.is_authenticated():
return context
enrollment = CourseEnrollment.get_enrollment(request.user, CourseKey.from_string(course_id))
if enrollment and enrollment.is_active:
context["enrollment_mode"] = enrollment.mode
return context
def _format_zendesk_custom_fields(context):
"""
Format the data in `context` for compatibility with the Zendesk API.
Ignore any keys that have not been configured in `ZENDESK_CUSTOM_FIELDS`.
"""
custom_fields = []
for key, val, in settings.ZENDESK_CUSTOM_FIELDS.items():
if key in context:
custom_fields.append({"id": val, "value": context[key]})
return custom_fields
def _record_feedback_in_zendesk( def _record_feedback_in_zendesk(
realname, realname,
email, email,
...@@ -231,7 +266,8 @@ def _record_feedback_in_zendesk( ...@@ -231,7 +266,8 @@ def _record_feedback_in_zendesk(
additional_info, additional_info,
group_name=None, group_name=None,
require_update=False, require_update=False,
support_email=None support_email=None,
custom_fields=None
): ):
""" """
Create a new user-requested Zendesk ticket. Create a new user-requested Zendesk ticket.
...@@ -246,6 +282,8 @@ def _record_feedback_in_zendesk( ...@@ -246,6 +282,8 @@ def _record_feedback_in_zendesk(
If `require_update` is provided, returns False when the update does not If `require_update` is provided, returns False when the update does not
succeed. This allows using the private comment to add necessary information succeed. This allows using the private comment to add necessary information
which the user will not see in followup emails from support. which the user will not see in followup emails from support.
If `custom_fields` is provided, submits data to those fields in Zendesk.
""" """
zendesk_api = _ZendeskApi() zendesk_api = _ZendeskApi()
...@@ -271,6 +309,10 @@ def _record_feedback_in_zendesk( ...@@ -271,6 +309,10 @@ def _record_feedback_in_zendesk(
"tags": zendesk_tags "tags": zendesk_tags
} }
} }
if custom_fields:
new_ticket["ticket"]["custom_fields"] = custom_fields
group = None group = None
if group_name is not None: if group_name is not None:
group = zendesk_api.get_group(group_name) group = zendesk_api.get_group(group_name)
...@@ -322,7 +364,7 @@ def get_feedback_form_context(request): ...@@ -322,7 +364,7 @@ def get_feedback_form_context(request):
context["subject"] = request.POST["subject"] context["subject"] = request.POST["subject"]
context["details"] = request.POST["details"] context["details"] = request.POST["details"]
context["tags"] = dict( context["tags"] = dict(
[(tag, request.POST[tag]) for tag in ["issue_type", "course_id"] if tag in request.POST] [(tag, request.POST[tag]) for tag in ["issue_type", "course_id"] if request.POST.get(tag)]
) )
context["additional_info"] = {} context["additional_info"] = {}
...@@ -412,6 +454,11 @@ def submit_feedback(request): ...@@ -412,6 +454,11 @@ def submit_feedback(request):
if not settings.ZENDESK_URL or not settings.ZENDESK_USER or not settings.ZENDESK_API_KEY: if not settings.ZENDESK_URL or not settings.ZENDESK_USER or not settings.ZENDESK_API_KEY:
raise Exception("Zendesk enabled but not configured") raise Exception("Zendesk enabled but not configured")
custom_fields = None
if settings.ZENDESK_CUSTOM_FIELDS:
custom_field_context = _get_zendesk_custom_field_context(request)
custom_fields = _format_zendesk_custom_fields(custom_field_context)
success = _record_feedback_in_zendesk( success = _record_feedback_in_zendesk(
context["realname"], context["realname"],
context["email"], context["email"],
...@@ -419,7 +466,8 @@ def submit_feedback(request): ...@@ -419,7 +466,8 @@ def submit_feedback(request):
context["details"], context["details"],
context["tags"], context["tags"],
context["additional_info"], context["additional_info"],
support_email=context["support_email"] support_email=context["support_email"],
custom_fields=custom_fields
) )
_record_feedback_in_datadog(context["tags"]) _record_feedback_in_datadog(context["tags"])
......
...@@ -333,6 +333,8 @@ COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '') ...@@ -333,6 +333,8 @@ COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '')
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '') COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '')
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull') CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
ZENDESK_URL = ENV_TOKENS.get('ZENDESK_URL', ZENDESK_URL) ZENDESK_URL = ENV_TOKENS.get('ZENDESK_URL', ZENDESK_URL)
ZENDESK_CUSTOM_FIELDS = ENV_TOKENS.get('ZENDESK_CUSTOM_FIELDS', ZENDESK_CUSTOM_FIELDS)
FEEDBACK_SUBMISSION_EMAIL = ENV_TOKENS.get("FEEDBACK_SUBMISSION_EMAIL") FEEDBACK_SUBMISSION_EMAIL = ENV_TOKENS.get("FEEDBACK_SUBMISSION_EMAIL")
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS) MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
......
...@@ -1003,6 +1003,7 @@ FEEDBACK_SUBMISSION_EMAIL = None ...@@ -1003,6 +1003,7 @@ FEEDBACK_SUBMISSION_EMAIL = None
ZENDESK_URL = None ZENDESK_URL = None
ZENDESK_USER = None ZENDESK_USER = None
ZENDESK_API_KEY = None ZENDESK_API_KEY = None
ZENDESK_CUSTOM_FIELDS = {}
##### EMBARGO ##### ##### EMBARGO #####
EMBARGO_SITE_REDIRECT_URL = None EMBARGO_SITE_REDIRECT_URL = None
......
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
@import 'shared/modal'; @import 'shared/modal';
@import 'shared/activation_messages'; @import 'shared/activation_messages';
@import 'shared/unsubscribe'; @import 'shared/unsubscribe';
@import 'shared/help-tab';
// shared - platform // shared - platform
@import 'multicourse/home'; @import 'multicourse/home';
......
...@@ -78,3 +78,9 @@ ...@@ -78,3 +78,9 @@
padding: 0 $baseline $baseline; padding: 0 $baseline $baseline;
} }
} }
.feedback-form-select {
background: $white;
margin-bottom: $baseline;
width: 100%;
}
.feedback-form-select {
background: $white;
margin-bottom: $baseline;
width: 100%;
}
...@@ -99,15 +99,21 @@ from xmodule.tabs import CourseTabList ...@@ -99,15 +99,21 @@ from xmodule.tabs import CourseTabList
<label data-field="email" for="feedback_form_email">${_('E-mail')}*</label> <label data-field="email" for="feedback_form_email">${_('E-mail')}*</label>
<input name="email" type="text" id="feedback_form_email" required> <input name="email" type="text" id="feedback_form_email" required>
% endif % endif
<div class="js-course-id-anchor">
% if course:
<input name="course_id" type="hidden" value="${unicode(course.id)}">
% endif
</div>
<label data-field="subject" for="feedback_form_subject">${_('Briefly describe your issue')}*</label> <label data-field="subject" for="feedback_form_subject">${_('Briefly describe your issue')}*</label>
<input name="subject" type="text" id="feedback_form_subject" required> <input name="subject" type="text" id="feedback_form_subject" required>
<label data-field="details" for="feedback_form_details">${_('Tell us the details')}*</label> <label data-field="details" for="feedback_form_details">${_('Tell us the details')}*</label>
<span class="tip" id="feedback_form_details_tip">${_('Describe what you were doing when you encountered the issue. Include any details that will help us to troubleshoot, including error messages that you saw.')}</span> <span class="tip" id="feedback_form_details_tip">${_('Describe what you were doing when you encountered the issue. Include any details that will help us to troubleshoot, including error messages that you saw.')}</span>
<textarea name="details" id="feedback_form_details" required aria-describedby="feedback_form_details_tip"></textarea> <textarea name="details" id="feedback_form_details" required aria-describedby="feedback_form_details_tip"></textarea>
<input name="issue_type" type="hidden"> <input name="issue_type" type="hidden">
% if course:
<input name="course_id" type="hidden" value="${unicode(course.id)}">
% endif
<div class="submit"> <div class="submit">
<input name="submit" type="submit" value="${_('Submit')}" id="feedback_submit"> <input name="submit" type="submit" value="${_('Submit')}" id="feedback_submit">
</div> </div>
...@@ -138,7 +144,12 @@ from xmodule.tabs import CourseTabList ...@@ -138,7 +144,12 @@ from xmodule.tabs import CourseTabList
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
var $helpModal = $("#help-modal"), var currentCourseId,
courseOptions = [],
userAuthenticated = false,
courseOptionsLoadInProgress = false,
finishedLoadingCourseOptions = false,
$helpModal = $("#help-modal"),
$closeButton = $("#help-modal .close-modal"), $closeButton = $("#help-modal .close-modal"),
$leanOverlay = $("#lean_overlay"), $leanOverlay = $("#lean_overlay"),
$feedbackForm = $("#feedback_form"), $feedbackForm = $("#feedback_form"),
...@@ -149,11 +160,101 @@ $(document).ready(function() { ...@@ -149,11 +160,101 @@ $(document).ready(function() {
$('area,input,select,textarea,button').removeAttr('tabindex'); $('area,input,select,textarea,button').removeAttr('tabindex');
$(".help-tab a").focus(); $(".help-tab a").focus();
$leanOverlay.removeAttr('tabindex'); $leanOverlay.removeAttr('tabindex');
},
showFeedback = function(event, issue_type, title, subject_label, details_label) {
event.preventDefault();
DialogTabControls.initializeTabKeyValues("#feedback_form_wrapper", $closeButton);
$("#feedback_form input[name='issue_type']").val(issue_type);
$("#feedback_form_wrapper header").html("<h2>" + title + "</h2><hr>");
$("#feedback_form_wrapper label[data-field='subject']").html(subject_label);
$("#feedback_form_wrapper label[data-field='details']").html(details_label);
if (userAuthenticated && finishedLoadingCourseOptions && courseOptions.length > 1) {
$('.js-course-id-anchor').html([
'<label for="feedback_form_course">' + '${_("Course") | n, js_escaped_string}' + '</label>',
'<select name="course_id" id="feedback_form_course" class="feedback-form-select">',
courseOptions.join(''),
'</select>'
].join(''));
}
$("#help_wrapper").css("display", "none");
$("#feedback_form_wrapper").css("display", "block");
$closeButton.focus();
},
loadCourseOptions = function() {
courseOptionsLoadInProgress = true;
$.ajax({
url: '/api/enrollment/v1/enrollment',
success: function(data) {
var i,
courseDetails,
courseName,
courseId,
option,
defaultOptionText = '${_("- Select -") | n, js_escaped_string}',
markedSelectedOption = false;
// Make sure courseOptions is empty before we begin pushing options into it.
courseOptions = [];
for (i = 0; i < data.length; i++) {
courseDetails = data[i].course_details;
if (!courseDetails) {
continue;
}
courseName = courseDetails.course_name;
courseId = courseDetails.course_id;
if (!(courseName && courseId)) {
continue;
}
// Build an option for this course and select it if it's the course we're currently viewing.
if (!markedSelectedOption && courseId === currentCourseId) {
option = buildCourseOption(courseName, courseId, true);
markedSelectedOption = true;
} else {
option = buildCourseOption(courseName, courseId, false);
}
courseOptions.push(option);
}
// Build the default option and select it if we haven't already selected another option.
option = buildCourseOption(defaultOptionText, '', !markedSelectedOption);
// Add the default option to the head of the courseOptions Array.
courseOptions.unshift(option);
finishedLoadingCourseOptions = true;
},
complete: function() {
courseOptionsLoadInProgress = false;
}
});
},
buildCourseOption = function(courseName, courseId, selected) {
var option = '<option value="' + _.escape(courseId) + '"';
if (selected) {
option += ' selected';
}
option += '>' + _.escape(courseName) + '</option>';
return option;
}; };
% if user.is_authenticated():
userAuthenticated = true;
% endif
% if course:
currentCourseId = "${unicode(course.id) | n, js_escaped_string}";
% endif
DialogTabControls.setKeydownListener($helpModal, $closeButton); DialogTabControls.setKeydownListener($helpModal, $closeButton);
$(".help-tab").click(function() { $(".help-tab").click(function() {
if (userAuthenticated && !finishedLoadingCourseOptions && !courseOptionsLoadInProgress) {
loadCourseOptions();
}
$helpModal.css("position", "absolute"); $helpModal.css("position", "absolute");
DialogTabControls.initializeTabKeyValues("#help_wrapper", $closeButton); DialogTabControls.initializeTabKeyValues("#help_wrapper", $closeButton);
$(".field-error").removeClass("field-error"); $(".field-error").removeClass("field-error");
...@@ -171,18 +272,6 @@ $(document).ready(function() { ...@@ -171,18 +272,6 @@ $(document).ready(function() {
$closeButton.focus(); $closeButton.focus();
}); });
showFeedback = function(event, issue_type, title, subject_label, details_label) {
$("#help_wrapper").css("display", "none");
DialogTabControls.initializeTabKeyValues("#feedback_form_wrapper", $closeButton);
$("#feedback_form input[name='issue_type']").val(issue_type);
$("#feedback_form_wrapper").css("display", "block");
$("#feedback_form_wrapper header").html("<h2>" + title + "</h2><hr>");
$("#feedback_form_wrapper label[data-field='subject']").html(subject_label);
$("#feedback_form_wrapper label[data-field='details']").html(details_label);
$closeButton.focus();
event.preventDefault();
};
$("#feedback_link_problem").click(function(event) { $("#feedback_link_problem").click(function(event) {
$("#feedback_form_details_tip").css({"display": "block", "padding-bottom": "5px"}); $("#feedback_form_details_tip").css({"display": "block", "padding-bottom": "5px"});
showFeedback( showFeedback(
......
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