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 = $('
diff --git a/cms/templates/widgets/metadata-edit.html b/cms/templates/widgets/metadata-edit.html
index 51fe400f88..aada438f38 100644
--- a/cms/templates/widgets/metadata-edit.html
+++ b/cms/templates/widgets/metadata-edit.html
@@ -1,5 +1,6 @@
<%
import hashlib
+ from xmodule.fields import StringyInteger, StringyFloat
hlskey = hashlib.md5(module.location.url()).hexdigest()
%>
@@ -7,17 +8,42 @@
% for field_name, field_value in editable_metadata_fields.items():
% if field_name == 'source_code':
- Edit High Level Source
+ % if field_value['explicitly_set'] is True:
+ Edit High Level Source
+ % endif
% else:
-
-
+
+
+ ## Change to True to see all the information being passed through.
+ % if False:
+
+
+
+
+
+
+ % if field_value['field'].values:
+
+ % for value in field_value['field'].values:
+
+ % endfor
+ % endif
+ % endif
% endif
% endfor
- % if 'source_code' in editable_metadata_fields:
- <%include file="source-edit.html" />
+ % if 'source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set']:
+ <%include file="source-edit.html" />
% endif
diff --git a/cms/templates/widgets/source-edit.html b/cms/templates/widgets/source-edit.html
index 883190d6b3..b7ee6c9db9 100644
--- a/cms/templates/widgets/source-edit.html
+++ b/cms/templates/widgets/source-edit.html
@@ -12,7 +12,7 @@
EPFL is one of the two Swiss Federal Institutes of Technology. With the status of a national school since 1969, the young engineering school has grown in many dimensions, to the extent of becoming one of the most famous European institutions of science and technology. It has three core missions: training, research and technology transfer.
-
EPFL is located in Lausanne in Switzerland, on the shores of the largest lake in Europe, Lake Geneva and at the foot of the Alps and Mont-Blanc. Its main campus brings together over 11,000 persons, students, researchers and staff in the same magical place. Because of its dynamism and rich student community, EPFL has been able to create a special spirit imbued with curiosity and simplicity. Daily interactions amongst students, researchers and entrepreneurs on campus give rise to new scientific, technological and architectural projects.
-
+
EPFL is the Swiss Federal Institute of Technology in Lausanne. The past decade has seen EPFL ascend to the very top of European institutions of science and technology: it is ranked #1 in Europe in the field of engineering by the Times Higher Education (based on publications and citations), Leiden Rankings, and the Academic Ranking of World Universities.
EPFL is a major force in entrepreneurship, with 2012 bringing in $100M in funding for ten EPFL startups. Both young spin-offs (like Typesafe and Pix4D) and companies that have long grown past the startup stage (like Logitech) actively transfer the results of EPFL's scientific innovation to industry.
+
%block>
${parent.body()}
diff --git a/lms/urls.py b/lms/urls.py
index 375335e3ea..b978e590f5 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -2,7 +2,6 @@ from django.conf import settings
from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.conf.urls.static import static
-from django.views.generic import RedirectView
from . import one_time_startup
@@ -10,10 +9,9 @@ import django.contrib.auth.views
# Uncomment the next two lines to enable the admin:
if settings.DEBUG:
- from django.contrib import admin
admin.autodiscover()
-urlpatterns = ('',
+urlpatterns = ('', # nopep8
# certificate view
url(r'^update_certificate$', 'certificates.views.update_certificate'),
@@ -116,8 +114,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'),
)
@@ -297,12 +296,12 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.news', name="news"),
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls'))
- )
+ )
urlpatterns += (
# This MUST be the last view in the courseware--it's a catch-all for custom tabs.
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/(?P[^/]+)/$',
'courseware.views.static_tab', name="static_tab"),
- )
+ )
if settings.MITX_FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'):
urlpatterns += (
@@ -344,13 +343,13 @@ if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
url(r'^migrate/reload/(?P[^/]+)/(?P[^/]+)$', 'lms_migration.migrate.manage_modulestores'),
url(r'^gitreload$', 'lms_migration.migrate.gitreload'),
url(r'^gitreload/(?P[^/]+)$', 'lms_migration.migrate.gitreload'),
- )
+ )
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
urlpatterns += (
url(r'^event_logs$', 'track.views.view_tracking_log'),
url(r'^event_logs/(?P.+)$', 'track.views.view_tracking_log'),
- )
+ )
# FoldIt views
urlpatterns += (
diff --git a/local-requirements.txt b/local-requirements.txt
index 177897f53d..201467d11e 100644
--- a/local-requirements.txt
+++ b/local-requirements.txt
@@ -2,8 +2,3 @@
-e common/lib/capa
-e common/lib/xmodule
-e .
-
-# XBlock:
-# Might change frequently, so put it in local-requirements.txt,
-# but conceptually is an external package, so it is in a separate repo.
--e git+https://github.com/edx/XBlock.git@96d8f5f4#egg=XBlock
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
diff --git a/rakefile b/rakefile
index 2b9cb9fd57..32d92a0349 100644
--- a/rakefile
+++ b/rakefile
@@ -174,6 +174,11 @@ end
desc "Install all python prerequisites for the lms and cms"
task :install_python_prereqs do
sh('pip install -r requirements.txt')
+ # Check for private-requirements.txt: used to install our libs as working dirs,
+ # or personal-use tools.
+ if File.file?("private-requirements.txt")
+ sh('pip install -r private-requirements.txt')
+ end
end
task :predjango do
@@ -301,6 +306,7 @@ end
desc "Open jasmine tests for #{system} in your default browser"
task "browse_jasmine_#{system}" do
+ compile_assets()
django_for_jasmine(system, true) do |jasmine_url|
Launchy.open(jasmine_url)
puts "Press ENTER to terminate".red
@@ -310,6 +316,7 @@ end
desc "Use phantomjs to run jasmine tests for #{system} from the console"
task "phantomjs_jasmine_#{system}" do
+ compile_assets()
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
django_for_jasmine(system, false) do |jasmine_url|
sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}")
diff --git a/test_root/db/.gitignore b/test_root/db/.gitignore
new file mode 100644
index 0000000000..98e6ef67fa
--- /dev/null
+++ b/test_root/db/.gitignore
@@ -0,0 +1 @@
+*.db