Files
edx-platform/lms/djangoapps/courseware/management/commands/clean_xml.py

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')