Skip to content
Snippets Groups Projects
Commit 8a0270ea authored by Calen Pennington's avatar Calen Pennington
Browse files

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

parent 0a79130b
No related merge requests found
Showing with 418 additions and 513 deletions
......@@ -27,6 +27,7 @@ import errno
import glob2
import lms.envs.common
import hashlib
from collections import defaultdict
from path import path
############################ FEATURE CONFIGURATION #############################
......@@ -176,47 +177,78 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
PIPELINE_CSS = {
'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
# Load javascript and css from all of the available descriptors, and
# prep it for use in pipeline js
from xmodule.x_module import XModuleDescriptor
from xmodule.raw_module import RawDescriptor
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
try:
os.makedirs(js_file_dir)
except OSError as exc:
if exc.errno == errno.EEXIST:
pass
else:
raise
fragments = set()
css_file_dir = PROJECT_ROOT / "static" / "sass" / "module"
module_styles_path = css_file_dir / "_module-styles.scss"
for dir_ in (js_file_dir, css_file_dir):
try:
os.makedirs(dir_)
except OSError as exc:
if exc.errno == errno.EEXIST:
pass
else:
raise
js_fragments = set()
css_fragments = defaultdict(set)
for descriptor in XModuleDescriptor.load_classes() + [RawDescriptor]:
descriptor_js = descriptor.get_javascript()
module_js = descriptor.module_class.get_javascript()
for filetype in ('coffee', 'js'):
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 = []
for idx, filetype, fragment in sorted(fragments):
path = os.path.join(js_file_dir, "{idx}-{hash}.{type}".format(
for idx, filetype, fragment in sorted(js_fragments):
path = js_file_dir / "{idx}-{hash}.{type}".format(
idx=idx,
hash=hashlib.md5(fragment).hexdigest(),
type=filetype))
type=filetype)
with open(path, 'w') as js_file:
js_file.write(fragment)
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 = {
'main': {
'source_filenames': [
......
*.css
module
......@@ -4,3 +4,5 @@
@import 'base', 'layout', 'content-types';
@import 'calendar';
@import 'section', 'unit';
@import 'module/module-styles.scss';
......@@ -14,6 +14,7 @@ def wrap_xmodule(get_html, module, template):
module: An XModule
template: A template that takes the variables:
content: the results of get_html,
class_: the module class name
module_name: the js_module_name of the module
"""
......@@ -21,6 +22,7 @@ def wrap_xmodule(get_html, module, template):
def _get_html():
return render_to_string(template, {
'content': get_html(),
'class_': module.__class__.__name__,
'module_name': module.js_module_name
})
return _get_html
......
......@@ -75,6 +75,7 @@ class CapaModule(XModule):
'js': [resource_string(__name__, 'js/src/capa/imageinput.js'),
resource_string(__name__, 'js/src/capa/schematic.js')]}
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):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
......
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;
}
}
div {
p.status {
text-indent: -9999px;
margin: -1px 0 0 10px;
}
&.unanswered {
p.status {
@include inline-block();
background: url('../images/unanswered-icon.png') center center no-repeat;
height: 14px;
width: 14px;
}
}
&.correct, &.ui-icon-check {
p.status {
@include inline-block();
background: url('../images/correct-icon.png') center center no-repeat;
height: 20px;
width: 25px;
}
input {
border-color: green;
}
}
&.incorrect, &.ui-icon-close {
p.status {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
width: 20px;
text-indent: -9999px;
}
input {
border-color: red;
}
}
> span {
display: block;
margin-bottom: lh(.5);
}
p.answer {
@include inline-block();
margin-bottom: 0;
margin-left: 10px;
&:before {
content: "Answer: ";
font-weight: bold;
display: inline;
}
&:empty {
&:before {
display: none;
}
}
}
div.equation {
clear: both;
padding: 6px;
background: #eee;
span {
margin-bottom: 0;
}
}
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;
}
}
}
ul {
list-style: disc outside none;
margin-bottom: lh();
margin-left: .75em;
margin-left: .75rem;
}
ol {
list-style: decimal outside none;
margin-bottom: lh();
margin-left: .75em;
margin-left: .75rem;
}
dl {
line-height: 1.4em;
}
dl dt {
font-weight: bold;
}
dl dd {
margin-bottom: 0;
}
dd {
margin-left: .5em;
margin-left: .5rem;
}
li {
line-height: 1.4em;
margin-bottom: lh(.5);
&:last-child {
margin-bottom: 0;
}
}
p {
margin-bottom: lh();
}
table {
margin-bottom: lh();
width: 100%;
// border: 1px solid #ddd;
border-collapse: collapse;
th {
// border-bottom: 2px solid #ccc;
font-weight: bold;
text-align: left;
}
td {
// border: 1px solid #ddd;
}
caption, th, td {
padding: .25em .75em .25em 0;
padding: .25rem .75rem .25rem 0;
}
caption {
background: #f1f1f1;
margin-bottom: .75em;
margin-bottom: .75rem;
padding: .75em 0;
padding: .75rem 0;
}
tr, td, th {
vertical-align: middle;
}
}
hr {
background: #ddd;
border: none;
clear: both;
color: #ddd;
float: none;
height: 1px;
margin: 0 0 .75rem;
width: 100%;
}
.hidden {
display: none;
visibility: hidden;
}
#{$all-text-inputs} {
display: inline;
width: auto;
}
// this supports a deprecated element and should be removed once the center tag is removed
center {
display: block;
margin: lh() 0;
border: 1px solid #ccc;
padding: lh();
}
}
......@@ -64,7 +64,50 @@ class Plugin(object):
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.
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):
# if the icon class depends on the data in the module
icon_class = 'other'
js = {}
js_module_name = None
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
'''
Construct a new xmodule
......@@ -196,28 +236,8 @@ class XModule(object):
get is a dictionary-like object '''
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):
class XModuleDescriptor(Plugin, HTMLSnippet):
"""
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,
......@@ -228,8 +248,6 @@ class XModuleDescriptor(Plugin):
and can generate XModules (which do know about student state).
"""
entry_point = "xmodule.v1"
js = {}
js_module_name = None
module_class = XModule
# A list of metadata that this module can inherit from its parent module
......@@ -404,25 +422,6 @@ class XModuleDescriptor(Plugin):
"""
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 ===================================
def get_sample_state(self):
"""
......
<section class="xmodule_display" data-type="${module_name}">
<section class="xmodule_display xmodule_${class_}" data-type="${module_name}">
${content}
</section>
<section class="xmodule_edit" data-type="${module_name}">
<section class="xmodule_edit xmodule_${class_}" data-type="${module_name}">
${content}
</section>
......@@ -24,6 +24,7 @@ import tempfile
import glob2
import errno
import hashlib
from collections import defaultdict
import djcelery
from path import path
......@@ -336,31 +337,60 @@ main_vendor_js = [
from xmodule.x_module import XModuleDescriptor
from xmodule.hidden_module import HiddenDescriptor
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
try:
os.makedirs(js_file_dir)
except OSError as exc:
if exc.errno == errno.EEXIST:
pass
else:
raise
fragments = set()
css_file_dir = PROJECT_ROOT / "static" / "sass" / "module"
module_styles_path = css_file_dir / "_module-styles.scss"
for dir_ in (js_file_dir, css_file_dir):
try:
os.makedirs(dir_)
except OSError as exc:
if exc.errno == errno.EEXIST:
pass
else:
raise
js_fragments = set()
css_fragments = defaultdict(set)
for descriptor in XModuleDescriptor.load_classes() + [HiddenDescriptor]:
module_js = descriptor.module_class.get_javascript()
for filetype in ('coffee', 'js'):
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 = []
for idx, filetype, fragment in sorted(fragments):
path = os.path.join(js_file_dir, "{idx}-{hash}.{type}".format(
for idx, filetype, fragment in sorted(js_fragments):
path = js_file_dir / "{idx}-{hash}.{type}".format(
idx=idx,
hash=hashlib.md5(fragment).hexdigest(),
type=filetype))
type=filetype)
with open(path, 'w') as js_file:
js_file.write(fragment)
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 = {
'application': {
......
*.css
module
......@@ -20,4 +20,5 @@
@import 'course/old/courseware/sidebar';
@import 'course/old/courseware/video';
@import 'course/old/courseware/amplifier';
@import 'course/old/courseware/problems';
@import 'module/module-styles.scss';
......@@ -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 {
h1 {
background: none;
......
section.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;
}
}
div {
p.status {
text-indent: -9999px;
margin: -1px 0 0 10px;
}
&.unanswered {
p.status {
@include inline-block();
background: url('../images/unanswered-icon.png') center center no-repeat;
height: 14px;
width: 14px;
}
}
&.correct, &.ui-icon-check {
p.status {
@include inline-block();
background: url('../images/correct-icon.png') center center no-repeat;
height: 20px;
width: 25px;
}
input {
border-color: green;
}
}
&.incorrect, &.ui-icon-close {
p.status {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
width: 20px;
text-indent: -9999px;
}
input {
border-color: red;
}
}
> span {
display: block;
margin-bottom: lh(.5);
}
p.answer {
@include inline-block();
margin-bottom: 0;
margin-left: 10px;
&:before {
content: "Answer: ";
font-weight: bold;
display: inline;
}
&:empty {
&:before {
display: none;
}
}
}
div.equation {
clear: both;
padding: 6px;
background: #eee;
span {
margin-bottom: 0;
}
}
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;
}
}
}
ul {
list-style: disc outside none;
margin-bottom: lh();
margin-left: .75em;
margin-left: .75rem;
}
ol {
list-style: decimal outside none;
margin-bottom: lh();
margin-left: .75em;
margin-left: .75rem;
}
dl {
line-height: 1.4em;
}
dl dt {
font-weight: bold;
}
dl dd {
margin-bottom: 0;
}
dd {
margin-left: .5em;
margin-left: .5rem;
}
li {
line-height: 1.4em;
margin-bottom: lh(.5);
&:last-child {
margin-bottom: 0;
}
}
p {
margin-bottom: lh();
}
table {
margin-bottom: lh();
width: 100%;
// border: 1px solid #ddd;
border-collapse: collapse;
th {
// border-bottom: 2px solid #ccc;
font-weight: bold;
text-align: left;
}
td {
// border: 1px solid #ddd;
}
caption, th, td {
padding: .25em .75em .25em 0;
padding: .25rem .75rem .25rem 0;
}
caption {
background: #f1f1f1;
margin-bottom: .75em;
margin-bottom: .75rem;
padding: .75em 0;
padding: .75rem 0;
}
tr, td, th {
vertical-align: middle;
}
}
hr {
background: #ddd;
border: none;
clear: both;
color: #ddd;
float: none;
height: 1px;
margin: 0 0 .75rem;
width: 100%;
}
.hidden {
display: none;
visibility: hidden;
}
#{$all-text-inputs} {
display: inline;
width: auto;
}
// this supports a deprecated element and should be removed once the center tag is removed
center {
display: block;
margin: lh() 0;
border: 1px solid #ccc;
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;
}
}
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