diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 3712411aa3..8c34fd1fb3 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -94,6 +94,7 @@ from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.util.user_messages import PageLevelMessages +from openedx.core.djangoapps.zendesk_proxy.utils import create_zendesk_ticket from openedx.core.djangolib.markup import HTML, Text from openedx.features.course_duration_limits.access import generate_course_expired_fragment from openedx.features.course_experience import ( @@ -112,7 +113,7 @@ from track import segment from util.cache import cache, cache_if_anonymous from util.db import outer_atomic from util.milestones_helpers import get_prerequisite_courses_display -from util.views import _record_feedback_in_zendesk, ensure_valid_course_key, ensure_valid_usage_key +from util.views import ensure_valid_course_key, ensure_valid_usage_key from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem @@ -1625,7 +1626,7 @@ def financial_assistance_request(request): # Thrown if fields are missing return HttpResponseBadRequest(u'The field {} is required.'.format(text_type(err))) - zendesk_submitted = _record_feedback_in_zendesk( + zendesk_submitted = create_zendesk_ticket( legal_name, email, u'Financial assistance request for learner {username} in course {course_name}'.format( @@ -1633,12 +1634,12 @@ def financial_assistance_request(request): course_name=course.display_name ), u'Financial Assistance Request', - {'course_id': course_id}, + tags={'course_id': course_id}, # Send the application as additional info on the ticket so # that it is not shown when support replies. This uses # OrderedDict so that information is presented in the right # order. - OrderedDict(( + additional_info=OrderedDict(( ('Username', username), ('Full Name', legal_name), ('Course ID', course_id), @@ -1650,8 +1651,7 @@ def financial_assistance_request(request): (FA_EFFORT_LABEL, '\n' + effort + '\n\n'), ('Client IP', ip_address), )), - group_name='Financial Assistance', - require_update=True + group='Financial Assistance', ) if not zendesk_submitted: diff --git a/openedx/core/djangoapps/zendesk_proxy/utils.py b/openedx/core/djangoapps/zendesk_proxy/utils.py index d905b58b82..4f629a57c5 100644 --- a/openedx/core/djangoapps/zendesk_proxy/utils.py +++ b/openedx/core/djangoapps/zendesk_proxy/utils.py @@ -11,21 +11,25 @@ from django.conf import settings import requests from rest_framework import status + log = logging.getLogger(__name__) -def create_zendesk_ticket(requester_name, requester_email, subject, body, custom_fields=None, uploads=None, tags=None): +def _std_error_message(details, payload): + """Internal helper to standardize error message. This allows for simpler splunk alerts.""" + return u'zendesk_proxy action required\n{}\nNo ticket created for payload {}'.format(details, payload) + + +def _get_request_headers(): + return { + 'content-type': 'application/json', + 'Authorization': u"Bearer {}".format(settings.ZENDESK_OAUTH_ACCESS_TOKEN), + } + +def create_zendesk_ticket(requester_name, requester_email, subject, body, group=None, custom_fields=None, uploads=None, tags=None, additional_info=None): """ Create a Zendesk ticket via API. - - Note that we do this differently in other locations (lms/djangoapps/commerce/signals.py and - common/djangoapps/util/views.py). Both of those callers use basic auth, and should be switched over to this oauth - implementation once the immediate pressures of zendesk_proxy are resolved. """ - def _std_error_message(details, payload): - """Internal helper to standardize error message. This allows for simpler splunk alerts.""" - return u'zendesk_proxy action required\n{}\nNo ticket created for payload {}'.format(details, payload) - if tags: # Remove duplicates from tags list tags = list(set(tags)) @@ -46,22 +50,22 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, custom } } + if not (settings.ZENDESK_URL and settings.ZENDESK_OAUTH_ACCESS_TOKEN): + log.error(_std_error_message("zendesk not configured", data)) + return status.HTTP_503_SERVICE_UNAVAILABLE + + if group: + group_id = get_zendesk_group_by_name(group) + data['ticket']['group_id'] = group_id + # Encode the data to create a JSON payload payload = json.dumps(data) - if not (settings.ZENDESK_URL and settings.ZENDESK_OAUTH_ACCESS_TOKEN): - log.error(_std_error_message("zendesk not configured", payload)) - return status.HTTP_503_SERVICE_UNAVAILABLE - # Set the request parameters url = urljoin(settings.ZENDESK_URL, '/api/v2/tickets.json') - headers = { - 'content-type': 'application/json', - 'Authorization': u"Bearer {}".format(settings.ZENDESK_OAUTH_ACCESS_TOKEN), - } try: - response = requests.post(url, data=payload, headers=headers) + response = requests.post(url, data=payload, headers=_get_request_headers()) # Check for HTTP codes other than 201 (Created) if response.status_code == status.HTTP_201_CREATED: @@ -73,7 +77,72 @@ def create_zendesk_ticket(requester_name, requester_email, subject, body, custom payload ) ) + if additional_info: + ticket = json.loads(response.text)['ticket'] + return post_additional_info_as_comment(ticket['id'], additional_info) + return response.status_code except Exception: # pylint: disable=broad-except log.exception(_std_error_message('Internal server error', payload)) return status.HTTP_500_INTERNAL_SERVER_ERROR + + +def get_zendesk_group_by_name(name): + """ + Calls the Zendesk list-groups api + + Returns the group Id matching the name. + """ + url = urljoin(settings.ZENDESK_URL, '/api/v2/groups.json') + + try: + response = requests.post(url, headers=_get_request_headers()) + + groups = json.loads(response.text)['groups'] + for group in groups: + if group['name'] == name: + return group['id'] + except Exception as e: # pylint: disable=broad-except + log.exception(_std_error_message('Internal server error', 'None')) + + return status.HTTP_500_INTERNAL_SERVER_ERROR + log.exception(_std_error_message('Tried to get zendesk group which does not exist', name)) + raise Exception + + +def post_additional_info_as_comment(ticket_id, additional_info): + """ + Post the Additional Provided as a comment, So that it is only visible + to management and not students. + """ + additional_info_string = ( + u"Additional information:\n\n" + + u"\n".join(u"%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None) + ) + + data = { + 'ticket': { + 'comment': { + 'body': additional_info_string, + 'publuc': False + } + } + } + + url = urljoin(settings.ZENDESK_URL, 'api/v2/tickets/{}.json'.format(ticket_id)) + + try: + response = requests.put(url, data=json.dumps(data), headers=_get_request_headers()) + if response.status_code == 200: + log.debug(u'Successfully created comment for ticket {}'.format(ticket_id)) + else: + log.error( + _std_error_message( + u'Unexpected response: {} - {}'.format(response.status_code, response.content), + data + ) + ) + return response.status_code + except Exception: # pylint: disable=broad-except + log.exception(_std_error_message('Internal server error', data)) + return status.HTTP_500_INTERNAL_SERVER_ERROR