diff --git a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py index 5bb5ea4f5b00..cb9bfe548eb6 100644 --- a/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py +++ b/openedx/core/djangoapps/appsembler/settings/settings/aws_lms.py @@ -101,3 +101,8 @@ def plugin_settings(settings): settings.USE_S3_FOR_CUSTOMER_THEMES = True _add_theme_static_dirs(settings) + + settings.TAHOE_DEFAULT_COURSE_NAME = settings.ENV_TOKENS.get('TAHOE_DEFAULT_COURSE_NAME', '') + settings.TAHOE_DEFAULT_COURSE_GITHUB_ORG = settings.ENV_TOKENS.get('TAHOE_DEFAULT_COURSE_GITHUB_ORG', '') + settings.TAHOE_DEFAULT_COURSE_GITHUB_NAME = settings.ENV_TOKENS.get('TAHOE_DEFAULT_COURSE_GITHUB_NAME', '') + settings.TAHOE_DEFAULT_COURSE_VERSION = settings.ENV_TOKENS.get('TAHOE_DEFAULT_COURSE_VERSION', '') diff --git a/openedx/core/djangoapps/appsembler/sites/serializers.py b/openedx/core/djangoapps/appsembler/sites/serializers.py index 96b40b8eee43..39458c15c1a5 100644 --- a/openedx/core/djangoapps/appsembler/sites/serializers.py +++ b/openedx/core/djangoapps/appsembler/sites/serializers.py @@ -6,7 +6,7 @@ from organizations.models import Organization from openedx.core.djangoapps.site_configuration.models import SiteConfiguration -from openedx.core.djangoapps.appsembler.sites.tasks import clone_course +from openedx.core.djangoapps.appsembler.sites.tasks import import_course_on_site_creation from openedx.core.djangoapps.appsembler.sites.models import AlternativeDomain from openedx.core.djangoapps.appsembler.sites.utils import sass_to_dict, dict_to_sass, bootstrap_site @@ -135,11 +135,9 @@ def create(self, validated_data): site_configuration.save() # clone course - if settings.CLONE_COURSE_FOR_NEW_SIGNUPS: - source_course_locator = CourseLocator.from_string(settings.COURSE_TO_CLONE) - destination_course_locator = CourseLocator(organization.name, 'My first course', '2017') - clone_course.apply_async( - (unicode(source_course_locator), unicode(destination_course_locator), user.id), + if settings.FEATURES.get("APPSEMBLER_IMPORT_DEFAULT_COURSE_ON_SITE_CREATION", False): + import_course_on_site_creation.apply_async( + organization, queue="edx.core.cms.high" ) return { diff --git a/openedx/core/djangoapps/appsembler/sites/tasks.py b/openedx/core/djangoapps/appsembler/sites/tasks.py index 822637d07a1d..54be77a76f63 100644 --- a/openedx/core/djangoapps/appsembler/sites/tasks.py +++ b/openedx/core/djangoapps/appsembler/sites/tasks.py @@ -1,69 +1,103 @@ """ This file contains celery tasks for contentstore views """ +import os import logging +import requests +import uuid +import os +import tarfile +import shutil +import datetime + +from backports import tempfile + from celery.task import task from celery.utils.log import get_task_logger -from django.contrib.auth.models import User +from django.conf import settings + +from student.roles import CourseAccessRole +from django.contrib.auth.models import User from opaque_keys.edx.keys import CourseKey from student.roles import CourseInstructorRole, CourseStaffRole +from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError +from xmodule.modulestore.xml_importer import import_course_from_xml LOGGER = get_task_logger(__name__) FULL_COURSE_REINDEX_THRESHOLD = 1 @task() -def clone_course(source_course_key_string, destination_course_key_string, user_id, fields=None): +def import_course_on_site_creation(organization): """ Reruns a course in a new celery task. """ - # import here, at top level this import prevents the celery workers from starting up correctly - from edxval.api import copy_course_videos - from contentstore.utils import initialize_permissions - from contentstore.courseware_index import SearchIndexingError - from contentstore.views.course import reindex_course_and_check_access - try: - # deserialize the payload - source_course_key = CourseKey.from_string(source_course_key_string) - destination_course_key = CourseKey.from_string(destination_course_key_string) - - # use the split modulestore as the store for the rerun course, - # as the Mongo modulestore doesn't support multiple runs of the same course. - store = modulestore() - with store.default_store('split'): - store.clone_course(source_course_key, destination_course_key, user_id, fields=fields) - - # Send statistics - from appsembler.models import update_course_statistics - update_course_statistics() - - # set initial permissions for the user to access the course. - user = User.objects.get(id=user_id) - initialize_permissions(destination_course_key, user) - - # add course intructor and staff roles to the new user - CourseInstructorRole(destination_course_key).add_users(user) - CourseStaffRole(destination_course_key).add_users(user) - - # call edxval to attach videos to the rerun - copy_course_videos(source_course_key, destination_course_key) - - return "succeeded" - - except DuplicateCourseError as exc: - # do NOT delete the original course, only update the status - logging.exception(u'Course Clone Error') - return "duplicate course" - - except SearchIndexingError as search_err: - logging.exception(u'Course Clone index Error') - return "index error" + course_name = settings.TAHOE_DEFAULT_COURSE_NAME + course_github_org = settings.TAHOE_DEFAULT_COURSE_GITHUB_ORG + course_github_name = settings.TAHOE_DEFAULT_COURSE_GITHUB_NAME + course_version = settings.TAHOE_DEFAULT_COURSE_VERSION + + # build the github download URL from settings to download the course + # in tar.gz format from the github releases + course_download_url = 'https://github.com/{}/{}/archive/{}.tar.gz'.format( + course_github_org, + course_github_name, + course_version + ) + + # download the course in tar.gz format from github in /tmp using a + # random file name + with tempfile.TemporaryDirectory() as dir: + r = requests.get(course_download_url) + course_file_name = str(uuid.uuid4()) + ".tar.gz" + file_path = os.path.join("/", "tmp", course_file_name) + with open(file_path, 'wb') as fd: + for chunk in r.iter_content(chunk_size=128): + fd.write(chunk) + + # untar the downloaded file in the /tmp/random_dir just created + with tarfile.open(file_path) as tar: + extracted_course_folder = tar.getnames()[0] + tar.extractall(path=dir) + + # create the course key object with the current created + # organization, the course name from the settings and the current + # year + course_target_id = CourseKey.from_string( + 'course-v1:{}+{}+{}'.format( + organization.short_name, + course_name, + str(datetime.datetime.now().year) + ) + ) + + # import the course + mstore = modulestore() + course_items = import_course_from_xml( + mstore, + ModuleStoreEnum.UserID.mgmt_command, + os.path.join(dir, extracted_course_folder), + source_dirs=os.path.join(dir, extracted_course_folder, 'course'), + target_id=course_target_id, + create_if_not_present=True, + ) + + # remove the course file + os.remove(file_path) + + # add the new registered user as admin in the course + access_role, created = CourseAccessRole.objects.get_or_create( + user=organization.users.first(), + role="instructor", + course_id=course_target_id, + org=organization.short_name + ) # catch all exceptions so we can update the state and properly cleanup the course. except Exception as exc: # pylint: disable=broad-except @@ -72,7 +106,7 @@ def clone_course(source_course_key_string, destination_course_key_string, user_i try: # cleanup any remnants of the course - modulestore().delete_course(destination_course_key, user_id) + modulestore().delete_course(course_target_id, organization.users.first()) except ItemNotFoundError: # it's possible there was an error even before the course module was created pass diff --git a/requirements/edx/appsembler.txt b/requirements/edx/appsembler.txt index 0c785b07c9bf..db0bc0e14ed5 100644 --- a/requirements/edx/appsembler.txt +++ b/requirements/edx/appsembler.txt @@ -10,3 +10,4 @@ psycopg2-binary==2.8.3 django-compat==1.0.14 django-hijack==2.1.4 django-hijack-admin==2.1.4 +backports.tempfile==1.0