Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2026,6 +2026,16 @@
# .. toggle_creation_date: 2016-06-30
ENABLE_COMPREHENSIVE_THEMING = False

# .. setting_name: CUSTOM_RESOURCE_TEMPLATES_DIRECTORY
# .. setting_default: None
# .. setting_description: Path to an existing directory of YAML files containing
# html content to be used with the subclasses of xmodule.x_module.ResourceTemplates.
# Default example templates can be found in xmodule/templates/html.
# Note that the extension used is ".yaml" and not ".yml".
# See xmodule.x_module.ResourceTemplates for usage.
# "CUSTOM_RESOURCE_TEMPLATES_DIRECTORY" : null
CUSTOM_RESOURCE_TEMPLATES_DIRECTORY = None

############################ Global Database Configuration #####################

DATABASE_ROUTERS = [
Expand Down
31 changes: 29 additions & 2 deletions common/lib/xmodule/xmodule/tests/test_resource_templates.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""
Tests for xmodule.x_module.ResourceTemplates
"""


import pathlib
import unittest

from django.test import override_settings
from xmodule.x_module import ResourceTemplates

CUSTOM_RESOURCE_TEMPLATES_DIRECTORY = pathlib.Path(__file__).parent.parent / "templates/"


class ResourceTemplatesTests(unittest.TestCase):
"""
Expand All @@ -28,6 +30,20 @@ def test_templates_no_suchdir(self):
def test_get_template(self):
assert TestClass.get_template('latex_html.yaml')['template_id'] == 'latex_html.yaml'

@override_settings(CUSTOM_RESOURCE_TEMPLATES_DIRECTORY=CUSTOM_RESOURCE_TEMPLATES_DIRECTORY)
def test_get_custom_template(self):
assert TestClassResourceTemplate.get_template('latex_html.yaml')['template_id'] == 'latex_html.yaml'

@override_settings(CUSTOM_RESOURCE_TEMPLATES_DIRECTORY=CUSTOM_RESOURCE_TEMPLATES_DIRECTORY)
def test_custom_templates(self):
expected = {
'latex_html.yaml',
'zooming_image.yaml',
'announcement.yaml',
'anon_user_id.yaml'}
got = {t['template_id'] for t in TestClassResourceTemplate.templates()}
assert expected == got


class TestClass(ResourceTemplates):
"""
Expand Down Expand Up @@ -55,3 +71,14 @@ class TestClass2(TestClass):
@classmethod
def get_template_dir(cls):
return 'foo'


class TestClassResourceTemplate(ResourceTemplates):
"""
Like TestClass, but `template_packages` contains a module that doesn't
have any templates.

See `TestClass`.
"""
template_packages = ['capa.checker']
template_dir_name = 'test'
119 changes: 90 additions & 29 deletions common/lib/xmodule/xmodule/x_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@

import yaml

from django.conf import settings
from lazy import lazy
from lxml import etree
from opaque_keys.edx.asides import AsideDefinitionKeyV2, AsideUsageKeyV2
from opaque_keys.edx.keys import UsageKey
from pkg_resources import resource_exists, resource_isdir, resource_listdir, resource_string
from pkg_resources import resource_isdir, resource_string, resource_filename
from web_fragments.fragment import Fragment
from webob import Response
from webob.multidict import MultiDict
Expand Down Expand Up @@ -1031,35 +1032,64 @@ def policy_key(location):

class ResourceTemplates:
"""
Gets the templates associated w/ a containing cls. The cls must have a 'template_dir_name' attribute.
It finds the templates as directly in this directory under 'templates'.
Gets the yaml templates associated with a containing cls for display in the Studio.

The cls must have a 'template_dir_name' attribute. It finds the templates as directly
in this directory under 'templates'.

Additional templates can be loaded by setting the
CUSTOM_RESOURCE_TEMPLATES_DIRECTORY configuration setting.

Note that a template must end with ".yaml" extension otherwise it will not be
loaded.
"""
template_packages = [__name__]

@classmethod
def _load_template(cls, template_path, template_id):
"""
Reads an loads the yaml content provided in the template_path and
return the content as a dictionary.
"""
if not os.path.exists(template_path):
return None

with open(template_path) as file_object:
template = yaml.safe_load(file_object)
template['template_id'] = template_id
return template

@classmethod
def _load_templates_in_dir(cls, dirpath):
"""
Lists every resource template found in the provided dirpath.
"""
templates = []
for template_file in os.listdir(dirpath):
if not template_file.endswith('.yaml'):
log.warning("Skipping unknown template file %s", template_file)
continue

template = cls._load_template(os.path.join(dirpath, template_file), template_file)
templates.append(template)
return templates

@classmethod
def templates(cls):
"""
Returns a list of dictionary field: value objects that describe possible templates that can be used
to seed a module of this type.

Expects a class attribute template_dir_name that defines the directory
inside the 'templates' resource directory to pull templates from
inside the 'templates' resource directory to pull templates from.
"""
templates = []
dirname = cls.get_template_dir()
if dirname is not None:
for pkg in cls.template_packages:
if not resource_isdir(pkg, dirname):
continue
for template_file in resource_listdir(pkg, dirname):
if not template_file.endswith('.yaml'):
log.warning("Skipping unknown template file %s", template_file)
continue
template_content = resource_string(pkg, os.path.join(dirname, template_file))
template = yaml.safe_load(template_content)
template['template_id'] = template_file
templates.append(template)
return templates
templates = {}

for dirpath in cls.get_template_dirpaths():
for template in cls._load_templates_in_dir(dirpath):
templates[template['template_id']] = template

return list(templates.values())

@classmethod
def get_template_dir(cls): # lint-amnesty, pylint: disable=missing-function-docstring
Expand All @@ -1076,22 +1106,53 @@ def get_template_dir(cls): # lint-amnesty, pylint: disable=missing-function-doc
else:
return None

@classmethod
def get_template_dirpaths(cls):
"""
Returns of list of directories containing resource templates.
"""
template_dirpaths = []
template_dirname = cls.get_template_dir()
if template_dirname and resource_isdir(__name__, template_dirname):
template_dirpaths.append(resource_filename(__name__, template_dirname))

custom_template_dir = cls.get_custom_template_dir()
if custom_template_dir:
template_dirpaths.append(custom_template_dir)
return template_dirpaths

@classmethod
def get_custom_template_dir(cls):
"""
If settings.CUSTOM_RESOURCE_TEMPLATES_DIRECTORY is defined, check if it has a
subdirectory named as the class's template_dir_name and return the full path.
"""
template_dir_name = getattr(cls, 'template_dir_name', None)

if template_dir_name is None:
return

resource_dir = settings.CUSTOM_RESOURCE_TEMPLATES_DIRECTORY

if not resource_dir:
return None

template_dir_path = os.path.join(resource_dir, template_dir_name)

if os.path.exists(template_dir_path):
return template_dir_path
return None

@classmethod
def get_template(cls, template_id):
"""
Get a single template by the given id (which is the file name identifying it w/in the class's
template_dir_name)

"""
dirname = cls.get_template_dir()
if dirname is not None:
path = os.path.join(dirname, template_id)
for pkg in cls.template_packages:
if resource_exists(pkg, path):
template_content = resource_string(pkg, path)
template = yaml.safe_load(template_content)
template['template_id'] = template_id
return template
for directory in sorted(cls.get_template_dirpaths(), reverse=True):
abs_path = os.path.join(directory, template_id)
if os.path.exists(abs_path):
return cls._load_template(abs_path, template_id)


class XModuleDescriptorToXBlockMixin:
Expand Down
10 changes: 10 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4377,6 +4377,16 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# .. toggle_creation_date: 2016-06-30
ENABLE_COMPREHENSIVE_THEMING = False

# .. setting_name: CUSTOM_RESOURCE_TEMPLATES_DIRECTORY
# .. setting_default: None
# .. setting_description: Path to an existing directory of YAML files containing
# html content to be used with the subclasses of xmodule.x_module.ResourceTemplates.
# Default example templates can be found in xmodule/templates/html.
# Note that the extension used is ".yaml" and not ".yml".
# See xmodule.x_module.ResourceTemplates for usage.
# "CUSTOM_RESOURCE_TEMPLATES_DIRECTORY" : null
CUSTOM_RESOURCE_TEMPLATES_DIRECTORY = None

# API access management
API_ACCESS_MANAGER_EMAIL = 'api-access@example.com'
API_ACCESS_FROM_EMAIL = 'api-requests@example.com'
Expand Down