-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Modulestore Support for Content libraries (Part of SOL-1, SOL-2, SOL-3) #6033
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """ | ||
| 'library' XBlock (LibraryRoot) | ||
| """ | ||
| import logging | ||
|
|
||
| from .studio_editable import StudioEditableModule | ||
| from xblock.core import XBlock | ||
| from xblock.fields import Scope, String, List | ||
| from xblock.fragment import Fragment | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
| # Make '_' a no-op so we can scrape strings | ||
| _ = lambda text: text | ||
|
|
||
|
|
||
| class LibraryRoot(XBlock): | ||
| """ | ||
| The LibraryRoot is the root XBlock of a content library. All other blocks in | ||
| the library are its children. It contains metadata such as the library's | ||
| display_name. | ||
| """ | ||
| display_name = String( | ||
| help=_("Enter the name of the library as it should appear in Studio."), | ||
| default="Library", | ||
| display_name=_("Library Display Name"), | ||
| scope=Scope.settings | ||
| ) | ||
| advanced_modules = List( | ||
| display_name=_("Advanced Module List"), | ||
| help=_("Enter the names of the advanced components to use in your library."), | ||
| scope=Scope.settings | ||
| ) | ||
| has_children = True | ||
| has_author_view = True | ||
|
|
||
| def __unicode__(self): | ||
| return u"Library: {}".format(self.display_name) | ||
|
|
||
| def __str__(self): | ||
| return unicode(self).encode('utf-8') | ||
|
|
||
| def author_view(self, context): | ||
| """ | ||
| Renders the Studio preview view, which supports drag and drop. | ||
| """ | ||
| fragment = Fragment() | ||
| contents = [] | ||
|
|
||
| for child_key in self.children: # pylint: disable=E1101 | ||
| context['reorderable_items'].add(child_key) | ||
| child = self.runtime.get_block(child_key) | ||
| rendered_child = self.runtime.render_child(child, StudioEditableModule.get_preview_view_name(child), context) | ||
| fragment.add_frag_resources(rendered_child) | ||
|
|
||
| contents.append({ | ||
| 'id': unicode(child_key), | ||
| 'content': rendered_child.content, | ||
| }) | ||
|
|
||
| fragment.add_content(self.runtime.render_template("studio_render_children_view.html", { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As an XBlock, this really shouldn't be using
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cpennington this method will be changed in follow up PRs, so I would suggest leaving it as is for now, as it works and to be changed soon. If you are convinced it should be done in scope of this PR, what's the recommended way for rendering templates in XBlocks than? I believe there're no other XBlocks in edx-platform repo (except for pending review Discussion XBlock), so some hints on what's allowed and what's not would be appreciated :)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're going to be working on it soon, then it's fine to leave as-is for now.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, lean towards not using anything out of For rendering in particular, you can just use the Mako library directly, rather than using it via |
||
| 'items': contents, | ||
| 'xblock_context': context, | ||
| 'can_add': True, | ||
| 'can_reorder': True, | ||
| })) | ||
| return fragment | ||
|
|
||
| @property | ||
| def display_org_with_default(self): | ||
| """ | ||
| Org display names are not implemented. This just provides API compatibility with CourseDescriptor. | ||
| Always returns the raw 'org' field from the key. | ||
| """ | ||
| return self.scope_ids.usage_id.course_key.org | ||
|
|
||
| @property | ||
| def display_number_with_default(self): | ||
| """ | ||
| Display numbers are not implemented. This just provides API compatibility with CourseDescriptor. | ||
| Always returns the raw 'library' field from the key. | ||
| """ | ||
| return self.scope_ids.usage_id.course_key.library | ||
|
|
||
| @classmethod | ||
| def parse_xml(cls, xml_data, system, id_generator, **kwargs): | ||
| """ XML support not yet implemented. """ | ||
| raise NotImplementedError | ||
|
|
||
| def add_xml_to_node(self, resource_fs): | ||
| """ XML support not yet implemented. """ | ||
| raise NotImplementedError | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |
|
|
||
| from opaque_keys import InvalidKeyError | ||
| from opaque_keys.edx.keys import CourseKey, AssetKey | ||
| from opaque_keys.edx.locator import LibraryLocator | ||
| from opaque_keys.edx.locations import SlashSeparatedCourseKey | ||
| from xmodule.assetstore import AssetMetadata | ||
|
|
||
|
|
@@ -25,6 +26,7 @@ | |
| new_contract('CourseKey', CourseKey) | ||
| new_contract('AssetKey', AssetKey) | ||
| new_contract('AssetMetadata', AssetMetadata) | ||
| new_contract('LibraryLocator', LibraryLocator) | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -259,6 +261,23 @@ def get_courses(self, **kwargs): | |
| courses[course_id] = course | ||
| return courses.values() | ||
|
|
||
| @strip_key | ||
| def get_libraries(self, **kwargs): | ||
| """ | ||
| Returns a list containing the top level XBlock of the libraries (LibraryRoot) in this modulestore. | ||
| """ | ||
| libraries = {} | ||
| for store in self.modulestores: | ||
| if not hasattr(store, 'get_libraries'): | ||
| continue | ||
| # filter out ones which were fetched from earlier stores but locations may not be == | ||
| for course in store.get_libraries(**kwargs): | ||
| course_id = self._clean_course_id_for_mapping(course.location) | ||
| if course_id not in libraries: | ||
| # course is indeed unique. save it in result | ||
| libraries[course_id] = course | ||
| return libraries.values() | ||
|
|
||
| def make_course_key(self, org, course, run): | ||
| """ | ||
| Return a valid :class:`~opaque_keys.edx.keys.CourseKey` for this modulestore | ||
|
|
@@ -290,6 +309,24 @@ def get_course(self, course_key, depth=0, **kwargs): | |
| except ItemNotFoundError: | ||
| return None | ||
|
|
||
| @strip_key | ||
| @contract(library_key='LibraryLocator') | ||
| def get_library(self, library_key, depth=0, **kwargs): | ||
| """ | ||
| returns the library block associated with the given key. If no such library exists, | ||
| it returns None | ||
|
|
||
| :param library_key: must be a LibraryLocator | ||
| """ | ||
| try: | ||
| store = self._verify_modulestore_support(library_key, 'get_library') | ||
| return store.get_library(library_key, depth=depth, **kwargs) | ||
| except NotImplementedError: | ||
| log.exception("Modulestore configured for %s does not have get_library method", library_key) | ||
| return None | ||
| except ItemNotFoundError: | ||
| return None | ||
|
|
||
| @strip_key | ||
| def has_course(self, course_id, ignore_case=False, **kwargs): | ||
| """ | ||
|
|
@@ -507,6 +544,34 @@ def create_course(self, org, course, run, user_id, **kwargs): | |
|
|
||
| return course | ||
|
|
||
| @strip_key | ||
| def create_library(self, org, library, user_id, fields, **kwargs): | ||
| """ | ||
| Creates and returns a new library. | ||
|
|
||
| Args: | ||
| org (str): the organization that owns the course | ||
| library (str): the code/number/name of the library | ||
| user_id: id of the user creating the course | ||
| fields (dict): Fields to set on the course at initialization - e.g. display_name | ||
| kwargs: Any optional arguments understood by a subset of modulestores to customize instantiation | ||
|
|
||
| Returns: a LibraryRoot | ||
| """ | ||
| # first make sure an existing course/lib doesn't already exist in the mapping | ||
| lib_key = LibraryLocator(org=org, library=library) | ||
| if lib_key in self.mappings: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps do a case-insensitive check to ensure we don't have libraries whose only distinction is case.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dmitchell I believe this shouldn't be done as the proper way to do this is to add overloaded One more consideration -
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a difference between saying a locator is case-insensitive and saying that we don't allow the user to create something whose case insensitive id == another thing's. We are, however, discussing whether all locators should be case-insensitive. |
||
| raise DuplicateCourseError(lib_key, lib_key) | ||
|
|
||
| # create the library | ||
| store = self._verify_modulestore_support(None, 'create_library') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, this leads me to wonder whether we should change the definition of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dmitchell it makes some sense, but I would suggest creating separate method for that, rather than adding another mode to So, my suggestion is that it's possible to do so, but it would better be a separate method (e.g. Are you ok with that?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dmitchell just to clarify it - as of now, we're not implementing this as part of Content Libraries effort. Please let me or @antoviaque know if it's required/needed so that we schedule this task for implementation in (near?) future. |
||
| library = store.create_library(org, library, user_id, fields, **kwargs) | ||
|
|
||
| # add new library to the mapping | ||
| self.mappings[lib_key] = store | ||
|
|
||
| return library | ||
|
|
||
| @strip_key | ||
| def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs): | ||
| """ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's too bad that you have to do
StudioEditableModule.get_preview_view_name, rather than just having a defaultauthor_viewthat simply calls thestudent_view, so that Studio could just always renderauthor_view.@andy-armstrong @cahrens: Future enhancement?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was struggling with how to "mix in" special Studio editing capabilities without overriding custom xblock implementations. I'd love us to reconcile the various semi-documented view methods (student_view, author_view, studio_view, new view TBD for default editing support of xblocks).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cpennington I believe this note is not actionable now, at least on my side. Am I right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's right.