Skip to content

Commit

Permalink
feat: Cleaned up imports, reverted export_olx to original state, crea…
Browse files Browse the repository at this point in the history
…ted new export_imscc file
  • Loading branch information
mystica-l committed Dec 6, 2024
1 parent f487286 commit 7975d0d
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 67 deletions.
121 changes: 121 additions & 0 deletions cms/djangoapps/contentstore/management/commands/export_imscc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
A Django command that exports a course to a tar.gz file using IMSCC protocol
At present, it differs from Studio exports in several ways:
* It does not include static content.
* It only supports the export of courses. It does not export libraries.
"""

import os
import re
import shutil
import tarfile
from tempfile import mkdtemp, mktemp
from textwrap import dedent

from django.core.management.base import BaseCommand, CommandError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from path import Path as path

from xmodule.modulestore.django import modulestore
from xmodule.modulestore.imscc_exporter import export_course_to_imscc

class Command(BaseCommand):
"""
Export a course to IMSCC. The output is compressed as a tar.gz file.
"""
help = dedent(__doc__).strip()

def add_arguments(self, parser):
parser.add_argument('course_id', nargs="+") #nargs = "+" allows parsing of unlimited course ids
parser.add_argument('--output')
parser.add_argument('--external-tool-only', action = 'store_true', help = 'Export Common Cartridge file using only external tools and no assignment types')

def handle(self, *args, **options):
external_tool_only = options.get('external_tool_only', False)
course_ids = options['course_id']

# stores all the different course keys based on the inputted course ids
course_keys = []
for course_id in course_ids:
try:
course_keys.append(CourseKey.from_string(course_id))
except InvalidKeyError:
raise CommandError("Unparsable course_id") # lint-amnesty, pylint: disable=raise-missing-from
except IndexError:
raise CommandError("Insufficient arguments") # lint-amnesty, pylint: disable=raise-missing-from

filename = options['output']
pipe_results = False

if filename is None:
filename = mktemp()
pipe_results = True

export_course_to_tarfile(course_keys, filename, external_tool_only)

results = self._get_results(filename) if pipe_results else b''

# results is of type bytes, so we must write the underlying buffer directly.
self.stdout.buffer.write(results)

def _get_results(self, filename):
"""
Load results from file.
Returns:
bytes: bytestring of file contents.
"""
with open(filename, 'rb') as f:
results = f.read()
os.remove(filename)
return results


def export_course_to_tarfile(course_keys, filename, external_tool_only):
"""Exports a course into a tar.gz file"""
tmp_dir = mkdtemp()
try:
course_dir = export_course_to_directory(course_keys, tmp_dir, external_tool_only)
compress_directory(course_dir, filename)
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)


def export_course_to_directory(course_keys, root_dir, external_tool_only):
"""Export course into a directory"""
# attempt to get all the courses based on the course_keys
store = modulestore()
courses = []
for course_key in course_keys:
course = store.get_course(course_key)
if course is None:
raise CommandError("Invalid course_id")
courses.append(course)

course_ids = []
for course in courses:
course_ids.append(course.id)
# The safest characters are A-Z, a-z, 0-9, <underscore>, <period> and <hyphen>.
# We represent the first four with \w.
# TODO: Once we support courses with unicode characters, we will need to revisit this.
replacement_char = '-'
course_dir = replacement_char.join([courses[0].id.org, courses[0].id.course, courses[0].id.run])
course_dir = re.sub(r'[^\w\.\-]', replacement_char, course_dir)

if len(courses) > 1:
course_dir = "MULTI-COURSE-EXPORT"
export_course_to_imscc(store, None, course_ids, root_dir, course_dir, external_tool_only)

export_dir = path(root_dir) / course_dir
return export_dir


def compress_directory(directory, filename):
"""Compress a directory into a tar.gz file"""
mode = 'w:gz'
name = path(directory).name
with tarfile.open(filename, mode) as tar_file:
tar_file.add(directory, arcname=name)
65 changes: 19 additions & 46 deletions cms/djangoapps/contentstore/management/commands/export_olx.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@

from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.imscc_exporter import export_course_to_imscc



class Command(BaseCommand):
Expand All @@ -40,31 +38,18 @@ class Command(BaseCommand):
help = dedent(__doc__).strip()

def add_arguments(self, parser):
parser.add_argument('course_id', nargs="+") #nargs = "+" allows parsing of unlimited course ids
parser.add_argument('course_id')
parser.add_argument('--output')
parser.add_argument('--cc-lti', action = 'store_true', help = 'Run the command with Common Cartridge format')
parser.add_argument('--external-tool-only', action = 'store_true', help = 'Export Common Cartridge file using only external tools and no assignment types')

def handle(self, *args, **options):
cc_lti = options.get('cc_lti', False)
external_tool_only = options.get('external_tool_only', False)
course_ids = options['course_id']

# Raise an error only allowing courses to be exported 1 at a time when not using Common Cartridge packaging standards
if not cc_lti and len(course_ids) > 1:
raise CommandError("Can only export 1 OpenEdX course at at time in default OpenEdX packaging standards")
if external_tool_only and not cc_lti:
raise CommandError("Cannot export with the --external_tool_only option in default OpenEdX packaging standards")

# stores all the different course keys based on the inputted course ids
course_keys = []
for course_id in course_ids:
try:
course_keys.append(CourseKey.from_string(course_id))
except InvalidKeyError:
raise CommandError("Unparsable course_id") # lint-amnesty, pylint: disable=raise-missing-from
except IndexError:
raise CommandError("Insufficient arguments") # lint-amnesty, pylint: disable=raise-missing-from
course_id = options['course_id']

try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
raise CommandError("Unparsable course_id") # lint-amnesty, pylint: disable=raise-missing-from
except IndexError:
raise CommandError("Insufficient arguments") # lint-amnesty, pylint: disable=raise-missing-from

filename = options['output']
pipe_results = False
Expand All @@ -73,7 +58,7 @@ def handle(self, *args, **options):
filename = mktemp()
pipe_results = True

export_course_to_tarfile(course_keys, filename, cc_lti, external_tool_only)
export_course_to_tarfile(course_key, filename)

results = self._get_results(filename) if pipe_results else b''

Expand All @@ -93,43 +78,31 @@ def _get_results(self, filename):
return results


def export_course_to_tarfile(course_keys, filename, cc_lti, external_tool_only):
def export_course_to_tarfile(course_key, filename):
"""Exports a course into a tar.gz file"""
tmp_dir = mkdtemp()
try:
course_dir = export_course_to_directory(course_keys, tmp_dir, cc_lti, external_tool_only)
course_dir = export_course_to_directory(course_key, tmp_dir)
compress_directory(course_dir, filename)
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)


def export_course_to_directory(course_keys, root_dir, cc_lti, external_tool_only):
def export_course_to_directory(course_key, root_dir):
"""Export course into a directory"""
# attempt to get all the courses based on the course_keys
store = modulestore()
courses = []
for course_key in course_keys:
course = store.get_course(course_key)
if course is None:
raise CommandError("Invalid course_id")
courses.append(course)

course_ids = []
for course in courses:
course_ids.append(course.id)
course = store.get_course(course_key)
if course is None:
raise CommandError("Invalid course_id")

# The safest characters are A-Z, a-z, 0-9, <underscore>, <period> and <hyphen>.
# We represent the first four with \w.
# TODO: Once we support courses with unicode characters, we will need to revisit this.
replacement_char = '-'
course_dir = replacement_char.join([courses[0].id.org, courses[0].id.course, courses[0].id.run])
course_dir = replacement_char.join([course.id.org, course.id.course, course.id.run])
course_dir = re.sub(r'[^\w\.\-]', replacement_char, course_dir)

if cc_lti:
if len(courses) > 1:
course_dir = "MULTI-COURSE-EXPORT"
export_course_to_imscc(store, None, course_ids, root_dir, course_dir, external_tool_only)
else:
export_course_to_xml(store, None, course_ids[0], root_dir, course_dir)
export_course_to_xml(store, None, course.id, root_dir, course_dir)

export_dir = path(root_dir) / course_dir
return export_dir
Expand Down
25 changes: 4 additions & 21 deletions common/lib/xmodule/xmodule/modulestore/imscc_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,15 @@
Methods for exporting course data to IMSCC
"""

import logging
import os
from abc import abstractmethod
from json import dumps

import lxml.etree
from fs.osfs import OSFS
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
from xblock.fields import Reference, ReferenceList, ReferenceValueDict, Scope

from xmodule.assetstore import AssetMetadata
from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError
from xmodule.modulestore import LIBRARY_ROOT, EdxJSONEncoder, ModuleStoreEnum
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.store_utilities import draft_node_constructor, get_draft_subtree_roots
from opaque_keys.edx.locator import CourseLocator
from xmodule.modulestore import ModuleStoreEnum

import uuid
from datetime import datetime
import re

DRAFT_DIR = "drafts"
PUBLISHED_DIR = "published"

DEFAULT_CONTENT_FIELDS = ['metadata', 'data']

class SerializableChapterSequential:
"""
Expand Down Expand Up @@ -69,7 +52,7 @@ def __eq__(self, other):
self.course_id == other.course_id)
return False

class TestExportManager:
class CourseExportManager:
"""
Manages IMSCC exporting for courselike objects.
"""
Expand Down Expand Up @@ -787,4 +770,4 @@ def export_course_to_imscc(modulestore, contentstore, course_key, root_dir, cour
"""
Thin wrapper for the Export Manager. See ExportManager for details.
"""
TestExportManager(modulestore, contentstore, course_key, root_dir, course_dir, external_tool_only).export()
CourseExportManager(modulestore, contentstore, course_key, root_dir, course_dir, external_tool_only).export()

0 comments on commit 7975d0d

Please sign in to comment.