161 lines
4.5 KiB
Python
161 lines
4.5 KiB
Python
"""
|
|
Contains functions that handle XML course data
|
|
"""
|
|
|
|
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
import lxml.etree
|
|
from django.core.management.base import BaseCommand
|
|
from fs.osfs import OSFS
|
|
from path import Path as path
|
|
|
|
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('.'): # lint-amnesty, pylint: disable=no-member
|
|
print(f'WARNING: Directory {export_dir} not-empty. May clobber/confuse things')
|
|
|
|
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: # lint-amnesty, pylint: disable=bare-except
|
|
print('Export failed!')
|
|
traceback.print_exc()
|
|
|
|
return False
|
|
|
|
|
|
def import_with_checks(course_dir): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
all_ok = True
|
|
|
|
print(f'Attempting to load "{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 f'{msg}\n{exc_str}'
|
|
|
|
courses = modulestore.get_courses()
|
|
|
|
n = len(courses)
|
|
if n != 1:
|
|
print(f'ERROR: Expect exactly 1 course. Loaded {n}: {courses}')
|
|
return (False, None)
|
|
|
|
course = courses[0]
|
|
errors = modulestore.get_course_errors(course.id)
|
|
if len(errors) != 0:
|
|
all_ok = False
|
|
print(
|
|
'\n' +
|
|
'========================================' +
|
|
'ERRORs during import:' +
|
|
'\n'.join(map(str_of_err, errors)) +
|
|
'========================================' +
|
|
'\n'
|
|
)
|
|
|
|
# print course
|
|
validators = (
|
|
traverse_tree,
|
|
)
|
|
|
|
print('========================================')
|
|
print('Running validators...')
|
|
|
|
for validate in validators:
|
|
print(f'Running {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(f'diff -r {course_dir} {export_dir}')
|
|
print('======== ideally there is no diff above this =======')
|
|
|
|
|
|
class Command(BaseCommand): # lint-amnesty, pylint: disable=missing-class-docstring
|
|
help = 'Imports specified course, validates it, then exports it in a canonical format.'
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument('course_dir',
|
|
help='path to the input course directory')
|
|
parser.add_argument('output_dir',
|
|
help='path to the output course directory')
|
|
parser.add_argument('--force',
|
|
action='store_true',
|
|
help='export course even if there were import errors')
|
|
|
|
def handle(self, *args, **options):
|
|
course_dir = options['course_dir']
|
|
output_dir = options['output_dir']
|
|
force = options['force']
|
|
|
|
(ok, course) = import_with_checks(course_dir)
|
|
if ok or force:
|
|
if not ok:
|
|
print('WARNING: Exporting despite errors')
|
|
export(course, output_dir)
|
|
check_roundtrip(output_dir)
|
|
else:
|
|
print('Did NOT export')
|