diff --git a/README b/README
deleted file mode 100644
index 2ed50ba063..0000000000
--- a/README
+++ /dev/null
@@ -1 +0,0 @@
-See doc/ for documentation.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..ec17d7c9a4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,148 @@
+This is edX, a platform for online course delivery. The project is primarily
+written in [Python](http://python.org/), using the
+[Django](https://www.djangoproject.com/) framework. We also use some
+[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/).
+
+Installation
+============
+The installation process is a bit messy at the moment. Here's a high-level
+overview of what you should do to get started.
+
+**TLDR:** There is a `create-dev-env.sh` script that will attempt to set all
+of this up for you. If you're in a hurry, run that script. Otherwise, I suggest
+that you understand what the script is doing, and why, by reading this document.
+
+Directory Hierarchy
+-------------------
+This code assumes that it is checked out in a directory that has three sibling
+directories: `data` (used for XML course data), `db` (used to hold a
+[sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you
+clone the repository into a directory called `edx` inside of a directory
+called `dev`, here's an example of how the directory hierarchy should look:
+
+ * dev
+ \
+ * data
+ * db
+ * log
+ * edx
+ \
+ README.md
+
+Language Runtimes
+-----------------
+You'll need to be sure that you have Python 2.7, Ruby 1.9.3, and NodeJS
+(latest stable) installed on your system. Some of these you can install
+using your system's package manager: [homebrew](http://mxcl.github.io/homebrew/)
+for Mac, [apt](http://wiki.debian.org/Apt) for Debian-based systems
+(including Ubuntu), [rpm](http://www.rpm.org/) or [yum](http://yum.baseurl.org/)
+for Red Hat based systems (including CentOS).
+
+If your system's package manager gives you the wrong version of a language
+runtime, then you'll need to use a versioning tool to install the correct version.
+Usually, you'll need to do this for Ruby: you can use
+[`rbenv`](https://github.com/sstephenson/rbenv) or [`rvm`](https://rvm.io/), but
+typically `rbenv` is simpler. For Python, you can use
+[`pythonz`](http://saghul.github.io/pythonz/),
+and for Node, you can use [`nvm`](https://github.com/creationix/nvm).
+
+Virtual Environments
+--------------------
+Often, different projects will have conflicting dependencies: for example, two
+projects depending on two different, incompatible versions of a library. Clearly,
+you can't have both versions installed and used on your machine simultaneously.
+Virtual environments were created to solve this problem: by installing libraries
+into an isolated environment, only projects that live inside the environment
+will be able to see and use those libraries. Got incompatible dependencies? Use
+different virtual environments, and your problem is solved.
+
+Remember, each language has a different implementation. Python has
+[`virtualenv`](http://www.virtualenv.org/), Ruby has
+[`bundler`](http://gembundler.com/), and Node's virtual environment support
+is built into [`npm`](https://npmjs.org/), its library management tool.
+For each language, decide if you want to use a virtual environment, or if you
+want to install all the language dependencies globally (and risk conflicts).
+I suggest you start with installing things globally until and unless things
+break; you can always switch over to a virtual environment later on.
+
+Language Packages
+-----------------
+The Python libraries we use are listed in `requirements.txt`. The Ruby libraries
+we use are listed in `Gemfile`. The Node libraries we use are listed in
+`packages.json`. Python has a library installer called
+[`pip`](http://www.pip-installer.org/), Ruby has a library installer called
+[`gem`](https://rubygems.org/) (or `bundle` if you're using a virtual
+environment), and Node has a library installer called
+[`npm`](https://npmjs.org/).
+Once you've got your languages and virtual environments set up, install
+the libraries like so:
+
+ $ pip install -r pre-requirements.txt
+ $ pip install -r requirements.txt
+ $ bundle install
+ $ npm install
+
+Other Dependencies
+------------------
+You'll also need to install [MongoDB](http://www.mongodb.org/), since our
+application uses it in addition to sqlite. You can install it through your
+system package manager, and I suggest that you configure it to start
+automatically when you boot up your system, so that you never have to worry
+about it again. For Mac, use
+[`launchd`](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/launchd.8.html)
+(running `brew info mongodb` will give you some commands you can copy-paste.)
+For Linux, you can use [`upstart`](http://upstart.ubuntu.com/), `chkconfig`,
+or any other process management tool.
+
+Configuring Your Project
+------------------------
+We use [`rake`](http://rake.rubyforge.org/) to execute common tasks in our
+project. The `rake` tasks are defined in the `rakefile`, or you can run `rake -T`
+to view a summary.
+
+Before you run your project, you need to create a sqlite database, create
+tables in that database, run database migrations, and populate templates for
+CMS templates. Fortunately, `rake` will do all of this for you! Just run:
+
+ $ rake django-admin[syncdb]
+ $ rake django-admin[migrate]
+ $ rake django-admin[update_templates]
+
+If you are running these commands using the [`zsh`](http://www.zsh.org/) shell,
+zsh will assume that you are doing
+[shell globbing](https://en.wikipedia.org/wiki/Glob_(programming)), search for
+a file in your directory named `django-adminsyncdb` or `django-adminmigrate`,
+and fail. To fix this, just surround the argument with quotation marks, so that
+you're running `rake "django-admin[syncdb]"`.
+
+Run Your Project
+----------------
+edX has two components: Studio, the course authoring system; and the LMS
+(learning management system) used by students. These two systems communicate
+through the MongoDB database, which stores course information.
+
+To run Studio, run:
+
+ $ rake cms
+
+To run the LMS, run:
+
+ $ rake lms[cms.dev]
+
+Studio runs on port 8001, while LMS runs on port 8000, so you can run both of
+these commands simultaneously, using two different terminal windows. To view
+Studio, visit `127.0.0.1:8001` in your web browser; to view the LMS, visit
+`127.0.0.1:8000`.
+
+There's also an older version of the LMS that saves its information in XML files
+in the `data` directory, instead of in Mongo. To run this older version, run:
+
+$ rake lms
+
+Further Documentation
+=====================
+Once you've got your project up and running, you can check out the `docs`
+directory to see more documentation about how edX is structured.
+
+
+
diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py
index 0d9621fc01..9560025441 100644
--- a/common/djangoapps/student/tests/factories.py
+++ b/common/djangoapps/student/tests/factories.py
@@ -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
diff --git a/common/djangoapps/util/tests.py b/common/djangoapps/util/tests.py
index 501deb776c..d829676eaf 100644
--- a/common/djangoapps/util/tests.py
+++ b/common/djangoapps/util/tests.py
@@ -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")
diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py
index cece37757b..c087e99cb5 100644
--- a/common/djangoapps/util/views.py
+++ b/common/djangoapps/util/views.py
@@ -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):
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 1ea51bd7f1..5efd7b4005 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -162,8 +162,7 @@ class CourseFields(object):
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
discussion_topics = Object(
help="Map of topics names to ids",
- scope=Scope.settings,
- computed_default=lambda c: {'General': {'id': c.location.html_id()}},
+ scope=Scope.settings
)
testcenter_info = Object(help="Dictionary of Test Center info", scope=Scope.settings)
announcement = Date(help="Date this course is announced", scope=Scope.settings)
@@ -234,6 +233,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
self._grading_policy = {}
self.set_grading_policy(self.grading_policy)
+ if self.discussion_topics == {}:
+ self.discussion_topics = {'General': {'id': self.location.html_id()}}
self.test_center_exams = []
test_center_info = self.testcenter_info
diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore
index bbd93c90e3..456e71bf8b 100644
--- a/common/lib/xmodule/xmodule/js/src/.gitignore
+++ b/common/lib/xmodule/xmodule/js/src/.gitignore
@@ -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
diff --git a/common/lib/xmodule/xmodule/js/src/capa/.gitignore b/common/lib/xmodule/xmodule/js/src/capa/.gitignore
new file mode 100644
index 0000000000..13b8deb002
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/capa/.gitignore
@@ -0,0 +1,2 @@
+!imageinput.js
+!schematic.js
diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore
new file mode 100644
index 0000000000..d4aa116a26
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore
@@ -0,0 +1 @@
+!*.js
diff --git a/common/lib/xmodule/xmodule/js/src/poll/.gitignore b/common/lib/xmodule/xmodule/js/src/poll/.gitignore
new file mode 100644
index 0000000000..d4aa116a26
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/poll/.gitignore
@@ -0,0 +1 @@
+!*.js
diff --git a/common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore b/common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore
new file mode 100644
index 0000000000..d4aa116a26
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore
@@ -0,0 +1 @@
+!*.js
diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore b/common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore
new file mode 100644
index 0000000000..c7a88ce092
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore
@@ -0,0 +1 @@
+!html5_video.js
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
index 7788e23980..31237af7b9 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
@@ -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))
diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py
index 15bab32c14..0d789964e9 100644
--- a/common/lib/xmodule/xmodule/tests/test_course_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_course_module.py
@@ -40,34 +40,20 @@ class DummySystem(ImportSystem):
)
-class IsNewCourseTestCase(unittest.TestCase):
- """Make sure the property is_new works on courses"""
+def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
+ """Get a dummy course"""
- def setUp(self):
- # Needed for test_is_newish
- datetime_patcher = patch.object(
- xmodule.course_module, 'datetime',
- Mock(wraps=datetime.datetime)
- )
- mocked_datetime = datetime_patcher.start()
- mocked_datetime.utcnow.return_value = time_to_datetime(NOW)
- self.addCleanup(datetime_patcher.stop)
+ system = DummySystem(load_error_modules=True)
- @staticmethod
- def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
- """Get a dummy course"""
+ def to_attrb(n, v):
+ return '' if v is None else '{0}="{1}"'.format(n, v).lower()
- system = DummySystem(load_error_modules=True)
+ is_new = to_attrb('is_new', is_new)
+ announcement = to_attrb('announcement', announcement)
+ advertised_start = to_attrb('advertised_start', advertised_start)
+ end = to_attrb('end', end)
- def to_attrb(n, v):
- return '' if v is None else '{0}="{1}"'.format(n, v).lower()
-
- is_new = to_attrb('is_new', is_new)
- announcement = to_attrb('announcement', announcement)
- advertised_start = to_attrb('advertised_start', advertised_start)
- end = to_attrb('end', end)
-
- start_xml = '''
+ start_xml = '''
+ Have a course-specific question? + + Post it on the course forums. + +
+Have a general question about edX? Check the FAQ.
+✕
+✕
++ Thanks for your feedback. We will read your message, and our + support team may contact you to respond or ask for further clarification. +
+ +✕
+