Commit c656d9a4 by Calen Pennington

Allow for modular css from XModules, and split capa module css out as a test

parent bdd07a97
...@@ -27,6 +27,7 @@ import errno ...@@ -27,6 +27,7 @@ import errno
import glob2 import glob2
import lms.envs.common import lms.envs.common
import hashlib import hashlib
from collections import defaultdict
from path import path from path import path
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
...@@ -176,47 +177,78 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' ...@@ -176,47 +177,78 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
PIPELINE_CSS = { # Load javascript and css from all of the available descriptors, and
'base-style': {
'source_filenames': ['sass/base-style.scss'],
'output_filename': 'css/base-style.css',
},
}
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss']
# Load javascript from all of the available descriptors, and
# prep it for use in pipeline js # prep it for use in pipeline js
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module" js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
try: css_file_dir = PROJECT_ROOT / "static" / "sass" / "module"
os.makedirs(js_file_dir) module_styles_path = css_file_dir / "_module-styles.scss"
except OSError as exc:
for dir_ in (js_file_dir, css_file_dir):
try:
os.makedirs(dir_)
except OSError as exc:
if exc.errno == errno.EEXIST: if exc.errno == errno.EEXIST:
pass pass
else: else:
raise raise
fragments = set() js_fragments = set()
css_fragments = defaultdict(set)
for descriptor in XModuleDescriptor.load_classes() + [RawDescriptor]: for descriptor in XModuleDescriptor.load_classes() + [RawDescriptor]:
descriptor_js = descriptor.get_javascript() descriptor_js = descriptor.get_javascript()
module_js = descriptor.module_class.get_javascript() module_js = descriptor.module_class.get_javascript()
for filetype in ('coffee', 'js'): for filetype in ('coffee', 'js'):
for idx, fragment in enumerate(descriptor_js.get(filetype, []) + module_js.get(filetype, [])): for idx, fragment in enumerate(descriptor_js.get(filetype, []) + module_js.get(filetype, [])):
fragments.add((idx, filetype, fragment)) js_fragments.add((idx, filetype, fragment))
for class_ in (descriptor, descriptor.module_class):
fragments = class_.get_css()
for filetype in ('sass', 'scss', 'css'):
for idx, fragment in enumerate(fragments.get(filetype, [])):
css_fragments[idx, filetype, fragment].add(class_.__name__)
module_js_sources = [] module_js_sources = []
for idx, filetype, fragment in sorted(fragments): for idx, filetype, fragment in sorted(js_fragments):
path = os.path.join(js_file_dir, "{idx}-{hash}.{type}".format( path = js_file_dir / "{idx}-{hash}.{type}".format(
idx=idx, idx=idx,
hash=hashlib.md5(fragment).hexdigest(), hash=hashlib.md5(fragment).hexdigest(),
type=filetype)) type=filetype)
with open(path, 'w') as js_file: with open(path, 'w') as js_file:
js_file.write(fragment) js_file.write(fragment)
module_js_sources.append(path.replace(PROJECT_ROOT / "static/", "")) module_js_sources.append(path.replace(PROJECT_ROOT / "static/", ""))
css_imports = defaultdict(set)
for (idx, filetype, fragment), classes in sorted(css_fragments.items()):
fragment_name = "{idx}-{hash}.{type}".format(
idx=idx,
hash=hashlib.md5(fragment).hexdigest(),
type=filetype)
# Prepend _ so that sass just includes the files into a single file
with open(css_file_dir / '_' + fragment_name, 'w') as js_file:
js_file.write(fragment)
for class_ in classes:
css_imports[class_].add(fragment_name)
with open(module_styles_path, 'w') as module_styles:
for class_, fragment_names in css_imports.items():
imports = "\n".join('@import "{0}";'.format(name) for name in fragment_names)
module_styles.write(""".xmodule_{class_} {{ {imports} }}""".format(
class_=class_, imports=imports
))
PIPELINE_CSS = {
'base-style': {
'source_filenames': ['sass/base-style.scss'],
'output_filename': 'css/base-style.css',
},
}
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss']
PIPELINE_JS = { PIPELINE_JS = {
'main': { 'main': {
'source_filenames': [ 'source_filenames': [
......
...@@ -4,3 +4,5 @@ ...@@ -4,3 +4,5 @@
@import 'base', 'layout', 'content-types'; @import 'base', 'layout', 'content-types';
@import 'calendar'; @import 'calendar';
@import 'section', 'unit'; @import 'section', 'unit';
@import 'module/module-styles.scss';
...@@ -14,6 +14,7 @@ def wrap_xmodule(get_html, module, template): ...@@ -14,6 +14,7 @@ def wrap_xmodule(get_html, module, template):
module: An XModule module: An XModule
template: A template that takes the variables: template: A template that takes the variables:
content: the results of get_html, content: the results of get_html,
class_: the module class name
module_name: the js_module_name of the module module_name: the js_module_name of the module
""" """
...@@ -21,6 +22,7 @@ def wrap_xmodule(get_html, module, template): ...@@ -21,6 +22,7 @@ def wrap_xmodule(get_html, module, template):
def _get_html(): def _get_html():
return render_to_string(template, { return render_to_string(template, {
'content': get_html(), 'content': get_html(),
'class_': module.__class__.__name__,
'module_name': module.js_module_name 'module_name': module.js_module_name
}) })
return _get_html return _get_html
......
...@@ -75,6 +75,7 @@ class CapaModule(XModule): ...@@ -75,6 +75,7 @@ class CapaModule(XModule):
'js': [resource_string(__name__, 'js/src/capa/imageinput.js'), 'js': [resource_string(__name__, 'js/src/capa/imageinput.js'),
resource_string(__name__, 'js/src/capa/schematic.js')]} resource_string(__name__, 'js/src/capa/schematic.js')]}
js_module_name = "Problem" js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
......
section.problem-set { h2 {
position: relative; margin-top: 0;
@extend .clearfix; margin-bottom: 15px;
width: flex-grid(2, 9);
h2 { padding-right: flex-gutter(9);
margin-top: 0; border-right: 1px dashed #ddd;
margin-bottom: 15px; @include box-sizing(border-box);
width: flex-grid(2, 9); display: table-cell;
padding-right: flex-gutter(9); vertical-align: top;
border-right: 1px dashed #ddd;
@include box-sizing(border-box); &.problem-header {
display: table-cell;
vertical-align: top;
&.problem-header {
section.staff { section.staff {
margin-top: 30px; margin-top: 30px;
font-size: 80%; font-size: 80%;
} }
} }
@media screen and (max-width:1120px) { @media screen and (max-width:1120px) {
display: block; display: block;
width: auto; width: auto;
border-right: 0; border-right: 0;
} }
@media print { @media print {
display: block; display: block;
width: auto; width: auto;
border-right: 0; border-right: 0;
} }
} }
section.problem { section.problem {
display: table-cell; display: table-cell;
width: flex-grid(7, 9); width: flex-grid(7, 9);
padding-left: flex-gutter(9); padding-left: flex-gutter(9);
@media screen and (max-width:1120px) { @media screen and (max-width:1120px) {
display: block; display: block;
width: auto; width: auto;
padding: 0; padding: 0;
} }
@media print { @media print {
display: block; display: block;
width: auto; width: auto;
padding: 0; padding: 0;
...@@ -51,9 +47,9 @@ section.problem-set { ...@@ -51,9 +47,9 @@ section.problem-set {
canvas, img { canvas, img {
page-break-inside: avoid; page-break-inside: avoid;
} }
} }
div { div {
p.status { p.status {
text-indent: -9999px; text-indent: -9999px;
margin: -1px 0 0 10px; margin: -1px 0 0 10px;
...@@ -156,53 +152,53 @@ section.problem-set { ...@@ -156,53 +152,53 @@ section.problem-set {
top: 6px; top: 6px;
} }
} }
} }
ul { ul {
list-style: disc outside none; list-style: disc outside none;
margin-bottom: lh(); margin-bottom: lh();
margin-left: .75em; margin-left: .75em;
margin-left: .75rem; margin-left: .75rem;
} }
ol { ol {
list-style: decimal outside none; list-style: decimal outside none;
margin-bottom: lh(); margin-bottom: lh();
margin-left: .75em; margin-left: .75em;
margin-left: .75rem; margin-left: .75rem;
} }
dl { dl {
line-height: 1.4em; line-height: 1.4em;
} }
dl dt { dl dt {
font-weight: bold; font-weight: bold;
} }
dl dd { dl dd {
margin-bottom: 0; margin-bottom: 0;
} }
dd { dd {
margin-left: .5em; margin-left: .5em;
margin-left: .5rem; margin-left: .5rem;
} }
li { li {
line-height: 1.4em; line-height: 1.4em;
margin-bottom: lh(.5); margin-bottom: lh(.5);
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
p { p {
margin-bottom: lh(); margin-bottom: lh();
} }
table { table {
margin-bottom: lh(); margin-bottom: lh();
width: 100%; width: 100%;
// border: 1px solid #ddd; // border: 1px solid #ddd;
...@@ -235,9 +231,9 @@ section.problem-set { ...@@ -235,9 +231,9 @@ section.problem-set {
vertical-align: middle; vertical-align: middle;
} }
} }
hr { hr {
background: #ddd; background: #ddd;
border: none; border: none;
clear: both; clear: both;
...@@ -246,47 +242,23 @@ section.problem-set { ...@@ -246,47 +242,23 @@ section.problem-set {
height: 1px; height: 1px;
margin: 0 0 .75rem; margin: 0 0 .75rem;
width: 100%; width: 100%;
} }
.hidden { .hidden {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
#{$all-text-inputs} { #{$all-text-inputs} {
display: inline; display: inline;
width: auto; width: auto;
} }
// this supports a deprecated element and should be removed once the center tag is removed // this supports a deprecated element and should be removed once the center tag is removed
center { center {
display: block; display: block;
margin: lh() 0; margin: lh() 0;
border: 1px solid #ccc; border: 1px solid #ccc;
padding: lh(); padding: lh();
}
}
section.action {
margin-top: lh(.5);
input[type="button"] {
padding: lh(.4) lh();
text-shadow: 0 -1px 0 #666;
}
}
}
section.problems-wrapper, div#seq_content {
@extend .problem-set;
} }
section.problems-wrapper {
display: table;
width: 100%;
@media screen and (max-width:1120px) {
display: block;
width: auto;
}
} }
...@@ -64,7 +64,50 @@ class Plugin(object): ...@@ -64,7 +64,50 @@ class Plugin(object):
in pkg_resources.iter_entry_points(cls.entry_point)] in pkg_resources.iter_entry_points(cls.entry_point)]
class XModule(object): class HTMLSnippet(object):
"""
A base class defining an interface for an object that is able to present an
html snippet, along with associated javascript and css
"""
js = {}
js_module_name = None
css = {}
@classmethod
def get_javascript(cls):
"""
Return a dictionary containing some of the following keys:
coffee: A list of coffeescript fragments that should be compiled and
placed on the page
js: A list of javascript fragments that should be included on the page
All of these will be loaded onto the page in the CMS
"""
return cls.js
@classmethod
def get_css(cls):
"""
Return a dictionary containing some of the following keys:
css: A list of css fragments that should be applied to the html contents
of the snippet
sass: A list of sass fragments that should be applied to the html contents
of the snippet
scss: A list of scss fragments that should be applied to the html contents
of the snippet
"""
return cls.css
def get_html(self):
"""
Return the html used to edit this module
"""
raise NotImplementedError("get_html() must be provided by specific modules")
class XModule(HTMLSnippet):
''' Implements a generic learning module. ''' Implements a generic learning module.
Subclasses must at a minimum provide a definition for get_html in order to be displayed to users. Subclasses must at a minimum provide a definition for get_html in order to be displayed to users.
...@@ -77,9 +120,6 @@ class XModule(object): ...@@ -77,9 +120,6 @@ class XModule(object):
# if the icon class depends on the data in the module # if the icon class depends on the data in the module
icon_class = 'other' icon_class = 'other'
js = {}
js_module_name = None
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
''' '''
Construct a new xmodule Construct a new xmodule
...@@ -196,28 +236,8 @@ class XModule(object): ...@@ -196,28 +236,8 @@ class XModule(object):
get is a dictionary-like object ''' get is a dictionary-like object '''
return "" return ""
# ================================== HTML INTERFACE DEFINITIONS ======================
@classmethod
def get_javascript(cls):
"""
Return a dictionary containing some of the following keys:
coffee: A list of coffeescript fragments that should be compiled and
placed on the page
js: A list of javascript fragments that should be included on the page
All of these will be loaded onto the page in the LMS
"""
return cls.js
def get_html(self):
''' HTML, as shown in the browser. This is the only method that must be implemented
'''
raise NotImplementedError("get_html must be defined for all XModules that appear on the screen. Not defined in %s" % self.__class__.__name__)
class XModuleDescriptor(Plugin, HTMLSnippet):
class XModuleDescriptor(Plugin):
""" """
An XModuleDescriptor is a specification for an element of a course. This could An XModuleDescriptor is a specification for an element of a course. This could
be a problem, an organizational element (a group of content), or a segment of video, be a problem, an organizational element (a group of content), or a segment of video,
...@@ -228,8 +248,6 @@ class XModuleDescriptor(Plugin): ...@@ -228,8 +248,6 @@ class XModuleDescriptor(Plugin):
and can generate XModules (which do know about student state). and can generate XModules (which do know about student state).
""" """
entry_point = "xmodule.v1" entry_point = "xmodule.v1"
js = {}
js_module_name = None
module_class = XModule module_class = XModule
# A list of metadata that this module can inherit from its parent module # A list of metadata that this module can inherit from its parent module
...@@ -404,25 +422,6 @@ class XModuleDescriptor(Plugin): ...@@ -404,25 +422,6 @@ class XModuleDescriptor(Plugin):
""" """
raise NotImplementedError('Modules must implement export_to_xml to enable xml export') raise NotImplementedError('Modules must implement export_to_xml to enable xml export')
# ================================== HTML INTERFACE DEFINITIONS ======================
@classmethod
def get_javascript(cls):
"""
Return a dictionary containing some of the following keys:
coffee: A list of coffeescript fragments that should be compiled and
placed on the page
js: A list of javascript fragments that should be included on the page
All of these will be loaded onto the page in the CMS
"""
return cls.js
def get_html(self):
"""
Return the html used to edit this module
"""
raise NotImplementedError("get_html() must be provided by specific modules")
# =============================== Testing =================================== # =============================== Testing ===================================
def get_sample_state(self): def get_sample_state(self):
""" """
......
<section class="xmodule_display" data-type="${module_name}"> <section class="xmodule_display xmodule_${class_}" data-type="${module_name}">
${content} ${content}
</section> </section>
<section class="xmodule_edit" data-type="${module_name}"> <section class="xmodule_edit xmodule_${class_}" data-type="${module_name}">
${content} ${content}
</section> </section>
...@@ -24,6 +24,7 @@ import tempfile ...@@ -24,6 +24,7 @@ import tempfile
import glob2 import glob2
import errno import errno
import hashlib import hashlib
from collections import defaultdict
import djcelery import djcelery
from path import path from path import path
...@@ -336,31 +337,60 @@ main_vendor_js = [ ...@@ -336,31 +337,60 @@ main_vendor_js = [
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.hidden_module import HiddenDescriptor from xmodule.hidden_module import HiddenDescriptor
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module" js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
try: css_file_dir = PROJECT_ROOT / "static" / "sass" / "module"
os.makedirs(js_file_dir) module_styles_path = css_file_dir / "_module-styles.scss"
except OSError as exc:
for dir_ in (js_file_dir, css_file_dir):
try:
os.makedirs(dir_)
except OSError as exc:
if exc.errno == errno.EEXIST: if exc.errno == errno.EEXIST:
pass pass
else: else:
raise raise
fragments = set() js_fragments = set()
css_fragments = defaultdict(set)
for descriptor in XModuleDescriptor.load_classes() + [HiddenDescriptor]: for descriptor in XModuleDescriptor.load_classes() + [HiddenDescriptor]:
module_js = descriptor.module_class.get_javascript() module_js = descriptor.module_class.get_javascript()
for filetype in ('coffee', 'js'): for filetype in ('coffee', 'js'):
for idx, fragment in enumerate(module_js.get(filetype, [])): for idx, fragment in enumerate(module_js.get(filetype, [])):
fragments.add((idx, filetype, fragment)) js_fragments.add((idx, filetype, fragment))
module_css = descriptor.module_class.get_css()
for filetype in ('sass', 'scss', 'css'):
for idx, fragment in enumerate(module_css.get(filetype, [])):
css_fragments[idx, filetype, fragment].add(descriptor.module_class.__name__)
module_js_sources = [] module_js_sources = []
for idx, filetype, fragment in sorted(fragments): for idx, filetype, fragment in sorted(js_fragments):
path = os.path.join(js_file_dir, "{idx}-{hash}.{type}".format( path = js_file_dir / "{idx}-{hash}.{type}".format(
idx=idx, idx=idx,
hash=hashlib.md5(fragment).hexdigest(), hash=hashlib.md5(fragment).hexdigest(),
type=filetype)) type=filetype)
with open(path, 'w') as js_file: with open(path, 'w') as js_file:
js_file.write(fragment) js_file.write(fragment)
module_js_sources.append(path.replace(PROJECT_ROOT / "static/", "")) module_js_sources.append(path.replace(PROJECT_ROOT / "static/", ""))
css_imports = defaultdict(set)
for (idx, filetype, fragment), classes in sorted(css_fragments.items()):
fragment_name = "{idx}-{hash}.{type}".format(
idx=idx,
hash=hashlib.md5(fragment).hexdigest(),
type=filetype)
# Prepend _ so that sass just includes the files into a single file
with open(css_file_dir / '_' + fragment_name, 'w') as js_file:
js_file.write(fragment)
for class_ in classes:
css_imports[class_].add(fragment_name)
with open(module_styles_path, 'w') as module_styles:
for class_, fragment_names in css_imports.items():
imports = "\n".join('@import "{0}";'.format(name) for name in fragment_names)
module_styles.write(""".xmodule_{class_} {{ {imports} }}""".format(
class_=class_, imports=imports
))
PIPELINE_JS = { PIPELINE_JS = {
'application': { 'application': {
......
...@@ -20,4 +20,5 @@ ...@@ -20,4 +20,5 @@
@import 'course/old/courseware/sidebar'; @import 'course/old/courseware/sidebar';
@import 'course/old/courseware/video'; @import 'course/old/courseware/video';
@import 'course/old/courseware/amplifier'; @import 'course/old/courseware/amplifier';
@import 'course/old/courseware/problems';
@import 'module/module-styles.scss';
...@@ -49,142 +49,6 @@ div.course-wrapper { ...@@ -49,142 +49,6 @@ div.course-wrapper {
} }
} }
.problem-set {
position: relative;
@extend .clearfix;
h2 {
margin-top: 0;
margin-bottom: 15px;
width: flex-grid(2, 9);
padding-right: flex-gutter(9);
border-right: 1px dashed #ddd;
@include box-sizing(border-box);
display: table-cell;
vertical-align: top;
&.problem-header {
section.staff {
margin-top: 30px;
font-size: 80%;
}
}
@media screen and (max-width:1120px) {
display: block;
width: auto;
border-right: 0;
}
@media print {
display: block;
width: auto;
border-right: 0;
}
}
section.problem {
display: table-cell;
width: flex-grid(7, 9);
padding-left: flex-gutter(9);
@media screen and (max-width:1120px) {
display: block;
width: auto;
padding: 0;
}
@media print {
display: block;
width: auto;
padding: 0;
canvas, img {
page-break-inside: avoid;
}
}
span {
&.unanswered, &.ui-icon-bullet {
@include inline-block();
background: url('../images/unanswered-icon.png') center center no-repeat;
height: 14px;
position: relative;
top: 4px;
width: 14px;
}
&.correct, &.ui-icon-check {
@include inline-block();
background: url('../images/correct-icon.png') center center no-repeat;
height: 20px;
position: relative;
top: 6px;
width: 25px;
}
&.incorrect, &.ui-icon-close {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
width: 20px;
position: relative;
top: 6px;
}
}
}
div {
> span {
display: block;
margin-bottom: lh(.5);
&[answer] {
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
background: #f3f3f3;
margin: 0 (-(lh()));
padding: lh(.5) lh();
}
}
}
input[type="text"] {
display: inline-block;
width: 50%;
}
center {
display: block;
margin: lh() 0;
border: 1px solid #ccc;
padding: lh();
}
section.action {
margin-top: lh();
input[type="button"] {
padding: lh(.4) lh();
text-shadow: 0 -1px 0 #666;
}
}
}
section.problems-wrapper, div#seq_content {
@extend .problem-set;
}
section.problems-wrapper {
display: table;
width: 100%;
@media screen and (max-width:1120px) {
display: block;
width: auto;
}
}
div#seq_content { div#seq_content {
h1 { h1 {
background: none; background: none;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment