Merge branch 'master' into feature/christina/fields
This commit is contained in:
@@ -2,7 +2,7 @@ from student.models import (User, UserProfile, Registration,
|
||||
CourseEnrollmentAllowed, CourseEnrollment)
|
||||
from django.contrib.auth.models import Group
|
||||
from datetime import datetime
|
||||
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall
|
||||
from factory import DjangoModelFactory, Factory, SubFactory, PostGenerationMethodCall, post_generation
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
@@ -45,6 +45,16 @@ class UserFactory(DjangoModelFactory):
|
||||
last_login = datetime(2012, 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):
|
||||
is_staff = True
|
||||
|
||||
@@ -1,16 +1,280 @@
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
"""Tests for the util package"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from student.tests.factories import UserFactory
|
||||
from util import views
|
||||
from zendesk import ZendeskError
|
||||
import json
|
||||
import mock
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
@mock.patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_FEEDBACK_SUBMISSION": True})
|
||||
@override_settings(ZENDESK_URL="dummy", ZENDESK_USER="dummy", ZENDESK_API_KEY="dummy")
|
||||
@mock.patch("util.views._ZendeskApi", autospec=True)
|
||||
class SubmitFeedbackViaZendeskTest(TestCase):
|
||||
def setUp(self):
|
||||
"""Set up data for the test case"""
|
||||
self._request_factory = RequestFactory()
|
||||
self._anon_user = AnonymousUser()
|
||||
self._auth_user = UserFactory.create(
|
||||
email="test@edx.org",
|
||||
username="test",
|
||||
profile__name="Test User"
|
||||
)
|
||||
# This contains a tag to ensure that tags are submitted correctly
|
||||
self._anon_fields = {
|
||||
"email": "test@edx.org",
|
||||
"name": "Test User",
|
||||
"subject": "a subject",
|
||||
"details": "some details",
|
||||
"tag": "a tag"
|
||||
}
|
||||
# This does not contain a tag to ensure that tag is optional
|
||||
self._auth_fields = {"subject": "a subject", "details": "some details"}
|
||||
|
||||
def _test_request(self, user, fields):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
Generate a request and invoke the view, returning the response.
|
||||
|
||||
The request will be a POST request from the given `user`, with the given
|
||||
`fields` in the POST body.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
||||
req = self._request_factory.post(
|
||||
"/submit_feedback",
|
||||
data=fields,
|
||||
HTTP_REFERER="test_referer",
|
||||
HTTP_USER_AGENT="test_user_agent"
|
||||
)
|
||||
req.user = user
|
||||
return views.submit_feedback_via_zendesk(req)
|
||||
|
||||
def _assert_bad_request(self, response, field, zendesk_mock_class):
|
||||
"""
|
||||
Assert that the given `response` contains correct failure data.
|
||||
|
||||
It should have a 400 status code, and its content should be a JSON
|
||||
object containing the specified `field` and an `error`.
|
||||
"""
|
||||
self.assertEqual(response.status_code, 400)
|
||||
resp_json = json.loads(response.content)
|
||||
self.assertTrue("field" in resp_json)
|
||||
self.assertEqual(resp_json["field"], field)
|
||||
self.assertTrue("error" in resp_json)
|
||||
# There should be absolutely no interaction with Zendesk
|
||||
self.assertFalse(zendesk_mock_class.return_value.mock_calls)
|
||||
|
||||
def _test_bad_request_omit_field(self, user, fields, omit_field, zendesk_mock_class):
|
||||
"""
|
||||
Invoke the view with a request missing a field and assert correctness.
|
||||
|
||||
The request will be a POST request from the given `user`, with POST
|
||||
fields taken from `fields` minus the entry specified by `omit_field`.
|
||||
The response should have a 400 (bad request) status code and specify
|
||||
the invalid field and an error message, and the Zendesk API should not
|
||||
have been invoked.
|
||||
"""
|
||||
filtered_fields = {k: v for (k, v) in fields.items() if k != omit_field}
|
||||
resp = self._test_request(user, filtered_fields)
|
||||
self._assert_bad_request(resp, omit_field, zendesk_mock_class)
|
||||
|
||||
def _test_bad_request_empty_field(self, user, fields, empty_field, zendesk_mock_class):
|
||||
"""
|
||||
Invoke the view with an empty field and assert correctness.
|
||||
|
||||
The request will be a POST request from the given `user`, with POST
|
||||
fields taken from `fields`, replacing the entry specified by
|
||||
`empty_field` with the empty string. The response should have a 400
|
||||
(bad request) status code and specify the invalid field and an error
|
||||
message, and the Zendesk API should not have been invoked.
|
||||
"""
|
||||
altered_fields = fields.copy()
|
||||
altered_fields[empty_field] = ""
|
||||
resp = self._test_request(user, altered_fields)
|
||||
self._assert_bad_request(resp, empty_field, zendesk_mock_class)
|
||||
|
||||
def _test_success(self, user, fields):
|
||||
"""
|
||||
Generate a request, invoke the view, and assert success.
|
||||
|
||||
The request will be a POST request from the given `user`, with the given
|
||||
`fields` in the POST body. The response should have a 200 (success)
|
||||
status code.
|
||||
"""
|
||||
resp = self._test_request(user, fields)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_bad_request_anon_user_no_name(self, zendesk_mock_class):
|
||||
"""Test a request from an anonymous user not specifying `name`."""
|
||||
self._test_bad_request_omit_field(self._anon_user, self._anon_fields, "name", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._anon_user, self._anon_fields, "name", zendesk_mock_class)
|
||||
|
||||
def test_bad_request_anon_user_no_email(self, zendesk_mock_class):
|
||||
"""Test a request from an anonymous user not specifying `email`."""
|
||||
self._test_bad_request_omit_field(self._anon_user, self._anon_fields, "email", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._anon_user, self._anon_fields, "email", zendesk_mock_class)
|
||||
|
||||
def test_bad_request_anon_user_no_subject(self, zendesk_mock_class):
|
||||
"""Test a request from an anonymous user not specifying `subject`."""
|
||||
self._test_bad_request_omit_field(self._anon_user, self._anon_fields, "subject", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._anon_user, self._anon_fields, "subject", zendesk_mock_class)
|
||||
|
||||
def test_bad_request_anon_user_no_details(self, zendesk_mock_class):
|
||||
"""Test a request from an anonymous user not specifying `details`."""
|
||||
self._test_bad_request_omit_field(self._anon_user, self._anon_fields, "details", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._anon_user, self._anon_fields, "details", zendesk_mock_class)
|
||||
|
||||
def test_valid_request_anon_user(self, zendesk_mock_class):
|
||||
"""
|
||||
Test a valid request from an anonymous user.
|
||||
|
||||
The response should have a 200 (success) status code, and a ticket with
|
||||
the given information should have been submitted via the Zendesk API.
|
||||
"""
|
||||
zendesk_mock_instance = zendesk_mock_class.return_value
|
||||
zendesk_mock_instance.create_ticket.return_value = 42
|
||||
self._test_success(self._anon_user, self._anon_fields)
|
||||
expected_calls = [
|
||||
mock.call.create_ticket(
|
||||
{
|
||||
"ticket": {
|
||||
"requester": {"name": "Test User", "email": "test@edx.org"},
|
||||
"subject": "a subject",
|
||||
"comment": {"body": "some details"},
|
||||
"tags": ["a tag"]
|
||||
}
|
||||
}
|
||||
),
|
||||
mock.call.update_ticket(
|
||||
42,
|
||||
{
|
||||
"ticket": {
|
||||
"comment": {
|
||||
"public": False,
|
||||
"body":
|
||||
"Additional information:\n\n"
|
||||
"HTTP_USER_AGENT: test_user_agent\n"
|
||||
"HTTP_REFERER: test_referer"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
self.assertEqual(zendesk_mock_instance.mock_calls, expected_calls)
|
||||
|
||||
def test_bad_request_auth_user_no_subject(self, zendesk_mock_class):
|
||||
"""Test a request from an authenticated user not specifying `subject`."""
|
||||
self._test_bad_request_omit_field(self._auth_user, self._auth_fields, "subject", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._auth_user, self._auth_fields, "subject", zendesk_mock_class)
|
||||
|
||||
def test_bad_request_auth_user_no_details(self, zendesk_mock_class):
|
||||
"""Test a request from an authenticated user not specifying `details`."""
|
||||
self._test_bad_request_omit_field(self._auth_user, self._auth_fields, "details", zendesk_mock_class)
|
||||
self._test_bad_request_empty_field(self._auth_user, self._auth_fields, "details", zendesk_mock_class)
|
||||
|
||||
def test_valid_request_auth_user(self, zendesk_mock_class):
|
||||
"""
|
||||
Test a valid request from an authenticated user.
|
||||
|
||||
The response should have a 200 (success) status code, and a ticket with
|
||||
the given information should have been submitted via the Zendesk API.
|
||||
"""
|
||||
zendesk_mock_instance = zendesk_mock_class.return_value
|
||||
zendesk_mock_instance.create_ticket.return_value = 42
|
||||
self._test_success(self._auth_user, self._auth_fields)
|
||||
expected_calls = [
|
||||
mock.call.create_ticket(
|
||||
{
|
||||
"ticket": {
|
||||
"requester": {"name": "Test User", "email": "test@edx.org"},
|
||||
"subject": "a subject",
|
||||
"comment": {"body": "some details"},
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
),
|
||||
mock.call.update_ticket(
|
||||
42,
|
||||
{
|
||||
"ticket": {
|
||||
"comment": {
|
||||
"public": False,
|
||||
"body":
|
||||
"Additional information:\n\n"
|
||||
"username: test\n"
|
||||
"HTTP_USER_AGENT: test_user_agent\n"
|
||||
"HTTP_REFERER: test_referer"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
self.assertEqual(zendesk_mock_instance.mock_calls, expected_calls)
|
||||
|
||||
def test_get_request(self, zendesk_mock_class):
|
||||
"""Test that a GET results in a 405 even with all required fields"""
|
||||
req = self._request_factory.get("/submit_feedback", data=self._anon_fields)
|
||||
req.user = self._anon_user
|
||||
resp = views.submit_feedback_via_zendesk(req)
|
||||
self.assertEqual(resp.status_code, 405)
|
||||
self.assertIn("Allow", resp)
|
||||
self.assertEqual(resp["Allow"], "POST")
|
||||
# There should be absolutely no interaction with Zendesk
|
||||
self.assertFalse(zendesk_mock_class.mock_calls)
|
||||
|
||||
def test_zendesk_error_on_create(self, zendesk_mock_class):
|
||||
"""
|
||||
Test Zendesk returning an error on ticket creation.
|
||||
|
||||
We should return a 500 error with no body
|
||||
"""
|
||||
err = ZendeskError(msg="", error_code=404)
|
||||
zendesk_mock_instance = zendesk_mock_class.return_value
|
||||
zendesk_mock_instance.create_ticket.side_effect = err
|
||||
resp = self._test_request(self._anon_user, self._anon_fields)
|
||||
self.assertEqual(resp.status_code, 500)
|
||||
self.assertFalse(resp.content)
|
||||
|
||||
def test_zendesk_error_on_update(self, zendesk_mock_class):
|
||||
"""
|
||||
Test for Zendesk returning an error on ticket update.
|
||||
|
||||
If Zendesk returns any error on ticket update, we return a 200 to the
|
||||
browser because the update contains additional information that is not
|
||||
necessary for the user to have submitted their feedback.
|
||||
"""
|
||||
err = ZendeskError(msg="", error_code=500)
|
||||
zendesk_mock_instance = zendesk_mock_class.return_value
|
||||
zendesk_mock_instance.update_ticket.side_effect = err
|
||||
resp = self._test_request(self._anon_user, self._anon_fields)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@mock.patch.dict("django.conf.settings.MITX_FEATURES", {"ENABLE_FEEDBACK_SUBMISSION": False})
|
||||
def test_not_enabled(self, zendesk_mock_class):
|
||||
"""
|
||||
Test for Zendesk submission not enabled in `settings`.
|
||||
|
||||
We should raise Http404.
|
||||
"""
|
||||
with self.assertRaises(Http404):
|
||||
self._test_request(self._anon_user, self._anon_fields)
|
||||
|
||||
def test_zendesk_not_configured(self, zendesk_mock_class):
|
||||
"""
|
||||
Test for Zendesk not fully configured in `settings`.
|
||||
|
||||
For each required configuration parameter, test that setting it to
|
||||
`None` causes an otherwise valid request to return a 500 error.
|
||||
"""
|
||||
def test_case(missing_config):
|
||||
with mock.patch(missing_config, None):
|
||||
with self.assertRaises(Exception):
|
||||
self._test_request(self._anon_user, self._anon_fields)
|
||||
|
||||
test_case("django.conf.settings.ZENDESK_URL")
|
||||
test_case("django.conf.settings.ZENDESK_USER")
|
||||
test_case("django.conf.settings.ZENDESK_API_KEY")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import pprint
|
||||
import sys
|
||||
|
||||
@@ -7,15 +8,21 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.mail import send_mail
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.core.validators import ValidationError, validate_email
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponseServerError
|
||||
from django.shortcuts import redirect
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from urllib import urlencode
|
||||
import zendesk
|
||||
|
||||
import capa.calc
|
||||
import track.views
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def calculate(request):
|
||||
''' Calculator in footer of every page. '''
|
||||
equation = request.GET['equation']
|
||||
@@ -29,36 +36,142 @@ def calculate(request):
|
||||
return HttpResponse(json.dumps({'result': str(result)}))
|
||||
|
||||
|
||||
def send_feedback(request):
|
||||
''' Feeback mechanism in footer of every page. '''
|
||||
try:
|
||||
username = request.user.username
|
||||
class _ZendeskApi(object):
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
except:
|
||||
username = "anonymous"
|
||||
email = "anonymous"
|
||||
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:
|
||||
browser = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
browser = "Unknown"
|
||||
ticket_id = zendesk_api.create_ticket(new_ticket)
|
||||
except zendesk.ZendeskError as err:
|
||||
log.error("%s", str(err))
|
||||
return HttpResponse(status=500)
|
||||
|
||||
feedback = render_to_string("feedback_email.txt",
|
||||
{"subject": request.POST['subject'],
|
||||
"url": request.POST['url'],
|
||||
"time": datetime.datetime.now().isoformat(),
|
||||
"feedback": request.POST['message'],
|
||||
"email": email,
|
||||
"browser": browser,
|
||||
"user": username})
|
||||
# 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
|
||||
|
||||
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}))
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
def info(request):
|
||||
|
||||
5
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
5
common/lib/xmodule/xmodule/js/src/.gitignore
vendored
@@ -1 +1,4 @@
|
||||
# Please do not ignore *.js files. Some xmodules are written in JS.
|
||||
# Ignore .js files in this folder as they are compiled from coffeescript
|
||||
# For each of the xmodules subdirectories, add a .gitignore file that
|
||||
# will version any *.js file that is specifically written, not compiled.
|
||||
*.js
|
||||
|
||||
2
common/lib/xmodule/xmodule/js/src/capa/.gitignore
vendored
Normal file
2
common/lib/xmodule/xmodule/js/src/capa/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
!imageinput.js
|
||||
!schematic.js
|
||||
1
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.js
|
||||
1
common/lib/xmodule/xmodule/js/src/poll/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/poll/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.js
|
||||
1
common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!*.js
|
||||
1
common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore
vendored
Normal file
1
common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!html5_video.js
|
||||
@@ -37,11 +37,17 @@ class XModuleCourseFactory(Factory):
|
||||
new_course.display_name = display_name
|
||||
|
||||
new_course.lms.start = gmtime()
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
new_course.tabs = kwargs.get(
|
||||
'tabs',
|
||||
[
|
||||
{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}
|
||||
]
|
||||
)
|
||||
new_course.discussion_link = kwargs.get('discussion_link')
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), own_metadata(new_course))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
-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/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
|
||||
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
|
||||
|
||||
# Our libraries:
|
||||
-e git+https://github.com/edx/XBlock.git@483e0cb1#egg=XBlock
|
||||
|
||||
@@ -294,6 +294,27 @@ def get_course_tabs(user, course, active_page):
|
||||
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):
|
||||
|
||||
# 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):
|
||||
|
||||
tabs.extend(_textbooks({}, user, course, active_page))
|
||||
|
||||
## If they have a discussion link specified, use that even if we feature
|
||||
## flag discussions off. Disabling that is mostly a server safety feature
|
||||
## at this point, and we don't need to worry about external sites.
|
||||
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'))
|
||||
discussion_link = get_discussion_link(course)
|
||||
if discussion_link:
|
||||
tabs.append(CourseTab('Discussion', discussion_link, active_page == 'discussion'))
|
||||
|
||||
tabs.extend(_wiki({'name': 'Wiki', 'type': 'wiki'}, user, course, active_page))
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from django.test import TestCase
|
||||
from mock import MagicMock
|
||||
from mock import patch
|
||||
|
||||
import courseware.tabs as tabs
|
||||
|
||||
from django.test.utils import override_settings
|
||||
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):
|
||||
|
||||
@@ -257,3 +261,62 @@ class ValidateTabsTestCase(TestCase):
|
||||
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2])
|
||||
self.assertIsNone(tabs.validate_tabs(self.courses[3]))
|
||||
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)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'''
|
||||
Test for lms courseware app
|
||||
'''
|
||||
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
|
||||
from urlparse import urlsplit, urlunsplit
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.test import TestCase
|
||||
@@ -62,7 +62,7 @@ def mongo_store_config(data_dir):
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'collection': 'modulestore_%s' % uuid4().hex,
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
@@ -81,7 +81,7 @@ def draft_mongo_store_config(data_dir):
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'collection': 'modulestore_%s' % uuid4().hex,
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
@@ -92,7 +92,7 @@ def draft_mongo_store_config(data_dir):
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'collection': 'modulestore_%s' % uuid4().hex,
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ class TestGradebook(ModuleStoreTestCase):
|
||||
]
|
||||
|
||||
for user in self.users:
|
||||
UserProfileFactory.create(user=user)
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
for i in xrange(USER_COUNT-1):
|
||||
@@ -151,4 +150,4 @@ class TestLetterCutoffPolicy(TestGradebook):
|
||||
# User 0 has 0 on Homeworks [1]
|
||||
# User 0 has 0 on the class [1]
|
||||
# One use at the top of the page [1]
|
||||
self.assertEquals(3, self.response.content.count('grade_None'))
|
||||
self.assertEquals(3, self.response.content.count('grade_None'))
|
||||
|
||||
@@ -88,6 +88,8 @@ META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {})
|
||||
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '')
|
||||
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '')
|
||||
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 ###############
|
||||
# Secret things: passwords, access keys, etc.
|
||||
@@ -123,3 +125,6 @@ DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
|
||||
# Analytics dashboard server
|
||||
ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL")
|
||||
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 = {
|
||||
|
||||
# Give a UI to show a student's submission history in a problem by the
|
||||
# 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
|
||||
@@ -323,6 +326,14 @@ WIKI_LINK_DEFAULT_LEVEL = 2
|
||||
PEARSONVUE_SIGNINPAGE_URL = "https://www1.pearsonvue.com/testtaker/signin/SignInPage/EDX"
|
||||
# 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 #####################
|
||||
|
||||
#By setting up the default settings with an incorrect user name and password,
|
||||
@@ -582,3 +593,4 @@ INSTALLED_APPS = (
|
||||
# Discussion forums
|
||||
'django_comment_client',
|
||||
)
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
display: block;
|
||||
color: #8F0E0E;
|
||||
|
||||
+ input {
|
||||
+ input, + textarea {
|
||||
border: 1px solid #CA1111;
|
||||
color: #8F0E0E;
|
||||
}
|
||||
|
||||
167
lms/templates/help_modal.html
Normal file
167
lms/templates/help_modal.html
Normal file
@@ -0,0 +1,167 @@
|
||||
<%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>✕</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>✕</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>✕</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)
|
||||
<%include file="signup_modal.html" />
|
||||
<%include file="forgot_password_modal.html" />
|
||||
%endif
|
||||
|
||||
<%include file="help_modal.html"/>
|
||||
|
||||
@@ -114,8 +114,9 @@ urlpatterns = ('', # nopep8
|
||||
# Favicon
|
||||
(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
|
||||
# url(r'^send_feedback$', 'util.views.send_feedback'),
|
||||
# url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user