From 24fcab7f24e5fcf78f5345b321664050ca055b9f Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 6 Feb 2015 01:22:55 -0500 Subject: [PATCH] Very rough cut at generating a course outline after publishing. --- cms/envs/common.py | 2 + .../lib/xmodule/xmodule/modulestore/django.py | 17 +++++ .../lib/xmodule/xmodule/modulestore/mixed.py | 3 + .../xmodule/xmodule/modulestore/mongo/base.py | 2 + .../xmodule/modulestore/split_mongo/split.py | 4 +- .../modulestore/split_mongo/split_draft.py | 6 +- .../xmodule/modulestore/tests/utils.py | 3 +- common/lib/xmodule/xmodule/modulestore/xml.py | 3 +- lms/envs/common.py | 2 + openedx/core/djangoapps/content/__init__.py | 0 .../content/course_structures/__init__.py | 0 .../content/course_structures/admin.py | 5 ++ .../migrations/0001_initial.py | 40 ++++++++++++ .../course_structures/migrations/__init__.py | 0 .../content/course_structures/models.py | 62 +++++++++++++++++++ 15 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 openedx/core/djangoapps/content/__init__.py create mode 100644 openedx/core/djangoapps/content/course_structures/__init__.py create mode 100644 openedx/core/djangoapps/content/course_structures/admin.py create mode 100644 openedx/core/djangoapps/content/course_structures/migrations/0001_initial.py create mode 100644 openedx/core/djangoapps/content/course_structures/migrations/__init__.py create mode 100644 openedx/core/djangoapps/content/course_structures/models.py diff --git a/cms/envs/common.py b/cms/envs/common.py index fbc8eb311a..947db13b9f 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -719,6 +719,8 @@ INSTALLED_APPS = ( # Additional problem types 'edx_jsme', # Molecular Structure + + 'openedx.core.djangoapps.content.course_structures', ) diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index 5cc452111d..7c094f0feb 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -11,6 +11,7 @@ from django.conf import settings if not settings.configured: settings.configure() from django.core.cache import get_cache, InvalidCacheBackendError +import django.dispatch import django.utils import re @@ -39,6 +40,20 @@ except ImportError: ASSET_IGNORE_REGEX = getattr(settings, "ASSET_IGNORE_REGEX", r"(^\._.*$)|(^\.DS_Store$)|(^.*~$)") +class SignalHandler(object): + course_published = django.dispatch.Signal(providing_args=["course_key", "version"]) + + _mapping = { + "course_published": course_published + } + + def __init__(self, modulestore_class): + self.modulestore_class = modulestore_class + + def send(self, signal_name, **kwargs): + signal = self._mapping[signal_name] + signal.send_robust(sender=self.modulestore_class, **kwargs) + def load_function(path): """ @@ -59,6 +74,7 @@ def create_modulestore_instance( i18n_service=None, fs_service=None, user_service=None, + signal_handler=None, ): """ This will return a new instance of a modulestore given an engine and options @@ -104,6 +120,7 @@ def create_modulestore_instance( i18n_service=i18n_service or ModuleI18nService(), fs_service=fs_service or xblock.reference.plugins.FSService(), user_service=user_service or xb_user_service, + signal_handler=signal_handler or SignalHandler(class_), **_options ) diff --git a/common/lib/xmodule/xmodule/modulestore/mixed.py b/common/lib/xmodule/xmodule/modulestore/mixed.py index b3c775619b..fb554d35e1 100644 --- a/common/lib/xmodule/xmodule/modulestore/mixed.py +++ b/common/lib/xmodule/xmodule/modulestore/mixed.py @@ -108,6 +108,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): fs_service=None, user_service=None, create_modulestore_instance=None, + signal_handler=None, **kwargs ): """ @@ -142,6 +143,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): for course_key, store_key in self.mappings.iteritems() if store_key == key ] + store = create_modulestore_instance( store_settings['ENGINE'], self.contentstore, @@ -150,6 +152,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): i18n_service=i18n_service, fs_service=fs_service, user_service=user_service, + signal_handler=signal_handler, ) # replace all named pointers to the store into actual pointers for course_key, store_name in self.mappings.iteritems(): diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py index a0d8001359..b6255bd4b7 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py @@ -504,6 +504,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo i18n_service=None, fs_service=None, user_service=None, + signal_handler=None, retry_wait_time=0.1, **kwargs): """ @@ -560,6 +561,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo self.user_service = user_service self._course_run_cache = {} + self.signal_handler = signal_handler def close_connections(self): """ diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 324f317698..2fdb9590bc 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -608,7 +608,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): default_class=None, error_tracker=null_error_tracker, i18n_service=None, fs_service=None, user_service=None, - services=None, **kwargs): + services=None, signal_handler=None, **kwargs): """ :param doc_store_config: must have a host, db, and collection entries. Other common entries: port, tz_aware. """ @@ -637,6 +637,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): if user_service is not None: self.services["user"] = user_service + self.signal_handler = signal_handler + def close_connections(self): """ Closes any open connections to the underlying databases diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py index 9c8e38073f..78c8d141e6 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py @@ -354,7 +354,11 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli # Now it's been published, add the object to the courseware search index so that it appears in search results CoursewareSearchIndexer.add_to_search_index(self, location) - return self.get_item(location.for_branch(ModuleStoreEnum.BranchName.published), **kwargs) + published_location = location.for_branch(ModuleStoreEnum.BranchName.published) + if self.signal_handler: + self.signal_handler.send("course_published", course_key=published_location.course_key) + + return self.get_item(published_location, **kwargs) def unpublish(self, location, user_id, **kwargs): """ diff --git a/common/lib/xmodule/xmodule/modulestore/tests/utils.py b/common/lib/xmodule/xmodule/modulestore/tests/utils.py index 6a2353e8ff..36e1149d8e 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/utils.py @@ -34,7 +34,8 @@ def create_modulestore_instance( options, i18n_service=None, fs_service=None, - user_service=None + user_service=None, + signal_handler=None, ): """ This will return a new instance of a modulestore given an engine and options diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 05d0eef0aa..48378e33ce 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -294,7 +294,8 @@ class XMLModuleStore(ModuleStoreReadBase): """ def __init__( self, data_dir, default_class=None, course_dirs=None, course_ids=None, - load_error_modules=True, i18n_service=None, fs_service=None, user_service=None, **kwargs + load_error_modules=True, i18n_service=None, fs_service=None, user_service=None, + signal_handler=None, **kwargs ): """ Initialize an XMLModuleStore from data_dir diff --git a/lms/envs/common.py b/lms/envs/common.py index 3be88b5932..530259370c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1620,6 +1620,8 @@ INSTALLED_APPS = ( 'survey', 'lms.djangoapps.lms_xblock', + + 'openedx.core.djangoapps.content.course_structures', ) ######################### MARKETING SITE ############################### diff --git a/openedx/core/djangoapps/content/__init__.py b/openedx/core/djangoapps/content/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/content/course_structures/__init__.py b/openedx/core/djangoapps/content/course_structures/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/content/course_structures/admin.py b/openedx/core/djangoapps/content/course_structures/admin.py new file mode 100644 index 0000000000..bc54f51334 --- /dev/null +++ b/openedx/core/djangoapps/content/course_structures/admin.py @@ -0,0 +1,5 @@ +from ratelimitbackend import admin + +from .models import CourseStructure + +admin.site.register(CourseStructure) diff --git a/openedx/core/djangoapps/content/course_structures/migrations/0001_initial.py b/openedx/core/djangoapps/content/course_structures/migrations/0001_initial.py new file mode 100644 index 0000000000..dda492432e --- /dev/null +++ b/openedx/core/djangoapps/content/course_structures/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CourseStructure' + db.create_table('course_structures_coursestructure', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)), + ('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)), + ('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)), + ('version', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('structure_json', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('course_structures', ['CourseStructure']) + + + def backwards(self, orm): + # Deleting model 'CourseStructure' + db.delete_table('course_structures_coursestructure') + + + models = { + 'course_structures.coursestructure': { + 'Meta': {'object_name': 'CourseStructure'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'structure_json': ('django.db.models.fields.TextField', [], {}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['course_structures'] \ No newline at end of file diff --git a/openedx/core/djangoapps/content/course_structures/migrations/__init__.py b/openedx/core/djangoapps/content/course_structures/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/content/course_structures/models.py b/openedx/core/djangoapps/content/course_structures/models.py new file mode 100644 index 0000000000..9ea48b1740 --- /dev/null +++ b/openedx/core/djangoapps/content/course_structures/models.py @@ -0,0 +1,62 @@ +import json + +from django.db import models +from django.dispatch import receiver +from celery.task import task +from model_utils.models import TimeStampedModel + +from opaque_keys.edx.keys import CourseKey + +from xmodule.modulestore.django import modulestore, SignalHandler +from xmodule_django.models import CourseKeyField + +class CourseStructure(TimeStampedModel): + + course_id = CourseKeyField(max_length=255, db_index=True) + version = models.CharField(max_length=255, blank=True, default="") + + # Right now the only thing we do with the structure doc is store it and + # send it on request. If we need to store a more complex data model later, + # we can do so and build a migration. The only problem with a normalized + # data model for this is that it will likely involve hundreds of rows, and + # we'd have to be careful about caching. + structure_json = models.TextField() + + # Index together: + # (course_id, version) + # (course_id, created) + + +def course_structure(course_key): + course = modulestore().get_course(course_key, depth=None) + blocks_stack = [course] + blocks_dict = {} + while blocks_stack: + curr_block = blocks_stack.pop() + children = curr_block.get_children() if curr_block.has_children else [] + blocks_dict[unicode(curr_block.scope_ids.usage_id)] = { + "usage_key": unicode(curr_block.scope_ids.usage_id), + "block_type": curr_block.category, + "display_name": curr_block.display_name, + "graded": curr_block.graded, + "format": curr_block.format, + "children": [unicode(ch.scope_ids.usage_id) for ch in children] + } + blocks_stack.extend(children) + return { + "root": unicode(course.scope_ids.usage_id), + "blocks": blocks_dict + } + +@receiver(SignalHandler.course_published) +def listen_for_course_publish(sender, course_key, **kwargs): + update_course_structure(course_key) + +@task() +def update_course_structure(course_key): + structure = course_structure(course_key) + CourseStructure.objects.create( + course_id=unicode(course_key), + structure_json=json.dumps(structure), + version="", + )