From 0f7378a171d746d2bf0416ce32a84fbc3d8ca746 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 Apr 2013 16:58:33 -0400 Subject: [PATCH 1/7] Modify UserFactory to create a profile for the user This allows specification of profile parameters when creating a user. Because the profile contents are always accessed from the database, the user must be saved to the database before the profile is created. This means that the profile cannot be created if the user is merely being built (and not saved) rather than created. --- common/djangoapps/student/tests/factories.py | 12 +++++++++++- lms/djangoapps/instructor/tests/test_gradebook.py | 3 +-- 2 files changed, 12 insertions(+), 3 deletions(-) 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/lms/djangoapps/instructor/tests/test_gradebook.py b/lms/djangoapps/instructor/tests/test_gradebook.py index 2de5c18bcd..4b1d22b594 100644 --- a/lms/djangoapps/instructor/tests/test_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_gradebook.py @@ -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')) \ No newline at end of file + self.assertEquals(3, self.response.content.count('grade_None')) From 8ef88fa5b0b64748286bbe3dfb802eb629af82d6 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 6 May 2013 10:24:38 -0400 Subject: [PATCH 2/7] Fix tests that randomly fail when run in concurrent jobs on jenkins. --- lms/djangoapps/courseware/tests/tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 4c9f592797..d5064ec5e5 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -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', } From 87072a9a58db8f42761392c3f26c536998cde4d7 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 Apr 2013 17:03:10 -0400 Subject: [PATCH 3/7] Add an endpoint for submission of Zendesk tickets by end users This functionality requires the Zendesk URL, user, and API key to be specified in django.conf.settings. Also, add a flag to MITX_FEATURES (enabled by default) to control the endpoint and the front-end feature (yet to be added). --- common/djangoapps/util/tests.py | 284 ++++++++++++++++++++++++++++++-- common/djangoapps/util/views.py | 167 ++++++++++++++++--- github-requirements.txt | 1 + lms/envs/aws.py | 5 + lms/envs/common.py | 14 +- lms/urls.py | 3 +- 6 files changed, 435 insertions(+), 39 deletions(-) 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/github-requirements.txt b/github-requirements.txt index 3b71d228e7..048f3cee68 100644 --- a/github-requirements.txt +++ b/github-requirements.txt @@ -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@5ce6f70a#egg=XBlock diff --git a/lms/envs/aws.py b/lms/envs/aws.py index aa30315eca..70e75f1f0d 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -80,6 +80,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. @@ -115,3 +117,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") diff --git a/lms/envs/common.py b/lms/envs/common.py index 32a213f06e..b804ae2a7a 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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', ) + diff --git a/lms/urls.py b/lms/urls.py index 082004c1be..7458d49025 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -116,8 +116,9 @@ urlpatterns = ('', # 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[^/]*)$', 'student.views.reactivation_email'), ) From 00729a8c13342654ed510ad5a09d056cd49d45e2 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 24 Apr 2013 12:06:48 -0400 Subject: [PATCH 4/7] Add an omnipresent help tab to the LMS The help tab opens a modal dialog that directs the user at various resources (e.g. the site FAQ and course forums) and allows the user to submit feedback to the feedback endpoint (which will ultimately create a ticket for the student support team). --- .../xmodule/modulestore/tests/factories.py | 16 +- lms/djangoapps/courseware/tabs.py | 33 +++- lms/djangoapps/courseware/tests/test_tabs.py | 63 +++++++ lms/static/sass/base/_base.scss | 57 ++++++ lms/templates/help_modal.html | 167 ++++++++++++++++++ lms/templates/navigation.html | 2 + 6 files changed, 324 insertions(+), 14 deletions(-) create mode 100644 lms/templates/help_modal.html 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/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 9f9a4e3e96..ea6f2fc556 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -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)) diff --git a/lms/djangoapps/courseware/tests/test_tabs.py b/lms/djangoapps/courseware/tests/test_tabs.py index 928b9ae0df..04c46a7820 100644 --- a/lms/djangoapps/courseware/tests/test_tabs.py +++ b/lms/djangoapps/courseware/tests/test_tabs.py @@ -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) diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss index ca56f542d6..d2d4a0564f 100644 --- a/lms/static/sass/base/_base.scss +++ b/lms/static/sass/base/_base.scss @@ -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; +} diff --git a/lms/templates/help_modal.html b/lms/templates/help_modal.html new file mode 100644 index 0000000000..83ea00068f --- /dev/null +++ b/lms/templates/help_modal.html @@ -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): + +
+ Help +
+ + + + + +%endif diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index e4c23e4836..4bb99d1ebd 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -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"/> From 203a958e68299831cd9fe23f85643530575b32ec Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 18 Apr 2013 16:07:24 -0400 Subject: [PATCH 5/7] Outline textareas in red on a form submission error This was previously done for input but not textarea. --- lms/static/sass/shared/_modal.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/sass/shared/_modal.scss b/lms/static/sass/shared/_modal.scss index bfa803fee2..2da64d54a6 100644 --- a/lms/static/sass/shared/_modal.scss +++ b/lms/static/sass/shared/_modal.scss @@ -155,7 +155,7 @@ display: block; color: #8F0E0E; - + input { + + input, + textarea { border: 1px solid #CA1111; color: #8F0E0E; } From 522751e425af15a1d3b75537ec32c6a1df229881 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 6 May 2013 14:28:05 -0400 Subject: [PATCH 6/7] Ignore the js files that are compiled from coffeescript when running the xmodule jasmine tests --- common/lib/xmodule/xmodule/js/src/.gitignore | 5 ++++- common/lib/xmodule/xmodule/js/src/annotatable/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/capa/.gitignore | 1 + .../lib/xmodule/xmodule/js/src/combinedopenended/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/conditional/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/discussion/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/html/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/peergrading/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/problem/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/raw/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/sequence/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/vertical/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/video/.gitignore | 2 ++ common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore | 2 ++ common/lib/xmodule/xmodule/js/src/wrapper/.gitignore | 1 + 15 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 common/lib/xmodule/xmodule/js/src/annotatable/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/capa/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/conditional/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/discussion/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/html/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/peergrading/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/problem/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/raw/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/sequence/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/vertical/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/video/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/wrapper/.gitignore diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore index bbd93c90e3..c2d956ce35 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 cover any .coffee -> .js files that get compiled. +*.js diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/.gitignore b/common/lib/xmodule/xmodule/js/src/annotatable/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/annotatable/.gitignore @@ -0,0 +1 @@ +*.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..77fdb1cbe9 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/capa/.gitignore @@ -0,0 +1 @@ +display.js diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore b/common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/common/lib/xmodule/xmodule/js/src/conditional/.gitignore b/common/lib/xmodule/xmodule/js/src/conditional/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/conditional/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/common/lib/xmodule/xmodule/js/src/discussion/.gitignore b/common/lib/xmodule/xmodule/js/src/discussion/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/discussion/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/common/lib/xmodule/xmodule/js/src/html/.gitignore b/common/lib/xmodule/xmodule/js/src/html/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/html/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/.gitignore b/common/lib/xmodule/xmodule/js/src/peergrading/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/peergrading/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/common/lib/xmodule/xmodule/js/src/problem/.gitignore b/common/lib/xmodule/xmodule/js/src/problem/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/problem/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/common/lib/xmodule/xmodule/js/src/raw/.gitignore b/common/lib/xmodule/xmodule/js/src/raw/.gitignore new file mode 100644 index 0000000000..7cc629ca26 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/raw/.gitignore @@ -0,0 +1 @@ +edit/*.js diff --git a/common/lib/xmodule/xmodule/js/src/sequence/.gitignore b/common/lib/xmodule/xmodule/js/src/sequence/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/sequence/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/common/lib/xmodule/xmodule/js/src/vertical/.gitignore b/common/lib/xmodule/xmodule/js/src/vertical/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/vertical/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/common/lib/xmodule/xmodule/js/src/video/.gitignore b/common/lib/xmodule/xmodule/js/src/video/.gitignore new file mode 100644 index 0000000000..39c7b67ac1 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/.gitignore @@ -0,0 +1,2 @@ +*.js +display/*.js diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore b/common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore new file mode 100644 index 0000000000..39c7b67ac1 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore @@ -0,0 +1,2 @@ +*.js +display/*.js diff --git a/common/lib/xmodule/xmodule/js/src/wrapper/.gitignore b/common/lib/xmodule/xmodule/js/src/wrapper/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/wrapper/.gitignore @@ -0,0 +1 @@ +*.js From c2cd75469b94fb5eadf3b151036177dfc1173487 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 6 May 2013 16:48:37 -0400 Subject: [PATCH 7/7] Change the methodology to ignore .js files by default. Any .js files that are coded can be handled individually. --- common/lib/xmodule/xmodule/js/src/.gitignore | 2 +- common/lib/xmodule/xmodule/js/src/annotatable/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/capa/.gitignore | 3 ++- common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/conditional/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/discussion/.gitignore | 1 - .../xmodule/xmodule/js/src/graphical_slider_tool/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/html/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/peergrading/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/poll/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/problem/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/raw/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/sequence/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/vertical/.gitignore | 1 - common/lib/xmodule/xmodule/js/src/video/.gitignore | 2 -- common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore | 2 -- .../lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore | 1 + common/lib/xmodule/xmodule/js/src/wrapper/.gitignore | 1 - 19 files changed, 7 insertions(+), 17 deletions(-) delete mode 100644 common/lib/xmodule/xmodule/js/src/annotatable/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/conditional/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/discussion/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/html/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/peergrading/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/poll/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/problem/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/raw/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/sequence/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/sequence/display/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/vertical/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/video/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore create mode 100644 common/lib/xmodule/xmodule/js/src/videoalpha/display/.gitignore delete mode 100644 common/lib/xmodule/xmodule/js/src/wrapper/.gitignore diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore index c2d956ce35..456e71bf8b 100644 --- a/common/lib/xmodule/xmodule/js/src/.gitignore +++ b/common/lib/xmodule/xmodule/js/src/.gitignore @@ -1,4 +1,4 @@ # Ignore .js files in this folder as they are compiled from coffeescript # For each of the xmodules subdirectories, add a .gitignore file that -# will cover any .coffee -> .js files that get compiled. +# will version any *.js file that is specifically written, not compiled. *.js diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/.gitignore b/common/lib/xmodule/xmodule/js/src/annotatable/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/annotatable/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/common/lib/xmodule/xmodule/js/src/capa/.gitignore b/common/lib/xmodule/xmodule/js/src/capa/.gitignore index 77fdb1cbe9..13b8deb002 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/.gitignore +++ b/common/lib/xmodule/xmodule/js/src/capa/.gitignore @@ -1 +1,2 @@ -display.js +!imageinput.js +!schematic.js diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore b/common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/common/lib/xmodule/xmodule/js/src/conditional/.gitignore b/common/lib/xmodule/xmodule/js/src/conditional/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/conditional/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/common/lib/xmodule/xmodule/js/src/discussion/.gitignore b/common/lib/xmodule/xmodule/js/src/discussion/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/discussion/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.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/html/.gitignore b/common/lib/xmodule/xmodule/js/src/html/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/html/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/.gitignore b/common/lib/xmodule/xmodule/js/src/peergrading/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/peergrading/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.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/problem/.gitignore b/common/lib/xmodule/xmodule/js/src/problem/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/problem/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/common/lib/xmodule/xmodule/js/src/raw/.gitignore b/common/lib/xmodule/xmodule/js/src/raw/.gitignore deleted file mode 100644 index 7cc629ca26..0000000000 --- a/common/lib/xmodule/xmodule/js/src/raw/.gitignore +++ /dev/null @@ -1 +0,0 @@ -edit/*.js diff --git a/common/lib/xmodule/xmodule/js/src/sequence/.gitignore b/common/lib/xmodule/xmodule/js/src/sequence/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/sequence/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.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/vertical/.gitignore b/common/lib/xmodule/xmodule/js/src/vertical/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/vertical/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/common/lib/xmodule/xmodule/js/src/video/.gitignore b/common/lib/xmodule/xmodule/js/src/video/.gitignore deleted file mode 100644 index 39c7b67ac1..0000000000 --- a/common/lib/xmodule/xmodule/js/src/video/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.js -display/*.js diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore b/common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore deleted file mode 100644 index 39c7b67ac1..0000000000 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.js -display/*.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/js/src/wrapper/.gitignore b/common/lib/xmodule/xmodule/js/src/wrapper/.gitignore deleted file mode 100644 index a6c7c2852d..0000000000 --- a/common/lib/xmodule/xmodule/js/src/wrapper/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js