""" Django pipeline finder for handling static assets required by XBlocks. """ import os from datetime import datetime from django.contrib.staticfiles import utils from django.contrib.staticfiles.finders import BaseFinder from django.contrib.staticfiles.storage import FileSystemStorage from django.core.files.storage import Storage from pkg_resources import resource_exists, resource_filename, resource_isdir, resource_listdir from xblock.core import XBlock from openedx.core.lib.xblock_utils import xblock_resource_pkg class XBlockPackageStorage(Storage): """ Storage implementation for accessing XBlock package resources. """ RESOURCE_PREFIX = 'xblock/resources/' def __init__(self, module, base_dir, *args, **kwargs): """ Returns a static file storage if available in the given app. """ super(XBlockPackageStorage, self).__init__(*args, **kwargs) self.module = module self.base_dir = base_dir # Register a prefix that collectstatic will add to each path self.prefix = os.path.join(self.RESOURCE_PREFIX, module) def path(self, name): """ Returns a file system filename for the specified file name. """ return resource_filename(self.module, os.path.join(self.base_dir, name)) def exists(self, path): """ Returns True if the specified path exists. """ if self.base_dir is None: return False return resource_exists(self.module, os.path.join(self.base_dir, path)) def listdir(self, path): """ Lists the directories beneath the specified path. """ directories = [] files = [] for item in resource_listdir(self.module, os.path.join(self.base_dir, path)): __, file_extension = os.path.splitext(item) if file_extension not in [".py", ".pyc", ".scss"]: if resource_isdir(self.module, os.path.join(self.base_dir, path, item)): directories.append(item) else: files.append(item) return directories, files def open(self, name, mode='rb'): """ Retrieves the specified file from storage. """ path = self.path(name) return FileSystemStorage(path).open(path, mode) def size(self, name): """ Returns the size of the package resource. """ return os.path.getsize(self.path(name)) def accessed_time(self, name): """ Returns a URL to the package resource. """ return datetime.fromtimestamp(os.path.getatime(self.path(name))) def created_time(self, name): """ Returns the created time of the package resource. """ return datetime.fromtimestamp(os.path.getctime(self.path(name))) def modified_time(self, name): """ Returns the modified time of the resource. """ return datetime.fromtimestamp(os.path.getmtime(self.path(name))) def url(self, name): """ Note: package resources do not support URLs """ raise NotImplementedError("Package resources do not support URLs") def delete(self, name): """ Note: deleting files from a package is not supported. """ raise NotImplementedError("Deleting files from a package is not supported") class XBlockPipelineFinder(BaseFinder): """ A static files finder that gets static assets from xblocks. """ def __init__(self, *args, **kwargs): """ The XBlockPipelineFinder creates a separate XBlockPackageStorage for every installed XBlock package when its initialized. After that initialization happens, we just proxy all list()/find() requests by iterating through the XBlockPackageStorage objects. """ super(XBlockPipelineFinder, self).__init__(*args, **kwargs) # xblock_resource_info holds (package_name, resources_dir) tuples. While # it never happens in practice, the XBlock API does allow different # XBlocks installed with the same setup.py to refer to their shared # static assets using different prefixes. xblock_resource_info = { (xblock_resource_pkg(xblock_class), xblock_class.get_resources_dir()) for __, xblock_class in XBlock.load_classes() } self.package_storages = [ XBlockPackageStorage(pkg_name, resources_dir) for pkg_name, resources_dir in xblock_resource_info ] def list(self, ignore_patterns): """ List all static files in all xblock packages. """ for storage in self.package_storages: if storage.exists(''): # check if storage location exists for path in utils.get_files(storage, ignore_patterns): yield path, storage def find(self, path, all=False): # pylint: disable=redefined-builtin """ Looks for files in the xblock package directories. """ matches = [] for storage in self.package_storages: if storage.exists(path): match = storage.path(path) if not all: return match matches.append(match) return matches