def has_changes(self): """ Helper method to check if this OLX bundle has any pending changes, including any deleted blocks. Returns a tuple of ( has_unpublished_changes, has_unpublished_deletes, ) Where has_unpublished_changes is true if there is any type of change, including deletes, and has_unpublished_deletes is only true if one or more blocks has been deleted since the last publish. """ if not self.draft_name: return (False, False) cached_result = self.cache.get(('has_changes', )) if cached_result is not None: return cached_result draft_files = get_bundle_files_cached(self.bundle_uuid, draft_name=self.draft_name) has_unpublished_changes = False has_unpublished_deletes = False for file_ in draft_files: if getattr(file_, 'modified', False): has_unpublished_changes = True break if not has_unpublished_changes: # Check if any links have changed: old_links = set( get_bundle_direct_links_with_cache(self.bundle_uuid).items()) new_links = set( get_bundle_direct_links_with_cache( self.bundle_uuid, draft_name=self.draft_name).items()) has_unpublished_changes = new_links != old_links published_file_paths = { f.path for f in get_bundle_files_cached(self.bundle_uuid) } draft_file_paths = {f.path for f in draft_files} for file_path in published_file_paths: if file_path not in draft_file_paths: has_unpublished_changes = True if file_path.endswith('/definition.xml'): # only set 'has_unpublished_deletes' if the actual main definition XML # file was deleted, not if only some asset file was deleted, etc. has_unpublished_deletes = True break result = (has_unpublished_changes, has_unpublished_deletes) self.cache.set(('has_changes', ), result) return result
def get_olx_files(self): """ Get the list of OLX files in this bundle (using a heuristic) Because this uses a heuristic, it will only return files with filenames that seem like OLX files that are in the expected locations of OLX files. They are not guaranteed to be valid OLX nor will OLX files in nonstandard locations be returned. Example return value: [ 'html/intro/definition.xml', 'unit/unit1/definition.xml', ] """ bundle_files = get_bundle_files_cached(self.bundle_uuid, draft_name=self.draft_name) return [f.path for f in bundle_files if f.path.endswith("/definition.xml")]
def get_static_files_for_definition(self, definition_key): """ Return a list of the static asset files related with a particular XBlock definition. If the bundle contains files like: problem/quiz1/definition.xml problem/quiz1/static/image1.png Then this will return [BundleFile(path="image1.png", size, url, hash_digest)] """ path_prefix = self.get_static_prefix_for_definition(definition_key) path_prefix_len = len(path_prefix) return [ blockstore_api.BundleFile(path=f.path[path_prefix_len:], size=f.size, url=f.url, hash_digest=f.hash_digest) for f in get_bundle_files_cached(self.bundle_uuid, draft_name=self.draft_name) if f.path.startswith(path_prefix) ]
def _get_changed_definitions(self): """ Helper method to get a list of all paths with changes, where a path is problem/quiz1/ Or similar (a type and an ID), excluding 'definition.xml' """ cached_result = self.cache.get(('changed_definition_prefixes', )) if cached_result is not None: return cached_result changed = [] bundle_files = get_bundle_files_cached(self.bundle_uuid, draft_name=self.draft_name) for file_ in bundle_files: if getattr(file_, 'modified', False) and file_.path.count('/') >= 2: (type_part, id_part, _rest) = file_.path.split('/', 2) prefix = type_part + '/' + id_part + '/' if prefix not in changed: changed.append(prefix) self.cache.set(('changed_definition_prefixes', ), changed) return changed
def get_bundle_includes(self): """ Scan through the bundle and all linked bundles as needed to generate a complete list of all the blocks that are included as child/grandchild/... blocks of the blocks in this bundle. Returns a dict of {usage_key -> BundleDefinitionLocator} Blocks in the bundle that have no parent are not included. """ cache_key = ("bundle_includes", ) usages_found = self.cache.get(cache_key) if usages_found is not None: return usages_found usages_found = {} def add_definitions_children(usage_key, def_key): """ Recursively add any children of the given XBlock usage+definition to usages_found. """ if not does_block_type_support_children(def_key.block_type): return try: xml_node = xml_for_definition(def_key) except: # pylint:disable=bare-except log.exception("Unable to load definition {}".format(def_key)) return for child in xml_node: if child.tag != 'xblock-include': continue try: parsed_include = parse_xblock_include(child) child_usage = usage_for_child_include( usage_key, def_key, parsed_include) child_def_key = definition_for_include( parsed_include, def_key) except BundleFormatException: log.exception( "Unable to parse a child of {}".format(def_key)) continue usages_found[child_usage] = child_def_key add_definitions_children(child_usage, child_def_key) # Find all the definitions in this bundle and recursively add all their descendants: bundle_files = get_bundle_files_cached(self.bundle_uuid, draft_name=self.draft_name) if self.draft_name: version_arg = {"draft_name": self.draft_name} else: version_arg = { "bundle_version": get_bundle_version_number(self.bundle_uuid) } for bfile in bundle_files: if not bfile.path.endswith( "/definition.xml") or bfile.path.count('/') != 2: continue # Not an OLX file. block_type, usage_id, _unused = bfile.path.split('/') def_key = BundleDefinitionLocator(bundle_uuid=self.bundle_uuid, block_type=block_type, olx_path=bfile.path, **version_arg) usage_key = LibraryUsageLocatorV2(self.library_key, block_type, usage_id) add_definitions_children(usage_key, def_key) self.cache.set(cache_key, usages_found) return usages_found