The old line:
from path import path
produced pylint errors because of the baroque way that path.py defined
"path". We tried to get them to change how they defined it, but they
deleted the name instead: https://github.com/jaraco/path.py/issues/102
(Jason then changed his mind, but this is a better way to use path.py,
it avoids the pylint error at least.)
158 lines
4.1 KiB
Python
158 lines
4.1 KiB
Python
import lxml.etree
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
from fs.osfs import OSFS
|
|
from path import Path as path
|
|
|
|
from django.core.management.base import BaseCommand
|
|
|
|
from xmodule.modulestore.xml import XMLModuleStore
|
|
|
|
|
|
def traverse_tree(course):
|
|
"""Load every descriptor in course. Return bool success value."""
|
|
queue = [course]
|
|
while len(queue) > 0:
|
|
node = queue.pop()
|
|
queue.extend(node.get_children())
|
|
|
|
return True
|
|
|
|
|
|
def export(course, export_dir):
|
|
"""Export the specified course to course_dir. Creates dir if it doesn't exist.
|
|
Overwrites files, does not clean out dir beforehand.
|
|
"""
|
|
fs = OSFS(export_dir, create=True)
|
|
if not fs.isdirempty('.'):
|
|
print ('WARNING: Directory {dir} not-empty.'
|
|
' May clobber/confuse things'.format(dir=export_dir))
|
|
|
|
try:
|
|
course.runtime.export_fs = fs
|
|
root = lxml.etree.Element('root')
|
|
course.add_xml_to_node(root)
|
|
with fs.open('course.xml', mode='w') as f:
|
|
root.write(f)
|
|
|
|
return True
|
|
except:
|
|
print 'Export failed!'
|
|
traceback.print_exc()
|
|
|
|
return False
|
|
|
|
|
|
def import_with_checks(course_dir):
|
|
all_ok = True
|
|
|
|
print "Attempting to load '{0}'".format(course_dir)
|
|
|
|
course_dir = path(course_dir)
|
|
data_dir = course_dir.dirname()
|
|
source_dirs = [course_dir.basename()]
|
|
|
|
# No default class--want to complain if it doesn't find plugins for any
|
|
# module.
|
|
modulestore = XMLModuleStore(
|
|
data_dir,
|
|
default_class=None,
|
|
source_dirs=source_dirs
|
|
)
|
|
|
|
def str_of_err(tpl):
|
|
(msg, exc_str) = tpl
|
|
return '{msg}\n{exc}'.format(msg=msg, exc=exc_str)
|
|
|
|
courses = modulestore.get_courses()
|
|
|
|
n = len(courses)
|
|
if n != 1:
|
|
print 'ERROR: Expect exactly 1 course. Loaded {n}: {lst}'.format(
|
|
n=n, lst=courses)
|
|
return (False, None)
|
|
|
|
course = courses[0]
|
|
errors = modulestore.get_course_errors(course.id)
|
|
if len(errors) != 0:
|
|
all_ok = False
|
|
print '\n'
|
|
print "=" * 40
|
|
print 'ERRORs during import:'
|
|
print '\n'.join(map(str_of_err, errors))
|
|
print "=" * 40
|
|
print '\n'
|
|
|
|
# print course
|
|
validators = (
|
|
traverse_tree,
|
|
)
|
|
|
|
print "=" * 40
|
|
print "Running validators..."
|
|
|
|
for validate in validators:
|
|
print 'Running {0}'.format(validate.__name__)
|
|
all_ok = validate(course) and all_ok
|
|
|
|
if all_ok:
|
|
print 'Course passes all checks!'
|
|
else:
|
|
print "Course fails some checks. See above for errors."
|
|
return all_ok, course
|
|
|
|
|
|
def check_roundtrip(course_dir):
|
|
"""Check that import->export leaves the course the same"""
|
|
|
|
print "====== Roundtrip import ======="
|
|
(ok, course) = import_with_checks(course_dir)
|
|
if not ok:
|
|
raise Exception("Roundtrip import failed!")
|
|
|
|
print "====== Roundtrip export ======="
|
|
export_dir = course_dir + ".rt"
|
|
export(course, export_dir)
|
|
|
|
# dircmp doesn't do recursive diffs.
|
|
# diff = dircmp(course_dir, export_dir, ignore=[], hide=[])
|
|
print "======== Roundtrip diff: ========="
|
|
sys.stdout.flush() # needed to make diff appear in the right place
|
|
os.system("diff -r {0} {1}".format(course_dir, export_dir))
|
|
print "======== ideally there is no diff above this ======="
|
|
|
|
|
|
def clean_xml(course_dir, export_dir, force):
|
|
(ok, course) = import_with_checks(course_dir)
|
|
if ok or force:
|
|
if not ok:
|
|
print "WARNING: Exporting despite errors"
|
|
export(course, export_dir)
|
|
check_roundtrip(export_dir)
|
|
else:
|
|
print "Did NOT export"
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = """Imports specified course.xml, validate it, then exports in
|
|
a canonical format.
|
|
|
|
Usage: clean_xml PATH-TO-COURSE-DIR PATH-TO-OUTPUT-DIR [force]
|
|
|
|
If 'force' is specified as the last argument, exports even if there
|
|
were import errors.
|
|
"""
|
|
|
|
def handle(self, *args, **options):
|
|
n = len(args)
|
|
if n < 2 or n > 3:
|
|
print Command.help
|
|
return
|
|
|
|
force = False
|
|
if n == 3 and args[2] == 'force':
|
|
force = True
|
|
clean_xml(args[0], args[1], force)
|