diff --git a/.gitignore b/.gitignore index 87a0778a6f..d01baf055a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ :2e# .AppleDouble database.sqlite +private-requirements.txt courseware/static/js/mathjax/* flushdb.sh build diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000000..93a8706d3e --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +mitx diff --git a/README b/README deleted file mode 100644 index 2ed50ba063..0000000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -See doc/ for documentation. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..ec17d7c9a4 --- /dev/null +++ b/README.md @@ -0,0 +1,148 @@ +This is edX, a platform for online course delivery. The project is primarily +written in [Python](http://python.org/), using the +[Django](https://www.djangoproject.com/) framework. We also use some +[Ruby](http://www.ruby-lang.org/) and some [NodeJS](http://nodejs.org/). + +Installation +============ +The installation process is a bit messy at the moment. Here's a high-level +overview of what you should do to get started. + +**TLDR:** There is a `create-dev-env.sh` script that will attempt to set all +of this up for you. If you're in a hurry, run that script. Otherwise, I suggest +that you understand what the script is doing, and why, by reading this document. + +Directory Hierarchy +------------------- +This code assumes that it is checked out in a directory that has three sibling +directories: `data` (used for XML course data), `db` (used to hold a +[sqlite](https://sqlite.org/) database), and `log` (used to hold logs). If you +clone the repository into a directory called `edx` inside of a directory +called `dev`, here's an example of how the directory hierarchy should look: + + * dev + \ + * data + * db + * log + * edx + \ + README.md + +Language Runtimes +----------------- +You'll need to be sure that you have Python 2.7, Ruby 1.9.3, and NodeJS +(latest stable) installed on your system. Some of these you can install +using your system's package manager: [homebrew](http://mxcl.github.io/homebrew/) +for Mac, [apt](http://wiki.debian.org/Apt) for Debian-based systems +(including Ubuntu), [rpm](http://www.rpm.org/) or [yum](http://yum.baseurl.org/) +for Red Hat based systems (including CentOS). + +If your system's package manager gives you the wrong version of a language +runtime, then you'll need to use a versioning tool to install the correct version. +Usually, you'll need to do this for Ruby: you can use +[`rbenv`](https://github.com/sstephenson/rbenv) or [`rvm`](https://rvm.io/), but +typically `rbenv` is simpler. For Python, you can use +[`pythonz`](http://saghul.github.io/pythonz/), +and for Node, you can use [`nvm`](https://github.com/creationix/nvm). + +Virtual Environments +-------------------- +Often, different projects will have conflicting dependencies: for example, two +projects depending on two different, incompatible versions of a library. Clearly, +you can't have both versions installed and used on your machine simultaneously. +Virtual environments were created to solve this problem: by installing libraries +into an isolated environment, only projects that live inside the environment +will be able to see and use those libraries. Got incompatible dependencies? Use +different virtual environments, and your problem is solved. + +Remember, each language has a different implementation. Python has +[`virtualenv`](http://www.virtualenv.org/), Ruby has +[`bundler`](http://gembundler.com/), and Node's virtual environment support +is built into [`npm`](https://npmjs.org/), its library management tool. +For each language, decide if you want to use a virtual environment, or if you +want to install all the language dependencies globally (and risk conflicts). +I suggest you start with installing things globally until and unless things +break; you can always switch over to a virtual environment later on. + +Language Packages +----------------- +The Python libraries we use are listed in `requirements.txt`. The Ruby libraries +we use are listed in `Gemfile`. The Node libraries we use are listed in +`packages.json`. Python has a library installer called +[`pip`](http://www.pip-installer.org/), Ruby has a library installer called +[`gem`](https://rubygems.org/) (or `bundle` if you're using a virtual +environment), and Node has a library installer called +[`npm`](https://npmjs.org/). +Once you've got your languages and virtual environments set up, install +the libraries like so: + + $ pip install -r pre-requirements.txt + $ pip install -r requirements.txt + $ bundle install + $ npm install + +Other Dependencies +------------------ +You'll also need to install [MongoDB](http://www.mongodb.org/), since our +application uses it in addition to sqlite. You can install it through your +system package manager, and I suggest that you configure it to start +automatically when you boot up your system, so that you never have to worry +about it again. For Mac, use +[`launchd`](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/launchd.8.html) +(running `brew info mongodb` will give you some commands you can copy-paste.) +For Linux, you can use [`upstart`](http://upstart.ubuntu.com/), `chkconfig`, +or any other process management tool. + +Configuring Your Project +------------------------ +We use [`rake`](http://rake.rubyforge.org/) to execute common tasks in our +project. The `rake` tasks are defined in the `rakefile`, or you can run `rake -T` +to view a summary. + +Before you run your project, you need to create a sqlite database, create +tables in that database, run database migrations, and populate templates for +CMS templates. Fortunately, `rake` will do all of this for you! Just run: + + $ rake django-admin[syncdb] + $ rake django-admin[migrate] + $ rake django-admin[update_templates] + +If you are running these commands using the [`zsh`](http://www.zsh.org/) shell, +zsh will assume that you are doing +[shell globbing](https://en.wikipedia.org/wiki/Glob_(programming)), search for +a file in your directory named `django-adminsyncdb` or `django-adminmigrate`, +and fail. To fix this, just surround the argument with quotation marks, so that +you're running `rake "django-admin[syncdb]"`. + +Run Your Project +---------------- +edX has two components: Studio, the course authoring system; and the LMS +(learning management system) used by students. These two systems communicate +through the MongoDB database, which stores course information. + +To run Studio, run: + + $ rake cms + +To run the LMS, run: + + $ rake lms[cms.dev] + +Studio runs on port 8001, while LMS runs on port 8000, so you can run both of +these commands simultaneously, using two different terminal windows. To view +Studio, visit `127.0.0.1:8001` in your web browser; to view the LMS, visit +`127.0.0.1:8000`. + +There's also an older version of the LMS that saves its information in XML files +in the `data` directory, instead of in Mongo. To run this older version, run: + +$ rake lms + +Further Documentation +===================== +Once you've got your project up and running, you can check out the `docs` +directory to see more documentation about how edX is structured. + + + diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py index 91f722a699..f7d1bbd8fe 100644 --- a/cms/djangoapps/contentstore/module_info_model.py +++ b/cms/djangoapps/contentstore/module_info_model.py @@ -75,11 +75,7 @@ def set_module_info(store, location, post_data): # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' for metadata_key, value in posted_metadata.items(): - # let's strip out any metadata fields from the postback which have been identified as system metadata - # and therefore should not be user-editable, so we should accept them back from the client - if metadata_key in module.system_metadata_fields: - del posted_metadata[metadata_key] - elif posted_metadata[metadata_key] is None: + if posted_metadata[metadata_key] is None: # remove both from passed in collection as well as the collection read in from the modulestore if metadata_key in module._model_data: del module._model_data[metadata_key] diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 153e37ed82..824d2119f1 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -676,11 +676,7 @@ def save_item(request): # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' for metadata_key, value in posted_metadata.items(): - # let's strip out any metadata fields from the postback which have been identified as system metadata - # and therefore should not be user-editable, so we should accept them back from the client - if metadata_key in existing_item.system_metadata_fields: - del posted_metadata[metadata_key] - elif posted_metadata[metadata_key] is None: + if posted_metadata[metadata_key] is None: # remove both from passed in collection as well as the collection read in from the modulestore if metadata_key in existing_item._model_data: del existing_item._model_data[metadata_key] @@ -1487,6 +1483,12 @@ def create_new_course(request): new_course = modulestore('direct').clone_item(template, dest_location) + # clone a default 'about' module as well + + about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview']) + dest_about_location = dest_location._replace(category='about', name='overview') + modulestore('direct').clone_item(about_template_location, dest_about_location) + if display_name is not None: new_course.display_name = display_name diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 4429e35692..708e79f0a3 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -14,13 +14,14 @@ class CourseMetadata(object): The objects have no predefined attrs but instead are obj encodings of the editable metadata. ''' - FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', - 'end', - 'enrollment_start', - 'enrollment_end', - 'tabs', - 'graceperiod', - 'checklists'] + FILTERED_LIST = ['xml_attributes', + 'start', + 'end', + 'enrollment_start', + 'enrollment_end', + 'tabs', + 'graceperiod', + 'checklists'] @classmethod def fetch(cls, course_location): diff --git a/cms/envs/common.py b/cms/envs/common.py index 680d19ca34..8effc773e0 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -206,6 +206,8 @@ PIPELINE_CSS = { }, } +# test_order: Determines the position of this chunk of javascript on +# the jasmine test page PIPELINE_JS = { 'main': { 'source_filenames': sorted( @@ -213,6 +215,7 @@ PIPELINE_JS = { rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js') ) + ['js/hesitate.js', 'js/base.js'], 'output_filename': 'js/cms-application.js', + 'test_order': 0 }, 'module-js': { 'source_filenames': ( @@ -220,11 +223,8 @@ PIPELINE_JS = { rooted_glob(COMMON_ROOT / 'static/', 'xmodule/modules/js/*.js') ), 'output_filename': 'js/cms-modules.js', + 'test_order': 1 }, - 'spec': { - 'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.js')), - 'output_filename': 'js/cms-spec.js' - } } PIPELINE_CSS_COMPRESSOR = None diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py index ac28f8fc9a..e046a6d37c 100644 --- a/cms/envs/jasmine.py +++ b/cms/envs/jasmine.py @@ -20,7 +20,7 @@ PIPELINE_JS['js-test-source'] = { 'source_filenames': sum([ pipeline_group['source_filenames'] for group_name, pipeline_group - in PIPELINE_JS.items() + in sorted(PIPELINE_JS.items(), key=lambda item: item[1].get('test_order', 1e100)) if group_name != 'spec' ], []), 'output_filename': 'js/cms-test-source.js' @@ -35,4 +35,10 @@ JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib') +# Remove the localization middleware class because it requires the test database +# to be sync'd and migrated in order to run the jasmine tests interactively +# with a browser +MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \ + if e != 'django.middleware.locale.LocaleMiddleware') + INSTALLED_APPS += ('django_jasmine', ) diff --git a/cms/envs/test.py b/cms/envs/test.py index 0c91999a74..63b5efc645 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -17,9 +17,6 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_ROOT = path('test_root') -# Makes the tests run much faster... -SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead - # Want static files in the same dir for running on jenkins. STATIC_ROOT = TEST_ROOT / "staticfiles" diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json index e7a66b5bc0..c2e1a8acf6 100644 --- a/cms/static/coffee/files.json +++ b/cms/static/coffee/files.json @@ -7,6 +7,7 @@ "js/vendor/jquery.cookie.js", "js/vendor/json2.js", "js/vendor/underscore-min.js", - "js/vendor/backbone-min.js" + "js/vendor/backbone-min.js", + "js/vendor/jquery.leanModal.min.js" ] } diff --git a/cms/static/coffee/spec/views/module_edit_spec.coffee b/cms/static/coffee/spec/views/module_edit_spec.coffee index 5e83ecb42d..baf9ee9c20 100644 --- a/cms/static/coffee/spec/views/module_edit_spec.coffee +++ b/cms/static/coffee/spec/views/module_edit_spec.coffee @@ -72,3 +72,14 @@ describe "CMS.Views.ModuleEdit", -> it "loads the .xmodule-display inside the module editor", -> expect(XModule.loadModule).toHaveBeenCalled() expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display')) + + describe "changedMetadata", -> + it "returns empty if no metadata loaded", -> + expect(@moduleEdit.changedMetadata()).toEqual({}) + + it "returns only changed values", -> + @moduleEdit.originalMetadata = {'foo', 'bar'} + spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''}) + @moduleEdit.loadEdit() + @moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'}) + expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'}) diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee index 3cb3b1703f..bf56807f66 100644 --- a/cms/static/coffee/src/views/module_edit.coffee +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -20,6 +20,7 @@ class CMS.Views.ModuleEdit extends Backbone.View loadEdit: -> if not @module @module = XModule.loadModule(@$el.find('.xmodule_edit')) + @originalMetadata = @metadata() metadata: -> # cdodge: package up metadata which is separated into a number of input fields @@ -35,6 +36,14 @@ class CMS.Views.ModuleEdit extends Backbone.View return _metadata + changedMetadata: -> + currentMetadata = @metadata() + changedMetadata = {} + for key of currentMetadata + if currentMetadata[key] != @originalMetadata[key] + changedMetadata[key] = currentMetadata[key] + return changedMetadata + cloneTemplate: (parent, template) -> $.post("/clone_item", { parent_location: parent @@ -60,7 +69,7 @@ class CMS.Views.ModuleEdit extends Backbone.View course: course_location_analytics id: _this.model.id - data.metadata = _.extend(data.metadata || {}, @metadata()) + data.metadata = _.extend(data.metadata || {}, @changedMetadata()) @hideModal() @model.save(data).done( => # # showToastMessage("Your changes have been saved.", null, 3) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 3a51d797ec..ad81963b0f 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -10,7 +10,7 @@ var $newComponentTypePicker; var $newComponentTemplatePickers; var $newComponentButton; -$(document).ready(function () { +$(document).ready(function() { $body = $('body'); $modal = $('.history-modal'); $modalCover = $('