Commit ec3c8458 by Greg Price

Merge pull request #1911 from edx/feature/gprice/feedback-button

parents 66185423 203a958e
...@@ -2,7 +2,7 @@ from student.models import (User, UserProfile, Registration, ...@@ -2,7 +2,7 @@ from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed, CourseEnrollment) CourseEnrollmentAllowed, CourseEnrollment)
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from datetime import datetime from datetime import datetime
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall, post_generation
from uuid import uuid4 from uuid import uuid4
...@@ -45,6 +45,16 @@ class UserFactory(DjangoModelFactory): ...@@ -45,6 +45,16 @@ class UserFactory(DjangoModelFactory):
last_login = datetime(2012, 1, 1) last_login = datetime(2012, 1, 1)
date_joined = datetime(2011, 1, 1) date_joined = datetime(2011, 1, 1)
@post_generation
def profile(obj, create, extracted, **kwargs):
if create:
obj.save()
return UserProfileFactory.create(user=obj, **kwargs)
elif kwargs:
raise Exception("Cannot build a user profile without saving the user")
else:
return None
class AdminFactory(UserFactory): class AdminFactory(UserFactory):
is_staff = True is_staff = True
......
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: try:
browser = request.META['HTTP_USER_AGENT'] validate_email(email)
except: except ValidationError:
browser = "Unknown" return build_error_response(400, "email", required_field_errs["email"])
feedback = render_to_string("feedback_email.txt", for header in ["HTTP_REFERER", "HTTP_USER_AGENT"]:
{"subject": request.POST['subject'], additional_info[header] = request.META.get(header)
"url": request.POST['url'],
"time": datetime.datetime.now().isoformat(), zendesk_api = _ZendeskApi()
"feedback": request.POST['message'],
"email": email, additional_info_string = (
"browser": browser, "Additional information:\n\n" +
"user": username}) "\n".join("%s: %s" % (key, value) for (key, value) in additional_info.items() if value is not None)
send_mail("MITx Feedback / " + request.POST['subject'],
feedback,
settings.DEFAULT_FROM_EMAIL,
[settings.DEFAULT_FEEDBACK_EMAIL],
fail_silently=False
) )
return HttpResponse(json.dumps({'success': True}))
new_ticket = {
"ticket": {
"requester": {"name": realname, "email": email},
"subject": subject,
"comment": {"body": details},
"tags": tags
}
}
try:
ticket_id = zendesk_api.create_ticket(new_ticket)
except zendesk.ZendeskError as err:
log.error("%s", str(err))
return HttpResponse(status=500)
# 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 as err:
log.error("%s", str(err))
# The update is not strictly necessary, so do not indicate failure to the user
pass
return HttpResponse()
def info(request): def info(request):
......
...@@ -37,11 +37,17 @@ class XModuleCourseFactory(Factory): ...@@ -37,11 +37,17 @@ class XModuleCourseFactory(Factory):
new_course.display_name = display_name new_course.display_name = display_name
new_course.lms.start = gmtime() new_course.lms.start = gmtime()
new_course.tabs = [{"type": "courseware"}, new_course.tabs = kwargs.get(
'tabs',
[
{"type": "courseware"},
{"type": "course_info", "name": "Course Info"}, {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"}, {"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}, {"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}] {"type": "progress", "name": "Progress"}
]
)
new_course.discussion_link = kwargs.get('discussion_link')
# Update the data in the mongo datastore # Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), own_metadata(new_course)) store.update_metadata(new_course.location.url(), own_metadata(new_course))
......
...@@ -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
...@@ -294,6 +294,27 @@ def get_course_tabs(user, course, active_page): ...@@ -294,6 +294,27 @@ def get_course_tabs(user, course, active_page):
return tabs return tabs
def get_discussion_link(course):
"""
Return the URL for the discussion tab for the given `course`.
If they have a discussion link specified, use that even if we disable
discussions. Disabling discsussions is mostly a server safety feature at
this point, and we don't need to worry about external sites. Otherwise,
if the course has a discussion tab or uses the default tabs, return the
discussion view URL. Otherwise, return None to indicate the lack of a
discussion tab.
"""
if course.discussion_link:
return course.discussion_link
elif not settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
return None
elif hasattr(course, 'tabs') and course.tabs and not any([tab['type'] == 'discussion' for tab in course.tabs]):
return None
else:
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id])
def get_default_tabs(user, course, active_page): def get_default_tabs(user, course, active_page):
# When calling the various _tab methods, can omit the 'type':'blah' from the # When calling the various _tab methods, can omit the 'type':'blah' from the
...@@ -308,15 +329,9 @@ def get_default_tabs(user, course, active_page): ...@@ -308,15 +329,9 @@ def get_default_tabs(user, course, active_page):
tabs.extend(_textbooks({}, user, course, active_page)) tabs.extend(_textbooks({}, user, course, active_page))
## If they have a discussion link specified, use that even if we feature discussion_link = get_discussion_link(course)
## flag discussions off. Disabling that is mostly a server safety feature if discussion_link:
## at this point, and we don't need to worry about external sites. tabs.append(CourseTab('Discussion', discussion_link, active_page == 'discussion'))
if course.discussion_link:
tabs.append(CourseTab('Discussion', course.discussion_link, active_page == 'discussion'))
elif settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
link = reverse('django_comment_client.forum.views.forum_form_discussion',
args=[course.id])
tabs.append(CourseTab('Discussion', link, active_page == 'discussion'))
tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page)) tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page))
......
from django.test import TestCase from django.test import TestCase
from mock import MagicMock from mock import MagicMock
from mock import patch
import courseware.tabs as tabs import courseware.tabs as tabs
from django.test.utils import override_settings from django.test.utils import override_settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
class ProgressTestCase(TestCase): class ProgressTestCase(TestCase):
...@@ -257,3 +261,62 @@ class ValidateTabsTestCase(TestCase): ...@@ -257,3 +261,62 @@ class ValidateTabsTestCase(TestCase):
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2]) self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2])
self.assertIsNone(tabs.validate_tabs(self.courses[3])) self.assertIsNone(tabs.validate_tabs(self.courses[3]))
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[4]) self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[4])
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class DiscussionLinkTestCase(ModuleStoreTestCase):
def setUp(self):
self.tabs_with_discussion = [
{'type':'courseware'},
{'type':'course_info'},
{'type':'discussion'},
{'type':'textbooks'},
]
self.tabs_without_discussion = [
{'type':'courseware'},
{'type':'course_info'},
{'type':'textbooks'},
]
@staticmethod
def _patch_reverse(course):
def patched_reverse(viewname, args):
if viewname == "django_comment_client.forum.views.forum_form_discussion" and args == [course.id]:
return "default_discussion_link"
else:
return None
return patch("courseware.tabs.reverse", patched_reverse)
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
def test_explicit_discussion_link(self):
"""Test that setting discussion_link overrides everything else"""
course = CourseFactory.create(discussion_link="other_discussion_link", tabs=self.tabs_with_discussion)
self.assertEqual(tabs.get_discussion_link(course), "other_discussion_link")
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": False})
def test_discussions_disabled(self):
"""Test that other cases return None with discussions disabled"""
for i, t in enumerate([None, self.tabs_with_discussion, self.tabs_without_discussion]):
course = CourseFactory.create(tabs=t, number=str(i))
self.assertEqual(tabs.get_discussion_link(course), None)
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def test_no_tabs(self):
"""Test a course without tabs configured"""
course = CourseFactory.create(tabs=None)
with self._patch_reverse(course):
self.assertEqual(tabs.get_discussion_link(course), "default_discussion_link")
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def test_tabs_with_discussion(self):
"""Test a course with a discussion tab configured"""
course = CourseFactory.create(tabs=self.tabs_with_discussion)
with self._patch_reverse(course):
self.assertEqual(tabs.get_discussion_link(course), "default_discussion_link")
@patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def test_tabs_without_discussion(self):
"""Test a course with tabs configured but without a discussion tab"""
course = CourseFactory.create(tabs=self.tabs_without_discussion)
self.assertEqual(tabs.get_discussion_link(course), None)
...@@ -49,7 +49,6 @@ class TestGradebook(ModuleStoreTestCase): ...@@ -49,7 +49,6 @@ class TestGradebook(ModuleStoreTestCase):
] ]
for user in self.users: for user in self.users:
UserProfileFactory.create(user=user)
CourseEnrollmentFactory.create(user=user, course_id=self.course.id) CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
for i in xrange(USER_COUNT-1): for i in xrange(USER_COUNT-1):
......
...@@ -88,6 +88,8 @@ META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {}) ...@@ -88,6 +88,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.
...@@ -123,3 +125,6 @@ DATADOG_API = AUTH_TOKENS.get("DATADOG_API") ...@@ -123,3 +125,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',
) )
...@@ -202,5 +202,62 @@ mark { ...@@ -202,5 +202,62 @@ mark {
} }
} }
.help-tab {
@include transform(rotate(-90deg));
@include transform-origin(0 0);
top: 50%;
left: 0;
position: fixed;
z-index: 99;
a:link, a:visited {
cursor: pointer;
border: 1px solid #ccc;
border-top-style: none;
@include border-radius(0px 0px 10px 10px);
background: transparentize(#fff, 0.25);
color: transparentize(#333, 0.25);
font-weight: bold;
text-decoration: none;
padding: 6px 22px 11px;
display: inline-block;
&:hover {
color: #fff;
background: #1D9DD9;
}
}
}
.help-buttons {
padding: 10px 50px;
a:link, a:visited {
padding: 15px 0px;
text-align: center;
cursor: pointer;
background: #fff;
text-decoration: none;
display: block;
border: 1px solid #ccc;
&#feedback_link_problem {
border-bottom-style: none;
@include border-radius(10px 10px 0px 0px);
}
&#feedback_link_question {
border-top-style: none;
@include border-radius(0px 0px 10px 10px);
}
&:hover {
color: #fff;
background: #1D9DD9;
}
}
}
#feedback_form textarea[name="details"] {
height: 150px;
}
...@@ -155,7 +155,7 @@ ...@@ -155,7 +155,7 @@
display: block; display: block;
color: #8F0E0E; color: #8F0E0E;
+ input { + input, + textarea {
border: 1px solid #CA1111; border: 1px solid #CA1111;
color: #8F0E0E; color: #8F0E0E;
} }
......
<%namespace name='static' file='static_content.html'/>
<%! from django.conf import settings %>
<%! from courseware.tabs import get_discussion_link %>
% if settings.MITX_FEATURES.get('ENABLE_FEEDBACK_SUBMISSION', False):
<div class="help-tab">
<a href="#help-modal" rel="leanModal">Help</a>
</div>
<section id="help-modal" class="modal">
<div class="inner-wrapper" id="help_wrapper">
<header>
<h2><span class="edx">edX</span> Help</h2>
<hr>
</header>
<%
discussion_link = get_discussion_link(course) if course else None
%>
% if discussion_link:
<p>
Have a course-specific question?
<a href="${discussion_link}" target="_blank"/>
Post it on the course forums.
</a>
</p>
<hr>
% endif
<p>Have a general question about edX? <a href="/help" target="_blank">Check the FAQ</a>.</p>
<hr>
<div class="help-buttons">
<a href="#" id="feedback_link_problem">Report a problem</a>
<a href="#" id="feedback_link_suggestion">Make a suggestion</a>
<a href="#" id="feedback_link_question">Ask a question</a>
</div>
## TODO: find a way to refactor this
<div class="close-modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</div>
<div class="inner-wrapper" id="feedback_form_wrapper">
<header></header>
<form id="feedback_form" class="feedback_form" method="post" data-remote="true" action="/submit_feedback">
<div id="feedback_error" class="modal-form-error"></div>
% if not user.is_authenticated():
<label data-field="name">Name*</label>
<input name="name" type="text">
<label data-field="email">E-mail*</label>
<input name="email" type="text">
% endif
<label data-field="subject">Subject*</label>
<input name="subject" type="text">
<label data-field="details">Details*</label>
<textarea name="details"></textarea>
<input name="tag" type="hidden">
<div class="submit">
<input name="submit" type="submit" value="Submit">
</div>
</form>
<div class="close-modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</div>
<div class="inner-wrapper" id="feedback_success_wrapper">
<header>
<h2>Thank You!</h2>
<hr>
</header>
<p>
Thanks for your feedback. We will read your message, and our
support team may contact you to respond or ask for further clarification.
</p>
<div class="close-modal">
<div class="inner">
<p>&#10005;</p>
</div>
</div>
</div>
</section>
<script type="text/javascript">
(function() {
$(".help-tab").click(function() {
$(".field-error").removeClass("field-error");
$("#feedback_form")[0].reset();
$("#feedback_form input[type='submit']").removeAttr("disabled");
$("#feedback_form_wrapper").css("display", "none");
$("#feedback_error").css("display", "none");
$("#feedback_success_wrapper").css("display", "none");
$("#help_wrapper").css("display", "block");
});
showFeedback = function(e, tag, title) {
$("#help_wrapper").css("display", "none");
$("#feedback_form input[name='tag']").val(tag);
$("#feedback_form_wrapper").css("display", "block");
$("#feedback_form_wrapper header").html("<h2>" + title + "</h2><hr>");
e.preventDefault();
};
$("#feedback_link_problem").click(function(e) {
showFeedback(e, "problem", "Report a Problem");
});
$("#feedback_link_suggestion").click(function(e) {
showFeedback(e, "suggestion", "Make a Suggestion");
});
$("#feedback_link_question").click(function(e) {
showFeedback(e, "question", "Ask a Question");
});
$("#feedback_form").submit(function() {
$("input[type='submit']", this).attr("disabled", "disabled");
});
$("#feedback_form").on("ajax:complete", function() {
$("input[type='submit']", this).removeAttr("disabled");
});
$("#feedback_form").on("ajax:success", function(event, data, status, xhr) {
$("#feedback_form_wrapper").css("display", "none");
$("#feedback_success_wrapper").css("display", "block");
});
$("#feedback_form").on("ajax:error", function(event, xhr, status, error) {
$(".field-error").removeClass("field-error");
var responseData;
try {
responseData = jQuery.parseJSON(xhr.responseText);
} catch(err) {
}
if (responseData) {
$("[data-field='"+responseData.field+"']").addClass("field-error");
$("#feedback_error").html(responseData.error).stop().css("display", "block");
} else {
// If no data (or malformed data) is returned, a server error occurred
htmlStr = "An error has occurred.";
% if settings.FEEDBACK_SUBMISSION_EMAIL:
htmlStr += " Please <a href='#' id='feedback_email'>send us e-mail</a>.";
% else:
// If no email is configured, we can't do much other than
// ask the user to try again later
htmlStr += " Please try again later.";
% endif
$("#feedback_error").html(htmlStr).stop().css("display", "block");
% if settings.FEEDBACK_SUBMISSION_EMAIL:
$("#feedback_email").click(function(e) {
mailto = "mailto:" + "${settings.FEEDBACK_SUBMISSION_EMAIL}" +
"?subject=" + $("#feedback_form input[name='subject']").val() +
"&body=" + $("#feedback_form textarea[name='details']").val();
window.open(mailto);
e.preventDefault();
});
%endif
}
});
})(this)
</script>
%endif
...@@ -96,3 +96,5 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -96,3 +96,5 @@ site_status_msg = get_site_status_msg(course_id)
<%include file="signup_modal.html" /> <%include file="signup_modal.html" />
<%include file="forgot_password_modal.html" /> <%include file="forgot_password_modal.html" />
%endif %endif
<%include file="help_modal.html"/>
...@@ -114,8 +114,9 @@ urlpatterns = ('', # nopep8 ...@@ -114,8 +114,9 @@ urlpatterns = ('', # nopep8
# 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