Commit 87072a9a by Greg Price

Add an endpoint for submission of Zendesk tickets by end users

This functionality requires the Zendesk URL, user, and API key to be specified
in django.conf.settings. Also, add a flag to MITX_FEATURES (enabled by default)
to control the endpoint and the front-end feature (yet to be added).
parent 0f7378a1
import datetime import datetime
import json import json
import logging
import pprint import pprint
import sys import sys
...@@ -7,15 +8,21 @@ from django.conf import settings ...@@ -7,15 +8,21 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.core.mail import send_mail from django.core.mail import send_mail
from django.http import Http404 from django.core.validators import ValidationError, validate_email
from django.http import HttpResponse from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError
from django.shortcuts import redirect from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from urllib import urlencode
import zendesk
import capa.calc import capa.calc
import track.views import track.views
log = logging.getLogger(__name__)
def calculate(request): def calculate(request):
''' Calculator in footer of every page. ''' ''' Calculator in footer of every page. '''
equation = request.GET['equation'] equation = request.GET['equation']
...@@ -29,36 +36,142 @@ def calculate(request): ...@@ -29,36 +36,142 @@ def calculate(request):
return HttpResponse(json.dumps({'result': str(result)})) return HttpResponse(json.dumps({'result': str(result)}))
def send_feedback(request): class _ZendeskApi(object):
''' Feeback mechanism in footer of every page. ''' def __init__(self):
try: """
username = request.user.username Instantiate the Zendesk API.
email = request.user.email
except: All of `ZENDESK_URL`, `ZENDESK_USER`, and `ZENDESK_API_KEY` must be set
username = "anonymous" in `django.conf.settings`.
email = "anonymous" """
self._zendesk_instance = zendesk.Zendesk(
settings.ZENDESK_URL,
settings.ZENDESK_USER,
settings.ZENDESK_API_KEY,
use_api_token=True,
api_version=2
)
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 submit_feedback_via_zendesk(request):
"""
Create a new user-requested Zendesk ticket.
If Zendesk submission is not enabled, any request will raise `Http404`.
If any configuration parameter (`ZENDESK_URL`, `ZENDESK_USER`, or
`ZENDESK_API_KEY`) is missing, any request will raise an `Exception`.
The request must be a POST request specifying `subject` and `details`.
If the user is not authenticated, the request must also specify `name` and
`email`. If the user is authenticated, the `name` and `email` will be
populated from the user's information. If any required parameter is
missing, a 400 error will be returned indicating which field is missing and
providing an error message. If Zendesk returns any error on ticket
creation, a 500 error will be returned with no body. 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. Whether or not the update succeeds, if the user's ticket is
successfully created, an empty successful response (200) will be returned.
"""
if not settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False):
raise Http404()
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
if (
not settings.ZENDESK_URL or
not settings.ZENDESK_USER or
not settings.ZENDESK_API_KEY
):
raise Exception("Zendesk enabled but not configured")
def build_error_response(status_code, field, err_msg):
return HttpResponse(json.dumps({"field": field, "error": err_msg}), status=status_code)
additional_info = {}
required_fields = ["subject", "details"]
if not request.user.is_authenticated():
required_fields += ["name", "email"]
required_field_errs = {
"subject": "Please provide a subject.",
"details": "Please provide details.",
"name": "Please provide your name.",
"email": "Please provide a valid e-mail.",
}
for field in required_fields:
if field not in request.POST or not request.POST[field]:
return build_error_response(400, field, required_field_errs[field])
subject = request.POST["subject"]
details = request.POST["details"]
tags = []
if "tag" in request.POST:
tags = [request.POST["tag"]]
if request.user.is_authenticated():
realname = request.user.profile.name
email = request.user.email
additional_info["username"] = request.user.username
else:
realname = request.POST["name"]
email = request.POST["email"]
try:
validate_email(email)
except ValidationError:
return build_error_response(400, "email", required_field_errs["email"])
for header in ["HTTP_REFERER", "HTTP_USER_AGENT"]:
additional_info[header] = request.META.get(header)
zendesk_api = _ZendeskApi()
additional_info_string = (
"Additional information:\n\n" +
"\n".join("%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None)
)
new_ticket = {
"ticket": {
"requester": {"name": realname, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": tags
}
}
try: try:
browser = request.META['HTTP_USER_AGENT'] ticket_id = zendesk_api.create_ticket(new_ticket)
except: except zendesk.ZendeskError as err:
browser = "Unknown" log.error("%s", str(err))
return HttpResponse(status=500)
feedback = render_to_string("feedback_email.txt",
{"subject": request.POST['subject'], # Additional information is provided as a private update so the information
"url": request.POST['url'], # is not visible to the user.
"time": datetime.datetime.now().isoformat(), ticket_update = {"ticket": {"comment": {"public": False, "body": additional_info_string}}}
"feedback": request.POST['message'], try:
"email": email, zendesk_api.update_ticket(ticket_id, ticket_update)
"browser": browser, except zendesk.ZendeskError as err:
"user": username}) log.error("%s", str(err))
# The update is not strictly necessary, so do not indicate failure to the user
send_mail("MITx Feedback / " + request.POST['subject'], pass
feedback,
settings.DEFAULT_FROM_EMAIL, return HttpResponse()
[settings.DEFAULT_FEEDBACK_EMAIL],
fail_silently=False
)
return HttpResponse(json.dumps({'success': True}))
def info(request): def info(request):
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
-e git://github.com/edx/django-pipeline.git#egg=django-pipeline -e git://github.com/edx/django-pipeline.git#egg=django-pipeline
-e git://github.com/edx/django-wiki.git@e2e84558#egg=django-wiki -e git://github.com/edx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@5ce6f70a#egg=XBlock -e git+https://github.com/edx/XBlock.git@5ce6f70a#egg=XBlock
...@@ -80,6 +80,8 @@ META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {}) ...@@ -80,6 +80,8 @@ META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {})
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '') 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")
FEEDBACK_SUBMISSION_EMAIL = ENV_TOKENS.get("FEEDBACK_SUBMISSION_EMAIL")
############################## SECURE AUTH ITEMS ############### ############################## SECURE AUTH ITEMS ###############
# Secret things: passwords, access keys, etc. # Secret things: passwords, access keys, etc.
...@@ -115,3 +117,6 @@ DATADOG_API = AUTH_TOKENS.get("DATADOG_API") ...@@ -115,3 +117,6 @@ DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
# Analytics dashboard server # Analytics dashboard server
ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL") ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL")
ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", "") ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", "")
ZENDESK_USER = AUTH_TOKENS.get("ZENDESK_USER")
ZENDESK_API_KEY = AUTH_TOKENS.get("ZENDESK_API_KEY")
...@@ -90,7 +90,10 @@ MITX_FEATURES = { ...@@ -90,7 +90,10 @@ MITX_FEATURES = {
# Give a UI to show a student's submission history in a problem by the # Give a UI to show a student's submission history in a problem by the
# Staff Debug tool. # Staff Debug tool.
'ENABLE_STUDENT_HISTORY_VIEW': True 'ENABLE_STUDENT_HISTORY_VIEW': True,
# Provide a UI to allow users to submit feedback from the LMS
'ENABLE_FEEDBACK_SUBMISSION': False,
} }
# Used for A/B testing # Used for A/B testing
...@@ -323,6 +326,14 @@ WIKI_LINK_DEFAULT_LEVEL = 2 ...@@ -323,6 +326,14 @@ WIKI_LINK_DEFAULT_LEVEL = 2
PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX" PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX"
# TESTCENTER_ACCOMMODATION_REQUEST_EMAIL = "exam-help@edx.org" # TESTCENTER_ACCOMMODATION_REQUEST_EMAIL = "exam-help@edx.org"
##### Feedback submission mechanism #####
FEEDBACK_SUBMISSION_EMAIL = None
##### Zendesk #####
ZENDESK_URL = None
ZENDESK_USER = None
ZENDESK_API_KEY = None
################################# open ended grading config ##################### ################################# open ended grading config #####################
#By setting up the default settings with an incorrect user name and password, #By setting up the default settings with an incorrect user name and password,
...@@ -582,3 +593,4 @@ INSTALLED_APPS = ( ...@@ -582,3 +593,4 @@ INSTALLED_APPS = (
# Discussion forums # Discussion forums
'django_comment_client', 'django_comment_client',
) )
...@@ -116,8 +116,9 @@ urlpatterns = ('', ...@@ -116,8 +116,9 @@ urlpatterns = ('',
# Favicon # Favicon
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}), (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
url(r'^submit_feedback$', 'util.views.submit_feedback_via_zendesk'),
# TODO: These urls no longer work. They need to be updated before they are re-enabled # TODO: These urls no longer work. They need to be updated before they are re-enabled
# url(r'^send_feedback$', 'util.views.send_feedback'),
# url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'), # url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'),
) )
......
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