From 981f5cee45507bff72f85215696de74db5937359 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 8 Nov 2012 16:07:17 -0500 Subject: [PATCH 01/59] initial buildout of a 'xlint' test to verify legacy coursewar --- .../contentstore/management/commands/xlint.py | 26 +++++++++++ .../xmodule/modulestore/xml_importer.py | 43 +++++++++++++++++-- rakefile | 12 ++++++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 cms/djangoapps/contentstore/management/commands/xlint.py diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py new file mode 100644 index 0000000000..355b639f2d --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/xlint.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand, CommandError +from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.django import modulestore +from xmodule.contentstore.django import contentstore + + +unnamed_modules = 0 + + +class Command(BaseCommand): + help = \ +'''Verify the structure of courseware as to it's suitability for import''' + + def handle(self, *args, **options): + if len(args) == 0: + raise CommandError("import requires at least one argument: [...]") + + data_dir = args[0] + if len(args) > 1: + course_dirs = args[1:] + else: + course_dirs = None + print "Importing. Data_dir={data}, course_dirs={courses}".format( + data=data_dir, + courses=course_dirs) + import_from_xml(None, data_dir, course_dirs, load_error_modules=False, validate_only=True) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 00ddb6a948..4a3526b53e 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -88,7 +88,7 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic def import_from_xml(store, data_dir, course_dirs=None, default_class='xmodule.raw_module.RawDescriptor', - load_error_modules=True, static_content_store=None, target_location_namespace = None): + load_error_modules=True, static_content_store=None, target_location_namespace=None, validate_only=False): """ Import the specified xml data_dir into the "store" modulestore, using org and course as the location org and course. @@ -110,6 +110,10 @@ def import_from_xml(store, data_dir, course_dirs=None, load_error_modules=load_error_modules ) + if validate_only: + validate_module_structure(module_store) + return module_store, [] + # NOTE: the XmlModuleStore does not implement get_items() which would be a preferable means # to enumerate the entire collection of course modules. It will be left as a TBD to implement that # method on XmlModuleStore. @@ -192,7 +196,6 @@ def import_from_xml(store, data_dir, course_dirs=None, store.update_item(module.location, module_data) - if 'children' in module.definition: store.update_children(module.location, module.definition['children']) @@ -200,6 +203,38 @@ def import_from_xml(store, data_dir, course_dirs=None, # inherited metadata everywhere. store.update_metadata(module.location, dict(module.own_metadata)) - - return module_store, course_items + + +def validate_category_hierarcy(module_store, course_id, parent_category, expected_child_category): + err_cnt = 0 + + parents = [] + # get all modules of parent_category + for module in module_store.modules[course_id].itervalues(): + if module.location.category == parent_category: + parents.append(module) + + for parent in parents: + for child_loc in [Location(child) for child in parent.definition.get('children', [])]: + if child_loc.category != expected_child_category: + err_cnt += 1 + print 'ERROR: child {0} of parent {1} was expected to be category of {2} but was {3}'.format( + child_loc, parent.location, expected_child_category, child_loc.category) + + return err_cnt + +def validate_module_structure(module_store): + err_cnt = 0 + warn_cnt = 0 + for course_id in module_store.modules.keys(): + # constrain that courses only have 'chapter' children + err_cnt += validate_category_hierarcy(module_store, course_id, "course", "chapter") + # constrain that chapters only have 'sequentials' + err_cnt += validate_category_hierarcy(module_store, course_id, "chapter", "sequential") + # constrain that sequentials only have 'verticals' + err_cnt += validate_category_hierarcy(module_store, course_id, "sequential", "vertical") + + print "SUMMARY: {0} Errors {1} Warnings".format(err_cnt, warn_cnt) + + diff --git a/rakefile b/rakefile index 4f1c15321f..beb787c8c3 100644 --- a/rakefile +++ b/rakefile @@ -364,6 +364,18 @@ namespace :cms do end end +namespace :cms do + desc "Import course data within the given DATA_DIR variable" + task :xlint do + if ENV['DATA_DIR'] + sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'])) + else + raise "Please specify a DATA_DIR variable that point to your data directory.\n" + + "Example: \`rake cms:import DATA_DIR=../data\`" + end + end +end + desc "Build a properties file used to trigger autodeploy builds" task :autodeploy_properties do File.open("autodeploy.properties", "w") do |file| From da3c3e5f20589ea8f8d16471f2cc358c5d6c3d78 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 9 Nov 2012 13:54:12 -0500 Subject: [PATCH 02/59] initial xlint implementation. Accumulate all import errors during XmlModuleStore importing. Also do checks post XmlModuleStore import and assert that the structure (course->chapter->sequential->vertical) is present in the courses. --- .../contentstore/management/commands/xlint.py | 6 ++-- common/lib/xmodule/xmodule/modulestore/xml.py | 7 ++-- .../xmodule/modulestore/xml_importer.py | 32 ++++++++++++++++++- common/lib/xmodule/xmodule/seq_module.py | 4 ++- rakefile | 4 ++- 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py index 355b639f2d..e8f7b248e4 100644 --- a/cms/djangoapps/contentstore/management/commands/xlint.py +++ b/cms/djangoapps/contentstore/management/commands/xlint.py @@ -9,8 +9,10 @@ unnamed_modules = 0 class Command(BaseCommand): help = \ -'''Verify the structure of courseware as to it's suitability for import''' - + ''' + Verify the structure of courseware as to it's suitability for import + To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)] + ''' def handle(self, *args, **options): if len(args) == 0: raise CommandError("import requires at least one argument: [...]") diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 6794703998..64ccf73d5e 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -303,7 +303,7 @@ class XMLModuleStore(ModuleStoreBase): try: course_descriptor = self.load_course(course_dir, errorlog.tracker) except Exception as e: - msg = "Failed to load course '{0}': {1}".format(course_dir, str(e)) + msg = "ERROR: Failed to load course '{0}': {1}".format(course_dir, str(e)) log.exception(msg) errorlog.tracker(msg) @@ -337,7 +337,7 @@ class XMLModuleStore(ModuleStoreBase): with open(policy_path) as f: return json.load(f) except (IOError, ValueError) as err: - msg = "Error loading course policy from {0}".format(policy_path) + msg = "ERROR: loading course policy from {0}".format(policy_path) tracker(msg) log.warning(msg + " " + str(err)) return {} @@ -458,7 +458,8 @@ class XMLModuleStore(ModuleStoreBase): module.metadata['data_dir'] = course_dir self.modules[course_descriptor.id][module.location] = module except Exception, e: - logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e))) + logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e))) + system.error_tracker("ERROR: " + str(e)) def get_instance(self, course_id, location, depth=0): """ diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index a1739851ac..23eea58a97 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -227,6 +227,28 @@ def validate_category_hierarcy(module_store, course_id, parent_category, expecte def validate_module_structure(module_store): err_cnt = 0 warn_cnt = 0 + + print module_store.errored_courses + + # first count all errors and warnings as part of the XMLModuleStore import + for err_log in module_store._location_errors.itervalues(): + for err_log_entry in err_log.errors: + msg = err_log_entry[0] + if msg.startswith('ERROR:'): + err_cnt+=1 + else: + warn_cnt+=1 + + # then count outright all courses that failed to load at all + for err_log in module_store.errored_courses.itervalues(): + for err_log_entry in err_log.errors: + msg = err_log_entry[0] + print msg + if msg.startswith('ERROR:'): + err_cnt+=1 + else: + warn_cnt+=1 + for course_id in module_store.modules.keys(): # constrain that courses only have 'chapter' children err_cnt += validate_category_hierarcy(module_store, course_id, "course", "chapter") @@ -235,6 +257,14 @@ def validate_module_structure(module_store): # constrain that sequentials only have 'verticals' err_cnt += validate_category_hierarcy(module_store, course_id, "sequential", "vertical") - print "SUMMARY: {0} Errors {1} Warnings".format(err_cnt, warn_cnt) + print "\n\n------------------------------------------\nVALIDATION SUMMARY: {0} Errors {1} Warnings\n".format(err_cnt, warn_cnt) + + if err_cnt > 0: + print "This course is not suitable for importing. Please fix courseware according to specifications before importing." + elif warn_cnt > 0: + print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing" + else: + print "This course can be imported successfully." + diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 0ade3e0e7d..155ad99480 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -127,8 +127,10 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): for child in xml_object: try: children.append(system.process_xml(etree.tostring(child)).location.url()) - except: + except Exception, e: log.exception("Unable to load child when parsing Sequence. Continuing...") + if system.error_tracker is not None: + system.error_tracker("ERROR: " + str(e)) continue return {'children': children} diff --git a/rakefile b/rakefile index beb787c8c3..d8386fbda2 100644 --- a/rakefile +++ b/rakefile @@ -367,7 +367,9 @@ end namespace :cms do desc "Import course data within the given DATA_DIR variable" task :xlint do - if ENV['DATA_DIR'] + if ENV['DATA_DIR'] and ENV['COURSE_DIR'] + sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'], ENV['COURSE_DIR'])) + elsif ENV['DATA_DIR'] sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'])) else raise "Please specify a DATA_DIR variable that point to your data directory.\n" + From 630c2fa21ed53b8af1bb9b41bb9bd4597e4f7d95 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 13 Nov 2012 10:55:17 -0500 Subject: [PATCH 03/59] Make jasmine testing quieter --- cms/envs/jasmine.py | 4 +++- common/lib/logsettings.py | 11 ++++++++--- lms/envs/jasmine.py | 4 +++- rakefile | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py index b29e170411..5c9be1cf9c 100644 --- a/cms/envs/jasmine.py +++ b/cms/envs/jasmine.py @@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", dev_env=True, - debug=True) + debug=True, + local_loglevel='ERROR', + console_loglevel='ERROR') PIPELINE_JS['js-test-source'] = { 'source_filenames': sum([ diff --git a/common/lib/logsettings.py b/common/lib/logsettings.py index 2b001b0517..1e96534128 100644 --- a/common/lib/logsettings.py +++ b/common/lib/logsettings.py @@ -3,6 +3,7 @@ import platform import sys from logging.handlers import SysLogHandler +LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] def get_logger_config(log_dir, logging_env="no_env", @@ -11,7 +12,8 @@ def get_logger_config(log_dir, dev_env=False, syslog_addr=None, debug=False, - local_loglevel='INFO'): + local_loglevel='INFO', + console_loglevel=None): """ @@ -30,9 +32,12 @@ def get_logger_config(log_dir, """ # Revert to INFO if an invalid string is passed in - if local_loglevel not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: + if local_loglevel not in LOG_LEVELS: local_loglevel = 'INFO' + if console_loglevel is None or console_loglevel not in LOG_LEVELS: + console_loglevel = 'DEBUG' if debug else 'INFO' + hostname = platform.node().split(".")[0] syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s " "[{hostname} %(process)d] [%(filename)s:%(lineno)d] " @@ -55,7 +60,7 @@ def get_logger_config(log_dir, }, 'handlers': { 'console': { - 'level': 'DEBUG' if debug else 'INFO', + 'level': console_loglevel, 'class': 'logging.StreamHandler', 'formatter': 'standard', 'stream': sys.stdout, diff --git a/lms/envs/jasmine.py b/lms/envs/jasmine.py index 317628f8ba..8551d80504 100644 --- a/lms/envs/jasmine.py +++ b/lms/envs/jasmine.py @@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", dev_env=True, - debug=True) + debug=True, + local_loglevel='ERROR', + console_loglevel='ERROR') PIPELINE_JS['js-test-source'] = { 'source_filenames': sum([ diff --git a/rakefile b/rakefile index 4f1c15321f..5156c78147 100644 --- a/rakefile +++ b/rakefile @@ -47,7 +47,7 @@ def django_for_jasmine(system, django_reload) end django_pid = fork do - exec(*django_admin(system, 'jasmine', 'runserver', "12345", reload_arg).split(' ')) + exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', "12345", reload_arg).split(' ')) end jasmine_url = 'http://localhost:12345/_jasmine/' up = false From b779a421d719f20e49e3c10cdca4483174b9a412 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 14 Nov 2012 12:37:17 -0500 Subject: [PATCH 04/59] check for the existence of static and static/subs directories --- .../xmodule/modulestore/xml_importer.py | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 23eea58a97..c6764b491c 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -111,7 +111,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ) if validate_only: - validate_module_structure(module_store) + perform_xlint(data_dir, course_dirs, module_store) return module_store, [] # NOTE: the XmlModuleStore does not implement get_items() which would be a preferable means @@ -224,11 +224,34 @@ def validate_category_hierarcy(module_store, course_id, parent_category, expecte return err_cnt -def validate_module_structure(module_store): +def validate_data_source_path_existence(path, is_err = True, extra_msg = None): + _cnt = 0 + if not os.path.exists(path): + print ("{0}: Expected folder at {1}. {2}".format('ERROR' if is_err == True else 'WARNING', path, extra_msg if + extra_msg is not None else '')) + _cnt = 1 + return _cnt + +def validate_data_source_paths(data_dir, course_dir): + # check that there is a '/static/' directory + course_path = data_dir / course_dir + err_cnt = 0 + warn_cnt = 0 + err_cnt += validate_data_source_path_existence(course_path / 'static') + warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err = False, + extra_msg = 'Video captions (if they are used) will not work unless they are static/subs.') + return err_cnt, warn_cnt + + +def perform_xlint(data_dir, course_dirs,module_store): err_cnt = 0 warn_cnt = 0 - print module_store.errored_courses + # check all data source path information + for course_dir in course_dirs: + _err_cnt, _warn_cnt = validate_data_source_paths(path(data_dir), course_dir) + err_cnt += _err_cnt + warn_cnt += _warn_cnt # first count all errors and warnings as part of the XMLModuleStore import for err_log in module_store._location_errors.itervalues(): From 58fef4a32a7c83fda74895448177ea78de6c5c1c Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 14 Nov 2012 14:52:24 -0500 Subject: [PATCH 05/59] Create factory for courses --- .../contentstore/tests/factories.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 cms/djangoapps/contentstore/tests/factories.py diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py new file mode 100644 index 0000000000..bfb7e7fc20 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/factories.py @@ -0,0 +1,57 @@ +from factory import Factory +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from time import gmtime +from uuid import uuid4 +from xmodule.timeparse import stringify_time + + +class XModuleCourseFactory(Factory): + """ + Factory for XModule courses. + """ + + ABSTRACT_FACTORY = True + + @classmethod + def _create(cls, target_class, *args, **kwargs): + + template = Location('i4x', 'edx', 'templates', 'course', 'Empty') + org = kwargs.get('org') + number = kwargs.get('number') + display_name = kwargs.get('display_name') + location = Location('i4x', org, number, + 'course', Location.clean(display_name)) + + store = modulestore('direct') + + # Write the data to the mongo datastore + new_course = store.clone_item(template, location) + + # This metadata code was copied from cms/djangoapps/contentstore/views.py + if display_name is not None: + new_course.metadata['display_name'] = display_name + + new_course.metadata['data_dir'] = uuid4().hex + new_course.metadata['start'] = stringify_time(gmtime()) + new_course.tabs = [{"type": "courseware"}, + {"type": "course_info", "name": "Course Info"}, + {"type": "discussion", "name": "Discussion"}, + {"type": "wiki", "name": "Wiki"}, + {"type": "progress", "name": "Progress"}] + + # Update the data in the mongo datastore + store.update_metadata(new_course.location.url(), new_course.own_metadata) + + return new_course + +class Course: + pass + +class CourseFactory(XModuleCourseFactory): + FACTORY_FOR = Course + + template = 'i4x://edx/templates/course/Empty' + org = 'MITx' + number = '999' + display_name = 'Robot Super Course' \ No newline at end of file From 93cc17cf3be9b4eba0432e2c2ff2b32251d38862 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 14 Nov 2012 15:03:55 -0500 Subject: [PATCH 06/59] Starting to generate jasmine tests for common/lib --- common/lib/.gitignore | 1 + .../lib/xmodule/jasmine_test_runner.html.erb | 44 ++++++++++++ .../xmodule/js/spec/capa/display_spec.coffee | 1 + .../xmodule/xmodule/js}/src/xmodule.coffee | 0 rakefile | 70 ++++++++++++++----- 5 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 common/lib/.gitignore create mode 100644 common/lib/xmodule/jasmine_test_runner.html.erb rename common/{static/coffee => lib/xmodule/xmodule/js}/src/xmodule.coffee (100%) diff --git a/common/lib/.gitignore b/common/lib/.gitignore new file mode 100644 index 0000000000..bf6b783416 --- /dev/null +++ b/common/lib/.gitignore @@ -0,0 +1 @@ +*/jasmine_test_runner.html diff --git a/common/lib/xmodule/jasmine_test_runner.html.erb b/common/lib/xmodule/jasmine_test_runner.html.erb new file mode 100644 index 0000000000..01d55e50a9 --- /dev/null +++ b/common/lib/xmodule/jasmine_test_runner.html.erb @@ -0,0 +1,44 @@ + + + + Jasmine Test Runner + + + + + + + + + + + + + + + <% for src in js_source %> + + <% end %> + + + <% for src in js_specs %> + + <% end %> + + + + + + + + diff --git a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee index 107930c3b1..9910e8969d 100644 --- a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee @@ -8,6 +8,7 @@ describe 'Problem', -> MathJax.Hub.getAllJax.andReturn [@stubbedJax] window.update_schematics = -> + jasmine.getFixtures().fixturesPath = 'xmodule/js/fixtures' loadFixtures 'problem.html' spyOn Logger, 'log' spyOn($.fn, 'load').andCallFake (url, callback) -> diff --git a/common/static/coffee/src/xmodule.coffee b/common/lib/xmodule/xmodule/js/src/xmodule.coffee similarity index 100% rename from common/static/coffee/src/xmodule.coffee rename to common/lib/xmodule/xmodule/js/src/xmodule.coffee diff --git a/rakefile b/rakefile index 4f1c15321f..a9bcdd8d79 100644 --- a/rakefile +++ b/rakefile @@ -3,6 +3,8 @@ require 'tempfile' require 'net/http' require 'launchy' require 'colorize' +require 'erb' +require 'tempfile' # Build Constants REPO_ROOT = File.dirname(__FILE__) @@ -79,6 +81,25 @@ def django_for_jasmine(system, django_reload) end end +def template_jasmine_runner(lib) + coffee_files = Dir["#{lib}/**/js/**/*.coffee", "common/static/coffee/src/**/*.coffee"] + if !coffee_files.empty? + sh("coffee -c #{coffee_files.join(' ')}") + end + phantom_jasmine_path = File.expand_path("common/test/phantom-jasmine") + common_js_root = File.expand_path("common/static/js") + common_coffee_root = File.expand_path("common/static/coffee/src") + js_specs = Dir["#{lib}/**/js/spec/**/*.js"].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)} + js_source = Dir["#{lib}/**/*.js"].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)} - js_specs + template = ERB.new(File.read("#{lib}/jasmine_test_runner.html.erb")) + template_output = "#{lib}/jasmine_test_runner.html" + File.open(template_output, 'w') do |f| + f.write(template.result(binding)) + end + yield File.expand_path(template_output) +end + + def report_dir_path(dir) return File.join(REPORT_DIR, dir.to_s) end @@ -126,22 +147,6 @@ end end task :pylint => "pylint_#{system}" - desc "Open jasmine tests in your default browser" - task "browse_jasmine_#{system}" do - django_for_jasmine(system, true) do |jasmine_url| - Launchy.open(jasmine_url) - puts "Press ENTER to terminate".red - $stdin.gets - end - end - - desc "Use phantomjs to run jasmine tests from the console" - task "phantomjs_jasmine_#{system}" do - 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}") - end - end end $failed_tests = 0 @@ -210,6 +215,23 @@ TEST_TASK_DIRS = [] end end end + + desc "Open jasmine tests for #{system} in your default browser" + task "browse_jasmine_#{system}" do + django_for_jasmine(system, true) do |jasmine_url| + Launchy.open(jasmine_url) + puts "Press ENTER to terminate".red + $stdin.gets + end + end + + desc "Use phantomjs to run jasmine tests for #{system} from the console" + task "phantomjs_jasmine_#{system}" do + 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}") + end + end end desc "Reset the relational database used by django. WARNING: this will delete all of your existing users" @@ -245,6 +267,22 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| sh("nosetests #{lib}") end + desc "Open jasmine tests for #{lib} in your default browser" + task "browse_jasmine_#{lib}" do + template_jasmine_runner(lib) do |f| + sh("python -m webbrowser -t 'file://#{f}'") + puts "Press ENTER to terminate".red + $stdin.gets + end + end + + desc "Use phantomjs to run jasmine tests for #{lib} from the console" + task "phantomjs_jasmine_#{lib}" do + phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs' + template_jasmine_runner(lib) do |f| + sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{f}") + end + end end task :report_dirs From 6f5b3fa1bb86fa08f20ce74f0beca4da1d4b5b03 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 14 Nov 2012 15:12:09 -0500 Subject: [PATCH 07/59] Create factory for Xmodule items --- .../contentstore/tests/factories.py | 49 ++++++++++++++++++- cms/djangoapps/contentstore/tests/tests.py | 13 +++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py index bfb7e7fc20..624b792096 100644 --- a/cms/djangoapps/contentstore/tests/factories.py +++ b/cms/djangoapps/contentstore/tests/factories.py @@ -54,4 +54,51 @@ class CourseFactory(XModuleCourseFactory): template = 'i4x://edx/templates/course/Empty' org = 'MITx' number = '999' - display_name = 'Robot Super Course' \ No newline at end of file + display_name = 'Robot Super Course' + +class XModuleItemFactory(Factory): + """ + Factory for XModule items. + """ + + ABSTRACT_FACTORY = True + + @classmethod + def _create(cls, target_class, *args, **kwargs): + + DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] + + parent_location = Location(kwargs.get('parent_location')) + template = Location(kwargs.get('template')) + display_name = kwargs.get('display_name') + + store = modulestore('direct') + + parent = store.get_item(parent_location) + dest_location = parent_location._replace(category=template.category, name=uuid4().hex) + + new_item = store.clone_item(template, dest_location) + + # TODO: This needs to be deleted when we have proper storage for static content + new_item.metadata['data_dir'] = parent.metadata['data_dir'] + + # replace the display name with an optional parameter passed in from the caller + if display_name is not None: + new_item.metadata['display_name'] = display_name + + store.update_metadata(new_item.location.url(), new_item.own_metadata) + + if new_item.location.category not in DETACHED_CATEGORIES: + store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()]) + + return new_item + +class Item: + pass + +class ItemFactory(XModuleItemFactory): + FACTORY_FOR = Item + + parent_location = 'i4x://MITx/999/course/Robot_Super_Course' + template = 'i4x://edx/templates/chapter/Empty' + display_name = 'Section One' \ No newline at end of file diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index b0bb6c86a1..72909cbdf1 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -323,3 +323,16 @@ class ContentStoreTest(TestCase): def test_edit_unit_full(self): self.check_edit_unit('full') + def test_factory(self): + + from factories import * + + course = CourseFactory.create() + print '\n' + print course + print '\n' + section = ItemFactory.create() + + print '\n' + print section + print '\n' From 0654665b20023ba19171ef26ef3de564e5ac2cac Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Thu, 15 Nov 2012 11:40:29 -0500 Subject: [PATCH 08/59] Enable XModule factories to work with the currently version of Factory Boy at PyPI (1.2.0) so that the code will work with 'pip install factory_boy' --- .../contentstore/tests/factories.py | 9 ++++ cms/djangoapps/contentstore/tests/tests.py | 47 +++++++++---------- test-requirements.txt | 1 + 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py index 624b792096..3274477098 100644 --- a/cms/djangoapps/contentstore/tests/factories.py +++ b/cms/djangoapps/contentstore/tests/factories.py @@ -6,12 +6,19 @@ from uuid import uuid4 from xmodule.timeparse import stringify_time +def XMODULE_COURSE_CREATION(class_to_create, **kwargs): + return XModuleCourseFactory._create(class_to_create, **kwargs) + +def XMODULE_ITEM_CREATION(class_to_create, **kwargs): + return XModuleItemFactory._create(class_to_create, **kwargs) + class XModuleCourseFactory(Factory): """ Factory for XModule courses. """ ABSTRACT_FACTORY = True + _creation_function = (XMODULE_COURSE_CREATION,) @classmethod def _create(cls, target_class, *args, **kwargs): @@ -62,6 +69,7 @@ class XModuleItemFactory(Factory): """ ABSTRACT_FACTORY = True + _creation_function = (XMODULE_ITEM_CREATION,) @classmethod def _create(cls, target_class, *args, **kwargs): @@ -74,6 +82,7 @@ class XModuleItemFactory(Factory): store = modulestore('direct') + # This code was based off that in cms/djangoapps/contentstore/views.py parent = store.get_item(parent_location) dest_location = parent_location._replace(category=template.category, name=uuid4().hex) diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 72909cbdf1..9ddbe049ad 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -14,6 +14,7 @@ import xmodule.modulestore.django from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml import copy +from factories import * def parse_json(response): @@ -220,12 +221,6 @@ class ContentStoreTest(TestCase): 'display_name': 'Robot Super Course', } - self.section_data = { - 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course', - 'template' : 'i4x://edx/templates/chapter/Empty', - 'display_name': 'Section One', - } - def tearDown(self): # Make sure you flush out the test modulestore after the end # of the last test because otherwise on the next run @@ -271,20 +266,27 @@ class ContentStoreTest(TestCase): status_code=200, html=True) + def test_course_factory(self): + course = CourseFactory.create() + self.assertIsInstance(course, xmodule.course_module.CourseDescriptor) + + def test_item_factory(self): + course = CourseFactory.create() + item = ItemFactory.create(parent_location=course.location) + self.assertIsInstance(item, xmodule.seq_module.SequenceDescriptor) + def test_course_index_view_with_course(self): """Test viewing the index page with an existing course""" - # Create a course so there is something to view - resp = self.client.post(reverse('create_new_course'), self.course_data) + CourseFactory.create(display_name='Robot Super Educational Course') resp = self.client.get(reverse('index')) self.assertContains(resp, - 'Robot Super Course', + 'Robot Super Educational Course', status_code=200, html=True) def test_course_overview_view_with_course(self): """Test viewing the course overview page with an existing course""" - # Create a course so there is something to view - resp = self.client.post(reverse('create_new_course'), self.course_data) + CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') data = { 'org': 'MITx', @@ -300,8 +302,15 @@ class ContentStoreTest(TestCase): def test_clone_item(self): """Test cloning an item. E.g. creating a new section""" - resp = self.client.post(reverse('create_new_course'), self.course_data) - resp = self.client.post(reverse('clone_item'), self.section_data) + CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') + + section_data = { + 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course', + 'template' : 'i4x://edx/templates/chapter/Empty', + 'display_name': 'Section One', + } + + resp = self.client.post(reverse('clone_item'), section_data) self.assertEqual(resp.status_code, 200) data = parse_json(resp) @@ -323,16 +332,4 @@ class ContentStoreTest(TestCase): def test_edit_unit_full(self): self.check_edit_unit('full') - def test_factory(self): - from factories import * - - course = CourseFactory.create() - print '\n' - print course - print '\n' - section = ItemFactory.create() - - print '\n' - print section - print '\n' diff --git a/test-requirements.txt b/test-requirements.txt index c9c15b340d..7048faad38 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,3 +3,4 @@ coverage nosexcover pylint pep8 +factory_boy From 39c33b42b284a11f6325de6a14e57831d69e18b4 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Thu, 15 Nov 2012 13:56:42 -0500 Subject: [PATCH 09/59] remove 'mode' property when creating a new CodeMirror control. This appears to completely crash if the user puts in a