From 0f7378a171d746d2bf0416ce32a84fbc3d8ca746 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 Apr 2013 16:58:33 -0400 Subject: [PATCH 01/26] 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 32d67be5f53d864966d50821e1e2bdea413fc644 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 3 May 2013 16:42:41 -0400 Subject: [PATCH 02/26] Get rid of _computed_default. --- common/lib/xmodule/xmodule/course_module.py | 4 ++-- common/lib/xmodule/xmodule/tests/test_course_module.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 1ea51bd7f1..9b37afd1fe 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,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): self._grading_policy = {} self.set_grading_policy(self.grading_policy) + CourseFields.discussion_topics._default = {'General': {'id': self.location.html_id()}} self.test_center_exams = [] test_center_info = self.testcenter_info diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 15bab32c14..34428d6883 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -171,3 +171,7 @@ class IsNewCourseTestCase(unittest.TestCase): d = self.get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00') self.assertEqual('Sep 04, 2014', d.end_date_text) + + def test_default_discussion_topics(self): + d = self.get_dummy_course('2012-12-02T12:00') + self.assertEqual({'General': {'id': 'i4x-test_org-test_course-course-test'}}, d.discussion_topics) From e44c6b6bf8c89d03347d47fc31297871a5b7bb51 Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 3 May 2013 17:05:02 -0400 Subject: [PATCH 03/26] Don't reach in to default value. --- common/lib/xmodule/xmodule/course_module.py | 3 +- .../xmodule/tests/test_course_module.py | 80 ++++++++++--------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 9b37afd1fe..5efd7b4005 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -233,7 +233,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): self._grading_policy = {} self.set_grading_policy(self.grading_policy) - CourseFields.discussion_topics._default = {'General': {'id': self.location.html_id()}} + 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/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 34428d6883..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 = ''' '''.format(org=ORG, course=COURSE, start=start, is_new=is_new, - announcement=announcement, advertised_start=advertised_start, end=end) + announcement=announcement, advertised_start=advertised_start, end=end) - return system.process_xml(start_xml) + return system.process_xml(start_xml) + + +class IsNewCourseTestCase(unittest.TestCase): + """Make sure the property is_new works on courses""" + + 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) @patch('xmodule.course_module.time.gmtime') def test_sorting_score(self, gmtime_mock): @@ -120,8 +120,8 @@ class IsNewCourseTestCase(unittest.TestCase): ] for a, b, assertion in dates: - a_score = self.get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score - b_score = self.get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score + a_score = get_dummy_course(start=a[0], announcement=a[1], advertised_start=a[2]).sorting_score + b_score = get_dummy_course(start=b[0], announcement=b[1], advertised_start=b[2]).sorting_score print "Comparing %s to %s" % (a, b) assertion(a_score, b_score) @@ -138,40 +138,42 @@ class IsNewCourseTestCase(unittest.TestCase): ] for s in settings: - d = self.get_dummy_course(start=s[0], advertised_start=s[1]) + d = get_dummy_course(start=s[0], advertised_start=s[1]) print "Checking start=%s advertised=%s" % (s[0], s[1]) self.assertEqual(d.start_date_text, s[2]) def test_is_newish(self): - descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True) + descriptor = get_dummy_course(start='2012-12-02T12:00', is_new=True) assert(descriptor.is_newish is True) - descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False) + descriptor = get_dummy_course(start='2013-02-02T12:00', is_new=False) assert(descriptor.is_newish is False) - descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True) + descriptor = get_dummy_course(start='2013-02-02T12:00', is_new=True) assert(descriptor.is_newish is True) - descriptor = self.get_dummy_course(start='2013-01-15T12:00') + descriptor = get_dummy_course(start='2013-01-15T12:00') assert(descriptor.is_newish is True) - descriptor = self.get_dummy_course(start='2013-03-01T12:00') + descriptor = get_dummy_course(start='2013-03-01T12:00') assert(descriptor.is_newish is True) - descriptor = self.get_dummy_course(start='2012-10-15T12:00') + descriptor = get_dummy_course(start='2012-10-15T12:00') assert(descriptor.is_newish is False) - descriptor = self.get_dummy_course(start='2012-12-31T12:00') + descriptor = get_dummy_course(start='2012-12-31T12:00') assert(descriptor.is_newish is True) def test_end_date_text(self): # No end date set, returns empty string. - d = self.get_dummy_course('2012-12-02T12:00') + d = get_dummy_course('2012-12-02T12:00') self.assertEqual('', d.end_date_text) - d = self.get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00') + d = get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00') self.assertEqual('Sep 04, 2014', d.end_date_text) + +class DiscussionTopicsTestCase(unittest.TestCase): def test_default_discussion_topics(self): - d = self.get_dummy_course('2012-12-02T12:00') + d = get_dummy_course('2012-12-02T12:00') self.assertEqual({'General': {'id': 'i4x-test_org-test_course-course-test'}}, d.discussion_topics) From 8ef88fa5b0b64748286bbe3dfb802eb629af82d6 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 6 May 2013 10:24:38 -0400 Subject: [PATCH 04/26] 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 457b678af714dd50ffe56e1efdf95101aa1409ae Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 2 May 2013 13:32:25 -0400 Subject: [PATCH 05/26] Wrote a proper README file So that no one else will have to go through what I went through. Hopefully. --- README | 1 - README.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) delete mode 100644 README create mode 100644 README.md 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..1642539c6b --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +This is edX, a platform for online higher education. 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 application 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. Incompatible dependencies? Use +different virtual environments, and your problem is solved. + +Once again, each language has a different implementation. Python has +[virtualenv](http://www.virtualenv.org/), Ruby has +[`bundler`](http://gembundler.com/), and Node has +[`nave`](https://github.com/isaacs/nave). 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 requirements.txt + $ bundle install + $ npm install + +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 and create +tables in that database. Fortunately, `rake` will do it for you! Just run: + + $ rake django-admin[syncdb] + $ rake django-admin[migrate] + +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 +---------------- +To *finally* get up and running, just run: + + $ rake cms + +And `rake` will start up your Django project on the localhost, port 8001. To +view your running project, type `127.0.0.1:8001` into your web browser, and +you should see edX in all its glory! + + +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. + + + From a58f39695aa55bf80012f9e8a920c9724d456b04 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 2 May 2013 13:50:22 -0400 Subject: [PATCH 06/26] Remove install.txt, because all that information is in the README.md file now --- install.txt | 74 ----------------------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 install.txt diff --git a/install.txt b/install.txt deleted file mode 100644 index 801036af6b..0000000000 --- a/install.txt +++ /dev/null @@ -1,74 +0,0 @@ -This document describes how to set up the MITx development environment -for both Linux (Ubuntu) and MacOS (OSX Lion). - -There is also a script "create-dev-env.sh" that automates these steps. - -1) Make an mitx_all directory and clone the repos - (download and install git and mercurial if you don't have them already) - - mkdir ~/mitx_all - cd ~/mitx_all - git clone git@github.com:MITx/mitx.git - hg clone ssh://hg-content@gp.mitx.mit.edu/data - -2) Install OSX dependencies (Mac users only) - - a) Install the brew utility if necessary - /usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)" - - b) Install the brew package list - cat ~/mitx_all/mitx/brew-formulas.txt | xargs brew install - - c) Install python pip if necessary - sudo easy_install pip - - d) Install python virtualenv if necessary - sudo pip install virtualenv virtualenvwrapper - - e) Install coffee script - curl http://npmjs.org/install.sh | sh - npm install -g coffee-script - -3) Install Ubuntu dependencies (Linux users only) - - sudo apt-get install curl python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript - - -4) Install rvm, ruby, and libraries - - echo "export rvm_path=$HOME/mitx_all/ruby" > $HOME/.rvmrc - curl -sL get.rvm.io | bash -s stable - source ~/mitx_all/ruby/scripts/rvm - rvm install 1.9.3 - gem install bundler - cd ~/mitx_all/mitx - bundle install - -5) Install python libraries - - source ~/mitx_all/python/bin/activate - cd ~/mitx_all - pip install -r mitx/pre-requirements.txt - pip install -r mitx/requirements.txt - -6) Create log and db dirs - - mkdir ~/mitx_all/log - mkdir ~/mitx_all/db - -7) Start the dev server - - To start using Django you will need - to activate the local Python and Ruby - environment: - - $ source ~/mitx_all/ruby/scripts/rvm - $ source ~/mitx_all/python/bin/activate - - To initialize and start a local instance of Django: - - $ cd ~/mitx_all/mitx - $ django-admin.py syncdb --settings=envs.dev --pythonpath=. - $ django-admin.py migrate --settings=envs.dev --pythonpath=. - $ django-admin.py runserver --settings=envs.dev --pythonpath=. - From a0727ac2261ce5e557a22ce504b20e64abdf1ede Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 2 May 2013 13:53:42 -0400 Subject: [PATCH 07/26] Virtualenv is a tool, and should be surrounded by backticks --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1642539c6b..db839cfbfb 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ will be able to see and use those libraries. Incompatible dependencies? Use different virtual environments, and your problem is solved. Once again, each language has a different implementation. Python has -[virtualenv](http://www.virtualenv.org/), Ruby has +[`virtualenv`](http://www.virtualenv.org/), Ruby has [`bundler`](http://gembundler.com/), and Node has [`nave`](https://github.com/isaacs/nave). For each language, decide if you want to use a virtual environment, or if you want to install all the From 1bda218e17d2ca6d7212f947e7544c85cce9535c Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 2 May 2013 16:27:19 -0400 Subject: [PATCH 08/26] Clarify purpose of `data` dir --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db839cfbfb..498277d646 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 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 application data?), `db` (used to hold a +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: From fccee7a1e320be73b8506d71f5a507bc83ce60e4 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 2 May 2013 16:29:26 -0400 Subject: [PATCH 09/26] Node does virtual environments using npm --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 498277d646..7d8a61e7b4 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,12 @@ different virtual environments, and your problem is solved. Once again, each language has a different implementation. Python has [`virtualenv`](http://www.virtualenv.org/), Ruby has -[`bundler`](http://gembundler.com/), and Node has -[`nave`](https://github.com/isaacs/nave). 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. +[`bundler`](http://gembundler.com/), and Node's virtual environment support +is built into [`npm`](https://npmjs.org/), it's 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 ----------------- From 06ade12f6a782e5cb0ca2f47d4f1fde16554b1c6 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 2 May 2013 16:42:51 -0400 Subject: [PATCH 10/26] Also need to install Mongo --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 7d8a61e7b4..05f1eff406 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,18 @@ the libraries like so: $ 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 From 03470fc48e09eef7b2ba7ba72013fd6799114705 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 2 May 2013 09:43:18 -0400 Subject: [PATCH 11/26] Add a mention of create-dev-env.sh --- doc/development.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/development.md b/doc/development.md index 95cc32329c..a6a1de4ef7 100644 --- a/doc/development.md +++ b/doc/development.md @@ -31,6 +31,14 @@ Check out the course data directories that you want to work with into the rake resetdb +## Installing + +To create your development environment, run the shell script in the root of +the repo: + + create-dev-env.sh + + ## Starting development servers Both the LMS and Studio can be started using the following shortcut tasks From 98243b2b53f77aca569e50b8d7936f4ae01148d5 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 3 May 2013 10:55:28 -0400 Subject: [PATCH 12/26] Document django-admin[update_templates] step --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05f1eff406..2c4d76f8fd 100644 --- a/README.md +++ b/README.md @@ -99,11 +99,13 @@ 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 and create -tables in that database. Fortunately, `rake` will do it for you! Just run: +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 From fa87cccb9ba7cf99fda9a9785a52b18d866efc95 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 3 May 2013 11:16:05 -0400 Subject: [PATCH 13/26] Document old-style `rake lms` command --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 2c4d76f8fd..9f5d7ba1c1 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,13 @@ And `rake` will start up your Django project on the localhost, port 8001. To view your running project, type `127.0.0.1:8001` into your web browser, and you should see edX in all its glory! +If you need to run old XML-only LMS (which doesn't use the database), run this +instead: + + $ rake lms + +And `rake` will start up the old project on localhost port 8000. + Further Documentation ===================== From 2a09f6bac10fa3bb04479dbb0c5429b2b82d5dd8 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 3 May 2013 11:28:05 -0400 Subject: [PATCH 14/26] Clarified based on @shnayder's comment --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f5d7ba1c1..80e534730d 100644 --- a/README.md +++ b/README.md @@ -124,8 +124,8 @@ And `rake` will start up your Django project on the localhost, port 8001. To view your running project, type `127.0.0.1:8001` into your web browser, and you should see edX in all its glory! -If you need to run old XML-only LMS (which doesn't use the database), run this -instead: +If you need to run old XML-only LMS (which doesn't use the mongo database for +course content), run this instead: $ rake lms From ee6d68e2e0ecc701f3f97c2110cc671c67c58ed3 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 3 May 2013 14:41:58 -0400 Subject: [PATCH 15/26] grammar --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80e534730d..5ac5f91632 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ different virtual environments, and your problem is solved. Once again, 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/), it's library management tool. +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 From 234ca0e07616490a0b7f5bc565036abebe4cf2e4 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 3 May 2013 14:51:56 -0400 Subject: [PATCH 16/26] Responding to @jzoldak's comments --- README.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5ac5f91632..f1532d53b6 100644 --- a/README.md +++ b/README.md @@ -116,21 +116,27 @@ you're running `rake "django-admin[syncdb]"`. Run Your Project ---------------- -To *finally* get up and running, just run: +edX has two components: Studio, the course authoring system; and the LMS +(leaning management system) used by students. These two systems communicate +through the MongoDB database, which stores course information. + +To run Studio, run: $ rake cms -And `rake` will start up your Django project on the localhost, port 8001. To -view your running project, type `127.0.0.1:8001` into your web browser, and -you should see edX in all its glory! +To run the LMS, run: -If you need to run old XML-only LMS (which doesn't use the mongo database for -course content), run this instead: + $ rake lms[cms.dev] - $ rake lms +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`. -And `rake` will start up the old project on localhost port 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 ===================== From 7b3646b39c798dc6642bcfae61b2c35b9d13a236 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 6 May 2013 11:05:06 -0400 Subject: [PATCH 17/26] Need to install `pre-requirements.txt` first --- README.md | 3 ++- pre-requirements.txt | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f1532d53b6..76dbe4b150 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ This is edX, a platform for online higher education. The project is primarily written in [Python](http://python.org/), using the -[Django](https://www.djangoproject.com/) framework. We also use some +[Django](https://www.djangoproject.com/) framework. We also use some [Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/). Installation @@ -77,6 +77,7 @@ environment), and Node has a library installer called 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 diff --git a/pre-requirements.txt b/pre-requirements.txt index 7ecead0ce7..d39199a741 100644 --- a/pre-requirements.txt +++ b/pre-requirements.txt @@ -1,2 +1,10 @@ +# We use `scipy` in our project, which relies on `numpy`. `pip` apparently +# installs packages in a two-step process, where it will first try to build +# all packages, and then try to install all packages. As a result, if we simply +# added these packages to the top of `requirements.txt`, `pip` would try to +# build `scipy` before `numpy` has been installed, and it would fail. By +# separating this out into a `pre-requirements.txt` file, we can make sure +# that `numpy` is built *and* installed before we try to build `scipy`. + numpy==1.6.2 distribute>=0.6.28 From 683906cdaf050ee94fa1e0b3972d6853d1dff8b5 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 6 May 2013 11:17:28 -0400 Subject: [PATCH 18/26] "One again" -> "Remember" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76dbe4b150..cd6a95cb0a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ into an isolated environment, only projects that live inside the environment will be able to see and use those libraries. Incompatible dependencies? Use different virtual environments, and your problem is solved. -Once again, each language has a different implementation. Python has +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. From b99584287484bf2c8e21ca064388282438d509df Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 6 May 2013 11:21:05 -0400 Subject: [PATCH 19/26] LMS is not a leaning management system That would be a chair. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cd6a95cb0a..27494fe6fa 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ you're running `rake "django-admin[syncdb]"`. Run Your Project ---------------- edX has two components: Studio, the course authoring system; and the LMS -(leaning management system) used by students. These two systems communicate +(learning management system) used by students. These two systems communicate through the MongoDB database, which stores course information. To run Studio, run: From 58bc0452eba5dc60abd69b63011b472c94cf6195 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 6 May 2013 11:25:52 -0400 Subject: [PATCH 20/26] Describe edX better --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 27494fe6fa..4ffbaf9642 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -This is edX, a platform for online higher education. The project is primarily +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/). From a779f6271692c1b68ea602fb972e1bc3aff065b8 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 6 May 2013 11:27:06 -0400 Subject: [PATCH 21/26] Make rhetorical question a bit clearer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ffbaf9642..ec17d7c9a4 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ 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. Incompatible dependencies? Use +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 From 87072a9a58db8f42761392c3f26c536998cde4d7 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 Apr 2013 17:03:10 -0400 Subject: [PATCH 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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