* Update Financial Assistance logic

Use the zendesk proxy app instead of the unsupported zendesk library.

* Move to pre-fetching the group IDs.

Rather than making extra requests to zendesk to list all groups and find
a specific group ID. Just make a pre-filled list of group IDs for the
groups we care about.  When a group name is passed in, it is checked
against this list and the ticket is created in the correct group so the
right people can respond to it.
This commit is contained in:
Ayub
2019-08-16 22:05:35 +05:00
committed by Feanil Patel
parent f5f875401a
commit 658cd5c62e
11 changed files with 155 additions and 309 deletions

View File

@@ -7,11 +7,9 @@ from functools import wraps
import calc
import crum
import zendesk
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.cache import caches
from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseServerError
from django.views.decorators.csrf import requires_csrf_token
from django.views.defaults import server_error
from opaque_keys import InvalidKeyError
@@ -20,8 +18,6 @@ from six.moves import map
import track.views
from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from student.models import CourseEnrollment
from student.roles import GlobalStaff
log = logging.getLogger(__name__)
@@ -168,232 +164,6 @@ def calculate(request):
return HttpResponse(json.dumps({'result': str(result)}))
class _ZendeskApi(object):
CACHE_PREFIX = 'ZENDESK_API_CACHE'
CACHE_TIMEOUT = 60 * 60
def __init__(self):
"""
Instantiate the Zendesk API.
All of `ZENDESK_URL`, `ZENDESK_USER`, and `ZENDESK_API_KEY` must be set
in `django.conf.settings`.
"""
self._zendesk_instance = zendesk.Zendesk(
settings.ZENDESK_URL,
settings.ZENDESK_USER,
settings.ZENDESK_API_KEY,
use_api_token=True,
api_version=2,
# As of 2012-05-08, Zendesk is using a CA that is not
# installed on our servers
client_args={"disable_ssl_certificate_validation": True}
)
def create_ticket(self, ticket):
"""
Create the given `ticket` in Zendesk.
The ticket should have the format specified by the zendesk package.
"""
ticket_url = self._zendesk_instance.create_ticket(data=ticket)
return zendesk.get_id_from_url(ticket_url)
def update_ticket(self, ticket_id, update):
"""
Update the Zendesk ticket with id `ticket_id` using the given `update`.
The update should have the format specified by the zendesk package.
"""
self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update)
def get_group(self, name):
"""
Find the Zendesk group named `name`. Groups are cached for
CACHE_TIMEOUT seconds.
If a matching group exists, it is returned as a dictionary
with the format specifed by the zendesk package.
Otherwise, returns None.
"""
cache = caches['default']
cache_key = '{prefix}_group_{name}'.format(prefix=self.CACHE_PREFIX, name=name)
cached = cache.get(cache_key)
if cached:
return cached
groups = self._zendesk_instance.list_groups()['groups']
for group in groups:
if group['name'] == name:
cache.set(cache_key, group, self.CACHE_TIMEOUT)
return group
return None
def _get_zendesk_custom_field_context(request, **kwargs):
"""
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
enterprise_learner_data = kwargs.get('learner_data', None)
if enterprise_learner_data:
enterprise_customer_name = enterprise_learner_data[0]['enterprise_customer']['name']
context["enterprise_customer_name"] = enterprise_customer_name
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(
realname,
email,
subject,
details,
tags,
additional_info,
group_name=None,
require_update=False,
support_email=None,
custom_fields=None
):
"""
Create a new user-requested Zendesk ticket.
Once created, the ticket will be updated with a private comment containing
additional information from the browser and server, such as HTTP headers
and user state. Returns a boolean value indicating whether ticket creation
was successful, regardless of whether the private comment update succeeded.
If `group_name` is provided, attaches the ticket to the matching Zendesk group.
If `require_update` is provided, returns False when the update does not
succeed. This allows using the private comment to add necessary information
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()
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)
)
# Tag all issues with LMS to distinguish channel in Zendesk; requested by student support team
zendesk_tags = list(tags.values()) + ["LMS"]
# Per edX support, we would like to be able to route feedback items by site via tagging
current_site_name = configuration_helpers.get_value("SITE_NAME")
if current_site_name:
current_site_name = current_site_name.replace(".", "_")
zendesk_tags.append("site_name_{site}".format(site=current_site_name))
new_ticket = {
"ticket": {
"requester": {"name": realname, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": zendesk_tags
}
}
if custom_fields:
new_ticket["ticket"]["custom_fields"] = custom_fields
group = None
if group_name is not None:
group = zendesk_api.get_group(group_name)
if group is not None:
new_ticket['ticket']['group_id'] = group['id']
if support_email is not None:
# If we do not include the `recipient` key here, Zendesk will default to using its default reply
# email address when support agents respond to tickets. By setting the `recipient` key here,
# we can ensure that WL site users are responded to via the correct Zendesk support email address.
new_ticket['ticket']['recipient'] = support_email
try:
ticket_id = zendesk_api.create_ticket(new_ticket)
if group_name is not None and group is None:
# Support uses Zendesk groups to track tickets. In case we
# haven't been able to correctly group this ticket, log its ID
# so it can be found later.
log.warning('Unable to find group named %s for Zendesk ticket with ID %s.', group_name, ticket_id)
except zendesk.ZendeskError:
log.exception("Error creating Zendesk ticket")
return False
# Additional information is provided as a private update so the information
# is not visible to the user.
ticket_update = {"ticket": {"comment": {"public": False, "body": additional_info_string}}}
try:
zendesk_api.update_ticket(ticket_id, ticket_update)
except zendesk.ZendeskError:
log.exception("Error updating Zendesk ticket with ID %s.", ticket_id)
# The update is not strictly necessary, so do not indicate
# failure to the user unless it has been requested with
# `require_update`.
if require_update:
return False
return True
def get_feedback_form_context(request):
"""
Extract the submitted form fields to be used as a context for
feedback submission.
"""
context = {}
context["subject"] = request.POST["subject"]
context["details"] = request.POST["details"]
context["tags"] = dict(
[(tag, request.POST[tag]) for tag in ["issue_type", "course_id"] if request.POST.get(tag)]
)
context["additional_info"] = {}
if request.user.is_authenticated:
context["realname"] = request.user.profile.name
context["email"] = request.user.email
context["additional_info"]["username"] = request.user.username
else:
context["realname"] = request.POST["name"]
context["email"] = request.POST["email"]
for header, pretty in [("HTTP_REFERER", "Page"), ("HTTP_USER_AGENT", "Browser"), ("REMOTE_ADDR", "Client IP"),
("SERVER_NAME", "Host")]:
context["additional_info"][pretty] = request.META.get(header)
context["support_email"] = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)
return context
def info(request):
''' Info page (link from main header) '''
# pylint: disable=unused-argument