Merge pull request #163 from MITx/dormsbee/multicourse

Enable multicourse
This commit is contained in:
Calen Pennington
2012-07-11 06:18:43 -07:00
2442 changed files with 80287 additions and 6570 deletions

View File

@@ -5,12 +5,17 @@ import logging
log = logging.getLogger(__name__)
def import_from_xml(org, course, data_dir):
def import_from_xml(data_dir, course_dirs=None):
"""
Import the specified xml data_dir into the django defined modulestore,
using org and course as the location org and course.
"""
module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor', eager=True)
module_store = XMLModuleStore(
data_dir,
default_class='xmodule.raw_module.RawDescriptor',
eager=True,
course_dirs=course_dirs
)
for module in module_store.modules.itervalues():
# TODO (cpennington): This forces import to overrite the same items.
@@ -26,4 +31,4 @@ def import_from_xml(org, course, data_dir):
modulestore().update_children(module.location, module.definition['children'])
modulestore().update_metadata(module.location, dict(module.metadata))
return module_store.course
return module_store

View File

@@ -13,8 +13,12 @@ class Command(BaseCommand):
'''Import the specified data directory into the default ModuleStore'''
def handle(self, *args, **options):
if len(args) != 3:
raise CommandError("import requires 3 arguments: <org> <course> <data directory>")
if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
org, course, data_dir = args
import_from_xml(org, course, data_dir)
data_dir = args[0]
if len(args) > 1:
course_dirs = args[1:]
else:
course_dirs = None
import_from_xml(data_dir, course_dirs)

View File

@@ -6,7 +6,7 @@ from django_future.csrf import ensure_csrf_cookie
from fs.osfs import OSFS
from django.core.urlresolvers import reverse
from xmodule.modulestore import Location
from github_sync import repo_path_from_location, export_to_github
from github_sync import export_to_github
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
@@ -51,11 +51,12 @@ def save_item(request):
modulestore().update_item(item_id, data)
# Export the course back to github
# This uses wildcarding to find the course, which requires handling
# multiple courses returned, but there should only ever be one
course_location = Location(item_id)._replace(category='course', name=None)
courses = modulestore().get_items(course_location)
for course in courses:
repo_path = repo_path_from_location(course.location)
export_to_github(course, repo_path, "CMS Edit")
export_to_github(course, "CMS Edit")
return HttpResponse(json.dumps({}))

View File

@@ -12,6 +12,7 @@ from .exceptions import GithubSyncError
log = logging.getLogger(__name__)
def import_from_github(repo_settings):
"""
Imports data into the modulestore based on the XML stored on github
@@ -19,10 +20,9 @@ def import_from_github(repo_settings):
repo_settings is a dictionary with the following keys:
path: file system path to the local git repo
branch: name of the branch to track on github
org: name of the organization to use in the imported course
course: name of the coures to use in the imported course
"""
repo_path = repo_settings['path']
data_dir, course_dir = os.path.split(repo_path)
if not os.path.isdir(repo_path):
Repo.clone_from(repo_settings['origin'], repo_path)
@@ -34,18 +34,12 @@ def import_from_github(repo_settings):
# Do a hard reset to the remote branch so that we have a clean import
git_repo.git.checkout(repo_settings['branch'])
git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True)
return git_repo.head.commit.hexsha, import_from_xml(repo_settings['org'], repo_settings['course'], repo_path)
module_store = import_from_xml(data_dir, course_dirs=[course_dir])
return git_repo.head.commit.hexsha, module_store.courses[course_dir]
def repo_path_from_location(location):
location = Location(location)
for name, repo in settings.REPOS.items():
if repo['org'] == location.org and repo['course'] == location.course:
return repo['path']
def export_to_github(course, repo_path, commit_message):
def export_to_github(course, commit_message):
repo_path = settings.DATA_DIR / course.metadata.get('course_dir', course.location.course)
fs = OSFS(repo_path)
xml = course.export_to_xml(fs)

View File

@@ -1,7 +1,7 @@
from django.test import TestCase
from path import path
import shutil
from github_sync import import_from_github, export_to_github, repo_path_from_location
from github_sync import import_from_github, export_to_github
from git import Repo
from django.conf import settings
from xmodule.modulestore.django import modulestore
@@ -10,6 +10,7 @@ from override_settings import override_settings
from github_sync.exceptions import GithubSyncError
@override_settings(DATA_DIR=path('test_root'))
class GithubSyncTestCase(TestCase):
def setUp(self):
@@ -29,8 +30,6 @@ class GithubSyncTestCase(TestCase):
'path': self.repo_dir,
'origin': self.remote_dir,
'branch': 'master',
'org': 'org',
'course': 'course'
})
def tearDown(self):
@@ -49,7 +48,7 @@ class GithubSyncTestCase(TestCase):
"""
self.assertEquals('Toy Course', self.import_course.metadata['display_name'])
self.assertIn(
Location('i4x://org/course/chapter/Overview'),
Location('i4x://edx/local_repo/chapter/Overview'),
[child.location for child in self.import_course.get_children()])
self.assertEquals(1, len(self.import_course.get_children()))
@@ -58,7 +57,7 @@ class GithubSyncTestCase(TestCase):
"""
Test that with the GITHUB_PUSH feature disabled, no content is pushed to the remote
"""
export_to_github(self.import_course, self.repo_dir, 'Test no-push')
export_to_github(self.import_course, 'Test no-push')
self.assertEquals(1, Repo(self.remote_dir).head.commit.count())
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
@@ -67,7 +66,7 @@ class GithubSyncTestCase(TestCase):
Test that with GITHUB_PUSH enabled, content is pushed to the remote
"""
self.import_course.metadata['display_name'] = 'Changed display name'
export_to_github(self.import_course, self.repo_dir, 'Test push')
export_to_github(self.import_course, 'Test push')
self.assertEquals(2, Repo(self.remote_dir).head.commit.count())
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
@@ -80,17 +79,6 @@ class GithubSyncTestCase(TestCase):
remote = Repo(self.remote_dir)
remote.git.commit(allow_empty=True, m="Testing conflict commit")
self.assertRaises(GithubSyncError, export_to_github, self.import_course, self.repo_dir, 'Test push')
self.assertRaises(GithubSyncError, export_to_github, self.import_course, 'Test push')
self.assertEquals(2, remote.head.reference.commit.count())
self.assertEquals("Testing conflict commit\n", remote.head.reference.commit.message)
@override_settings(REPOS={'namea': {'path': 'patha', 'org': 'orga', 'course': 'coursea'},
'nameb': {'path': 'pathb', 'org': 'orgb', 'course': 'courseb'}})
class RepoPathLookupTestCase(TestCase):
def test_successful_lookup(self):
self.assertEquals('patha', repo_path_from_location('i4x://orga/coursea/course/foo'))
self.assertEquals('pathb', repo_path_from_location('i4x://orgb/courseb/course/foo'))
def test_failed_lookup(self):
self.assertEquals(None, repo_path_from_location('i4x://c/c/course/foo'))

View File

@@ -36,14 +36,15 @@ MITX_FEATURES = {
############################# SET PATH INFORMATION #############################
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms
COMMON_ROOT = PROJECT_ROOT.dirname() / "common"
ENV_ROOT = PROJECT_ROOT.dirname().dirname() # virtualenv dir /mitx is in
REPO_ROOT = PROJECT_ROOT.dirname()
COMMON_ROOT = REPO_ROOT / "common"
ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in
COURSES_ROOT = ENV_ROOT / "data"
# FIXME: To support multiple courses, we should walk the courses dir at startup
DATA_DIR = COURSES_ROOT
sys.path.append(ENV_ROOT)
sys.path.append(REPO_ROOT)
sys.path.append(PROJECT_ROOT / 'djangoapps')
sys.path.append(PROJECT_ROOT / 'lib')
sys.path.append(COMMON_ROOT / 'djangoapps')
@@ -118,7 +119,7 @@ TEMPLATE_DEBUG = False
SITE_ID = 1
SITE_NAME = "localhost:8000"
HTTPS = 'on'
ROOT_URLCONF = 'mitx.cms.urls'
ROOT_URLCONF = 'cms.urls'
IGNORABLE_404_ENDS = ('favicon.ico')
# Email

85
cms/static/js/main.js Normal file
View File

@@ -0,0 +1,85 @@
$(document).ready(function(){
$('section.main-content').children().hide();
$(function(){
$('.editable').inlineEdit();
$('.editable-textarea').inlineEdit({control: 'textarea'});
});
var heighest = 0;
$('.cal ol > li').each(function(){
heighest = ($(this).height() > heighest) ? $(this).height() : heighest;
});
$('.cal ol > li').css('height',heighest + 'px');
$('.add-new-section').click(function() {
return false;
});
$('.new-week .close').click( function(){
$(this).parents('.new-week').hide();
$('p.add-new-week').show();
return false;
});
$('.save-update').click(function(){
$(this).parent().parent().hide();
return false;
});
setHeight = function(){
var windowHeight = $(this).height();
var contentHeight = windowHeight - 29;
$('section.main-content > section').css('min-height', contentHeight);
$('body.content .cal').css('height', contentHeight);
$('.edit-week').click( function() {
$('body').addClass('content');
$('body.content .cal').css('height', contentHeight);
$('section.week-new').show();
return false;
});
$('.cal ol li header h1 a').click( function() {
$('body').addClass('content');
$('body.content .cal').css('height', contentHeight);
$('section.week-edit').show();
return false;
});
$('a.sequence-edit').click(function(){
$('body').addClass('content');
$('body.content .cal').css('height', contentHeight);
$('section.sequence-edit').show();
return false;
});
}
$(document).ready(setHeight);
$(window).bind('resize', setHeight);
$('.video-new a').click(function(){
$('section.video-new').show();
return false;
});
$('a.video-edit').click(function(){
$('section.video-edit').show();
return false;
});
$('.problem-new a').click(function(){
$('section.problem-new').show();
return false;
});
$('a.problem-edit').click(function(){
$('section.problem-edit').show();
return false;
});
});

View File

@@ -0,0 +1,128 @@
section.video-new, section.video-edit, section.problem-new, section.problem-edit {
position: absolute;
top: 72px;
right: 0;
background: #fff;
width: flex-grid(6);
@include box-shadow(0 0 6px #666);
border: 1px solid #333;
border-right: 0;
z-index: 4;
> header {
background: #666;
@include clearfix;
color: #fff;
padding: 6px;
border-bottom: 1px solid #333;
-webkit-font-smoothing: antialiased;
h2 {
float: left;
font-size: 14px;
}
a {
color: #fff;
&.save-update {
float: right;
}
&.cancel {
float: left;
}
}
}
> section {
padding: 20px;
> header {
h1 {
font-size: 24px;
margin: 12px 0;
}
section {
&.status-settings {
ul {
list-style: none;
@include border-radius(2px);
border: 1px solid #999;
@include inline-block();
li {
@include inline-block();
border-right: 1px solid #999;
padding: 6px;
&:last-child {
border-right: 0;
}
&.current {
background: #eee;
}
}
}
a.settings {
@include inline-block();
margin: 0 20px;
border: 1px solid #999;
padding: 6px;
}
select {
float: right;
}
}
&.meta {
background: #eee;
padding: 10px;
margin: 20px 0;
@include clearfix();
div {
float: left;
margin-right: 20px;
h2 {
font-size: 14px;
@include inline-block();
}
p {
@include inline-block();
}
}
}
}
}
section.notes {
margin-top: 20px;
padding: 6px;
background: #eee;
border: 1px solid #ccc;
textarea {
@include box-sizing(border-box);
display: block;
width: 100%;
}
h2 {
font-size: 14px;
margin-bottom: 6px;
}
input[type="submit"]{
margin-top: 10px;
}
}
}
}

View File

@@ -0,0 +1,24 @@
section.problem-new, section.problem-edit {
> section {
textarea {
@include box-sizing(border-box);
display: block;
width: 100%;
}
div.preview {
background: #eee;
@include box-sizing(border-box);
height: 40px;
padding: 10px;
width: 100%;
}
a.save {
@extend .button;
@include inline-block();
margin-top: 20px;
}
}
}

View File

@@ -0,0 +1,33 @@
section.video-new, section.video-edit {
> section {
section.upload {
padding: 6px;
margin-bottom: 10px;
border: 1px solid #ddd;
a.upload-button {
@extend .button;
@include inline-block();
}
}
section.in-use {
h2 {
font-size: 14px;
}
div {
background: #eee;
text-align: center;
padding: 6px;
}
}
a.save-update {
@extend .button;
@include inline-block();
margin-top: 20px;
}
}
}

256
cms/static/sass/_week.scss Normal file
View File

@@ -0,0 +1,256 @@
section.week-edit,
section.week-new,
section.sequence-edit {
> header {
border-bottom: 2px solid #333;
@include clearfix();
div {
@include clearfix();
padding: 6px 20px;
h1 {
font-size: 18px;
text-transform: uppercase;
letter-spacing: 1px;
float: left;
}
p {
float: right;
}
&.week {
background: #eee;
font-size: 12px;
border-bottom: 1px solid #ccc;
h2 {
font-size: 12px;
@include inline-block();
margin-right: 20px;
}
ul {
list-style: none;
@include inline-block();
li {
@include inline-block();
margin-right: 10px;
p {
float: none;
}
}
}
}
}
section.goals {
background: #eee;
padding: 6px 20px;
border-top: 1px solid #ccc;
ul {
list-style: none;
color: #999;
li {
margin-bottom: 6px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
> section.content {
@include box-sizing(border-box);
padding: 20px;
section.filters {
@include clearfix;
margin-bottom: 10px;
background: #efefef;
border: 1px solid #ddd;
ul {
@include clearfix();
list-style: none;
padding: 6px;
li {
@include inline-block();
&.advanced {
float: right;
}
}
}
}
> div {
display: table;
border: 1px solid;
width: 100%;
section {
header {
background: #eee;
padding: 6px;
border-bottom: 1px solid #ccc;
@include clearfix;
h2 {
text-transform: uppercase;
letter-spacing: 1px;
font-size: 12px;
float: left;
}
}
&.modules {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(6, 9);
border-right: 1px solid #333;
&.empty {
text-align: center;
vertical-align: middle;
a {
@extend .button;
@include inline-block();
margin-top: 10px;
}
}
ol {
list-style: none;
border-bottom: 1px solid #333;
li {
border-bottom: 1px solid #333;
&:last-child{
border-bottom: 0;
}
a {
color: #000;
}
ol {
list-style: none;
li {
padding: 6px;
&:hover {
a.draggable {
opacity: 1;
}
}
a.draggable {
float: right;
opacity: .5;
}
&.group {
padding: 0;
header {
padding: 6px;
background: none;
h3 {
font-size: 14px;
}
}
ol {
border-left: 4px solid #999;
border-bottom: 0;
li {
&:last-child {
border-bottom: 0;
}
}
}
}
}
}
}
}
}
&.scratch-pad {
@include box-sizing(border-box);
display: table-cell;
width: flex-grid(3, 9) + flex-gutter(9);
vertical-align: top;
ol {
list-style: none;
border-bottom: 1px solid #999;
li {
border-bottom: 1px solid #999;
background: #f9f9f9;
&:last-child {
border-bottom: 0;
}
ul {
list-style: none;
li {
padding: 6px;
&:last-child {
border-bottom: 0;
}
&:hover {
a.draggable {
opacity: 1;
}
}
&.empty {
padding: 12px;
a {
@extend .button;
display: block;
text-align: center;
}
}
a.draggable {
float: right;
opacity: .3;
}
a {
color: #000;
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,242 @@
<ul class="tabs">
<li class="active">English (main)</li>
<li>French</li>
<li>English v2</li>
<li>+</li>
</ul>
<textarea class="captions">
{
"start": [
0, 2770, 5700, 7620, 10320, 12130, 13430, 15170, 17940, 20890, 22840, 26200, 28980, 30170, 32040, 33240, 36420, 37570, 41760, 44270, 48120, 50810, 52960, 54070, 56480, 58600, 59550, 62520, 67680, 69990, 74280, 77605, 81320, 85050, 88430, 92750, 96010, 99440, 103160, 106650, 110050, 114240, 118800, 120980, 122290, 125040, 127900, 130990, 133170, 134580, 139410, 143180, 144530, 147640, 150800, 153220, 156570, 161000, 162010, 162930, 164090, 165490, 167650, 170630, 172020, 174760, 178480, 181840, 185840, 188620, 194160, 196110, 199370, 201360, 204140, 209970, 211850, 215030, 218890, 221730, 225370, 228790, 231690, 234080, 236300, 237970, 240450, 244010, 247670, 251910, 253230, 260149, 263330, 266380, 269470, 273570, 278240, 280050, 282810, 288060, 292410, 297300, 298950, 300860, 302500, 304350, 309620, 312950, 318710, 322870, 323810, 328380, 332840, 334440, 338640, 341540, 345490, 348900, 350730, 354480, 357640, 362310, 365020, 366890, 368900, 373200, 374240, 379410, 381580, 381990, 385680, 389080, 390750, 393970, 395960, 397340, 401000, 403210, 405650, 408880, 411730, 415490, 421350, 425630, 427520, 430490, 435320, 436860, 439460, 443300, 447010, 450740, 453820, 456610, 460140, 463730, 466700, 471200, 472450, 475260, 476330, 480650, 483650, 486320, 489080, 491940, 496690, 501990, 502740, 507000, 511650, 513220, 517330, 519169, 524159, 528140, 529960, 531270, 535340, 541590, 543710, 545170, 550960, 551810, 555140, 556230, 557750, 560530, 564300, 566800, 567600, 569910, 573170, 578610, 580490, 585520, 586500, 589880, 591750, 596120, 597290, 600290, 602940, 606490, 608560, 610690, 612600, 613970, 616670, 621260, 622310, 624520, 626750, 629550, 632500, 635510, 637470, 638900, 640370, 644200, 647470, 648740, 652700, 653950 ],
"end": [
2770, 5700, 7620, 10320, 12130, 13430, 15170, 17940, 20890, 22840, 26200, 28980, 30170, 32040, 33240, 36420, 37570, 41760, 44269, 48120, 50809, 52960, 54070, 56480, 58599, 59550, 62519, 67680, 69990, 74280, 77605, 81320, 85050, 88429, 92750, 96010, 99440, 103160, 106649, 110050, 114240, 118800, 120980, 122290, 125040, 127900, 130990, 133170, 134579, 139410, 143180, 144530, 147640, 150799, 153220, 156570, 161000, 162010, 162929, 164090, 165490, 167650, 170630, 172019, 174760, 178480, 181840, 185840, 188620, 194160, 196109, 199370, 201360, 204140, 209970, 211850, 215030, 218890, 221730, 225369, 228790, 231690, 234079, 236300, 237970, 240450, 244010, 247670, 251910, 253230, 260149, 263330, 266380, 269469, 273570, 278240, 280050, 282810, 288060, 292410, 297300, 298950, 300860, 302500, 304350, 309620, 312950, 318710, 322870, 323810, 328380, 332840, 334440, 338640, 341539, 345490, 348900, 350729, 354480, 357640, 362310, 365020, 366890, 368900, 373200, 374240, 379410, 381580, 381990, 385680, 389080, 390750, 393970, 395960, 397340, 401000, 403210, 405650, 408880, 411730, 415490, 421350, 425630, 427520, 430490, 435320, 436860, 439460, 443299, 447010, 450740, 453820, 456610, 460140, 463729, 466700, 471200, 472450, 475260, 476330, 480650, 483650, 486320, 489080, 491940, 496690, 501990, 502740, 507000, 511650, 513220, 517330, 519169, 524159, 528140, 529960, 531270, 535340, 541590, 543710, 545170, 550959, 551810, 555140, 556230, 557750, 560530, 564300, 566800, 567599, 569910, 573170, 578610, 580490, 585520, 586500, 589880, 591750, 596120, 597290, 600290, 602939, 606490, 608560, 610689, 612600, 613970, 616670, 621260, 622310, 624520, 626750, 629550, 632500, 635510, 637470, 638900, 640370, 644200, 647470, 648740, 652700, 653950, 655050 ],
"text": [
"SUBJECT 1: The various methods\nI&#39;m going to show you--",
"in particular, the first method\none and method two for",
"solving non-linear equations--",
"are really just particular\nways of solving a pair of",
"equations where at least\none of each have some",
"non-linearity to them.",
"So let&#39;s start with the\ngraphical method.",
"And my circuit is on the\nright-hand side.",
"And I&#39;m showing you the same\nvoltage source resistor--",
"the Thevenin pattern--",
"connected to Device D. And\nbelow that, I have the",
"equation iD equals ae\nraised to bvD, which",
"is the device equation.",
"So as before, let me\ngo ahead and do all",
"the first few steps.",
"I go ahead and write the\nnode equation at vD.",
"And I got that.",
"I go in and substitute, as I\nalways do, in the node method",
"for the current, using\nthe device relation.",
"And just for fun here, let me\nkeep that separate for now.",
"In the analytical method, notice\nthat you ended up with",
"these two equations, and\nyou had to solve",
"for these two unknowns.",
"And we did that using\nanalytical methods.",
"In this video, I&#39;m going to\nsolve these using the",
"graphical method.",
"In order to do so here&#39;s what\nI&#39;m going to do I want to",
"start by rearranging the terms\nin my equation 1 to make it a",
"little bit more convenient\nto draw the graph.",
"So notice in equation 2, I\nhave iD equals something.",
"And then I have an expression\nin vD and iD.",
"So what I&#39;d like to do is let\nme start by taking this",
"equation here and expressing it\nin more of a standard form",
"so I can get something symmetric\nto equation 2.",
"So let me express this by\npulling iD to the left-hand",
"side all by itself, and so I\nget something like this.",
"So I get iD on the left-hand\nside, and then I&#39;m going to",
"move vD minus V divided by\nr do the right hand side.",
"So let&#39;s start with minus V\ndivided by R, and when I move",
"that to the right-hand side, it\nbecomes V divided by R. And",
"then when I move vD over R to\nthe right-hand side, I get",
"minus vD over R. And I&#39;ve simply\nnot done much here.",
"I just have done some\nrearranging of the terms in",
"equation 1.",
"And since I haven&#39;t done\nanything unique and different,",
"I&#39;m just going to label\nthis as 1 prime.",
"So I just got this from equation\n1, and I just labeled",
"that as 1 prime.",
"Continuing with the method--",
"and now I&#39;m summarizing for you\nequations 1 prime and 2--",
"we want both of them for iD on\nthe left-hand side expressed",
"as a function of vD.",
"And in equation 2,\nit is non-linear.",
"So the graphical method can\nbe summarized as follows.",
"To start, I want you to\nnotice something.",
"Essentially all we&#39;re trying to\ndo is find a solution, find",
"a value for vD and iD that\nsatisfies both equations, 1",
"prime and 2.",
"that&#39;s all we&#39;re trying to do.",
"It&#39;s just math here.",
"There&#39;s no circuits here.",
"We&#39;re just doing some relatively\nsimple math.",
"We just have to figure out what\niD and vD are, and we&#39;ll",
"use the graphical method.",
"So to do the graphical method,\nwhat I&#39;m going to do is I&#39;m",
"going to plot the two equations\nin a pair of graphs.",
"And let me first start by\nplotting equation 2.",
"And in equation 2, I&#39;m going\nto plot iD equals a",
"raised to ae bvD.",
"I&#39;m going to plot iD equals\na times e raised to bvD.",
"That&#39;s my equation 2.",
"And this is the plot\nthat I get for it.",
"It&#39;s a plot that I had before.",
"So that&#39;s my equation 2.",
"Notice that this plot here is\nsimply the constraint on iD",
"and vD imposed by the device.",
"This little circuit here\ncontaining the Thevenin",
"equivalent and connected to\nthe device, equation 2 is",
"simply the constraint imposed\nby the device.",
"The device properties are such\nthat it is going to constrain",
"iD and vD into some\nrelationship.",
"This constraint that I&#39;ve\nplotted here as equation 2 is",
"simply the constraint imposed\nby this device.",
"OK, next, let me\ngo to 1 prime.",
"And in 1 prime, let\nme go ahead and",
"plot the 1 prime equation.",
"And that equation, as we\nrewrite that here, is V",
"divided by R minus vD divided\nby R. So I&#39;m just going to",
"plot that equation for you\nin a second graph.",
"So how do I plot this?",
"So notice here that when vD is\n0, then iD is V divided by R.",
"So that is one point on\nthe straight line.",
"Notice that this is an equation\nfor a straight line.",
"It&#39;s a linear relationship\nbetween iD and vD.",
"Next, when iD is 0--\nso when iD is 0--",
"then notice that R and R can be\ncanceled out, and V will be",
"equal to vD.",
"So when iD is 0, V\nand vD are equal.",
"So therefore, vD equals V. So\nthis is the line when iD is 0.",
"And for that, vD equals V. So\nthen I get this [? long, ?]",
"straight line for the\nrelationship between vD and iD",
"according to 1 prime.",
"So what&#39;s the slope\nof this line here?",
"Can you tell me what the\nslope of this line is.",
"Let me give you a few seconds\nto think about it.",
null,
"OK, from the equation 1 prime--\nfrom this equation--",
"the slope is simply given by the\ncoefficient of vD, since",
"the slope is negative, which\nis why the line is inclined",
"the following way.",
"So the slope here is simply\nminus 1 divided by R.",
"So that is my constraint\nthat relates vD to iD.",
"And where did that constraint\ncome from?",
"That constraint is the\nconstraint on iD and vD that",
"has been imposed by the\nrest of the circuit.",
"So if the first constraint was\nimposed by the device, then",
"the second constraint is\nimposed by the Thevenin",
"equivalent that is connected\nto the device.",
"The Thevenin equivalent was\nthat V and R in series and",
"those two impose a Thevenin\nconstraint on the terminal",
"pair that relates vD and iD.",
"So now my next step is,\ngiven these two graphs",
"for iD versus vD--",
"and clearly those are\nmy two constraints.",
"Graph 2 says that iD and vD\nmust be somewhere on this",
"trajectory.",
"Graph 1 prime says, well, I&#39;m\nnot going to let vD and iD be",
"anywhere else but\non this curve.",
"That&#39;s it.",
"Both of them are fighting with\neach other and telling each",
"other, nope, I&#39;m not going to\nallow you to do anything.",
"You have to be on my curve.",
"So in this case, I have two\ncurves, and I need a point",
"that satisfies both curves.",
"And that is easy enough to do.",
"And I simply have to go\nand satisfy both these",
"constraints, and that will\ngive me the answer",
"for vD versus iD.",
"Before I do that, so I can go\nand solve it for you, let me",
"go and pick some values for\nthe various parameters.",
"So as before, I&#39;m going to pick\nV equals 1 volt, R equals",
"1 ohm, a, a quarter of an amp,\nand b to be one volt inverse.",
"So I&#39;ll pick the same parameters\nas I had done when",
"I did the analytical method.",
"Next, what I&#39;ll do is I&#39;ll\nsubstitute these parameters",
"into the two equations 1 prime\nand 2 and rewrite them with",
"the parameters substituted.",
"So for 1 prime, I get iD.",
"Since V is 1 and R is 1, I get\nV divided by R equals 1.",
"And then since R is\n1, I get minus vD.",
"So this is 1 prime, once I&#39;ve\nsubstituted the values.",
"And then for equation\n2, what do I get?",
"I get iD equals ae\nraised to bvD.",
"a is 1/4, so I write\nthat down.",
"b is 1, and so I\nget vD up here.",
"So I have my two equations in\nterms of the parameters I have",
"chosen, and now I can go ahead\nand plot the two equations and",
"see where they intersect.",
"I&#39;ve given you the form of the\ngraph here, and so let me go",
"ahead and plot this.",
"Let me go ahead and plot 2\nfirst. 2 looks like this,",
"where this point is 1/4.",
"That was the a point.",
"As I said before, this curve\nis simply my Equation 2.",
"Then let me go ahead and\nplot equation 1 prime.",
"And as you recall, that looked\nsomething like this where the",
"vD intercept was V and the\ny-intercept was given by V",
"divided by R.",
"And in this case, it was\n1, and V was also 1.",
"So those are my two curves, and\nhere&#39;s the point where the",
"two coincide.",
"And then I just have to go and\nfind the values of iD and vD",
"where they two intersect.",
"So at this point, it will be\n0.56 volts, and this point",
"will be 0.44 amps.",
"It&#39;s the same as what\nI calculated in",
"the analytical method.",
"So let me go ahead and write\nthat down. iD equals, in this",
"case, 0.44 amps, and vD\nequals 0.56 volts.",
"So basically, I&#39;ve just\ntaken the two graphs,",
"superimposed them.",
"This was 1 prime, and the\nnon-linear one was related to",
"Equation 2.",
"So before I jump off to one\nother thing, I can define",
"something for you.",
"Notice this curve here.",
"This is a straight line that\nreflects the Thevenin",
"constraint that I apply on\nmy non-linear device.",
"And I mentioned earlier we&#39;re\ngoing to do this again and",
"again and again.",
"I think you will see this at\nleast 10 more times in this",
"course where I take a Thevenin\nequivalent of the following",
"form, some voltage V, some R,\nand apply that in series",
"across something interesting.",
"So this line that I see here is\nthe constraint imposed by",
"the Thevenin equivalent.",
"And you can see that from\nthe equation 1 prime.",
"Now, there&#39;s a name\nfor this line.",
"So this line is called the &quot;load\nline.&quot; You will see more",
"reasons for this later.",
"But this line is called\nthe &quot;load line.&quot;",
"Let me show you one other little\ntrick, just in case you",
"didn&#39;t completely get how I\nsolved the graphical method.",
"But one little trick here.",
"If you like PowerPoint,\nyou will enjoy this.",
"And if you don&#39;t like\nPowerPoint, you",
"will hate me for it.",
"So if you look at these\ncurves, I have my two",
"equations in iD-- the 1 prime\nand 2-- and I&#39;ve plotted them",
"for you here.",
"On the left-hand side,\nI plotted equation 2.",
"Right-hand side, I plotted\nequation 1.",
"And fundamentally, all that the\ngraphical method is doing",
"is simply superimposing\nthe two graphs.",
"And by superimposing the two\ngraphs, it is finding the",
"point where the two\ncurves intersect.",
"And this is what you get.",
"So let me do that again.",
"So I take one of the curves and\nsuperimpose it on top of",
"the other curve given the\nsame axes and the same",
"scales for the axes.",
"And then I go ahead and find\nthe solution for the point",
"where the two intersect.",
null
]
}
</textarea>

View File

@@ -0,0 +1,3 @@
<li>
<img src="http://placehold.it/300x180" alt="" /><h5>Video-file-name</h5>
</li>

View File

@@ -0,0 +1,4 @@
<section class="caption-save">
<a href="#" class="close-box">Cancel</a>
<button class="close-box">Save changes</button>
</section>

View File

@@ -0,0 +1,187 @@
<section class="sequence-edit">
<header>
<div class="week">
<h2><a href="">Week 1</a></h2>
<ul>
<li>
<p class="editable"><strong>Goal title:</strong> This is the goal body and is where the goal will be further explained</p>
</li>
</ul>
</div>
<div>
<h1 class="editable">Lecture sequence</h1>
<p><strong>Group type:</strong> Ordered Sequence</p>
</div>
</header>
<section class="content">
<section class="filters">
<ul>
<li>
<label for="">Sort by</label>
<select>
<option value="">Recently Modified</option>
</select>
</li>
<li>
<label for="">Display</label>
<select>
<option value="">All content</option>
</select>
</li>
<li>
<select>
<option value="">Internal Only</option>
</select>
</li>
<li class="advanced">
<a href="#">Advanced filters</a>
</li>
<li>
<input type="search" name="" id="" value="" />
</li>
</ul>
</section>
<div>
<section class="modules">
<ol>
<li>
<ol>
<li>
<a href="" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="sequence-edit">Problem Group</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
<li class="group">
<header>
<h3>
<a href="#" class="problem-edit">Problem group</a>
<a href="#" class="draggable">handle</a>
</h3>
</header>
<ol>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
</ol>
</li>
<li>
<a href="#" class="problem-edit">Problem title 13</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="sequence-edit">Problem Group</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
</ol>
</li>
<!-- <li class="new-module"> -->
<!-- <%include file="new-module.html"/> -->
<!-- </li> -->
</ol>
</section>
<section class="scratch-pad">
<ol>
<li>
<header>
<h2>Section Scratch</h2>
</header>
<ul>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 13 </a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit"> Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
</ul>
</li>
<li>
<header>
<h2>Course Scratch</h2>
</header>
<ul>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 13 </a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit"> Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
</ul>
</li>
<!-- <li class="new-module"> -->
<!-- <%include file="new-module.html"/> -->
<!-- </li> -->
</ol>
</section>
</div>
</section>
</section>

View File

@@ -0,0 +1,7 @@
<div class="tooltip">
<ul>
<li><a href="#view" rel="leanModal">View</a></li>
<li><a href="#">Download</a></li>
<li><a href="#" class="delete-speed">Delete</a></li>
</ul>
</div>

View File

@@ -0,0 +1,38 @@
<li class="video-box">
<div class="thumb"><img src="http://placehold.it/100x65" /></div>
<div class="meta">
<strong>video-name</strong> 236mb Uploaded 6 hours ago by <em>Anant Agrawal</em>
<p>
<ul class="speed-list">
Speed
<li class="speed">
0.75x
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">Normal
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">1.25x
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">1.5x
<%include file="speed-tooltip.html"/>
</li>
<li style="background: #eee;" ><a href="#upload" rel="leanModal" class="new-upload">+</a></li>
</ul>
</p>
<p>
<a href="#">Download All</a>
<a href="#" style="color: brown;" class="remove">Delete All</a>
<a href="#" class="edit-captions"> Edit Captions </a>
<a href="#" class="use-video">Use clip ⬆</a>
</p>
</div>
<div class="caption-box">
<%include file="captions.html"/>
<%include file="save-captions.html"/>
</div>
</li>

View File

@@ -0,0 +1,35 @@
<li class="video-box">
<div class="thumb"><img src="http://placehold.it/155x90" /></div>
<div class="meta">
<strong>video-name</strong> 236mb
<p>Uploaded 6 hours ago by <em>Anant Agrawal</em></p>
<p>
<ul class="speed-list">
Speed
<li class="speed">
0.75x
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">Normal
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">1.25x
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">1.5x
<%include file="speed-tooltip.html"/>
</li>
<li style="background: #eee;" ><a href="#upload" rel="leanModal" class="new-upload">+</a></li>
</ul>
</p>
<p>
<a href="#">Download all</a>
<a href="#" stle="color: brown;" class="remove-video">Remove ⬇ </a>
</p>
</div>
<div style="margin-top: 30px;">
<%include file="captions.html"/>
</div>
</li>

View File

@@ -1,19 +1,26 @@
from staticfiles.storage import staticfiles_storage
import re
PREFIX = '/static/'
STATIC_PATTERN = re.compile(r"""
(?P<quote>['"]) # the opening quotes
{prefix} # the prefix
(?P<rest>.*?) # everything else in the url
(?P=quote) # the first matching closing quote
""".format(prefix=PREFIX), re.VERBOSE)
PREFIX_LEN = len(PREFIX)
def replace(static_url):
def replace(static_url, prefix=None):
if prefix is None:
prefix = ''
else:
prefix = prefix + '/'
quote = static_url.group('quote')
url = staticfiles_storage.url(static_url.group('rest'))
url = staticfiles_storage.url(prefix + static_url.group('rest'))
return "".join([quote, url, quote])
def replace_urls(text):
return STATIC_PATTERN.sub(replace, text)
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/'):
def replace_url(static_url):
return replace(static_url, staticfiles_prefix)
return re.sub(r"""
(?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes
{prefix} # the prefix
(?P<rest>.*?) # everything else in the url
(?P=quote) # the first matching closing quote
""".format(prefix=replace_prefix), replace_url, text)

View File

@@ -147,7 +147,7 @@ class LoncapaProblem(object):
used to give complex problems (eg programming questions) multiple points.
'''
maxscore = 0
for responder in self.responders.values():
for response, responder in self.responders.iteritems():
if hasattr(responder,'get_max_score'):
try:
maxscore += responder.get_max_score()
@@ -155,11 +155,7 @@ class LoncapaProblem(object):
log.debug('responder %s failed to properly return from get_max_score()' % responder) # FIXME
raise
else:
try:
maxscore += len(responder.get_answers())
except:
log.debug('responder %s failed to properly return get_answers()' % responder) # FIXME
raise
maxscore += len(self.responder_answers[response])
return maxscore
def get_score(self):
@@ -211,8 +207,8 @@ class LoncapaProblem(object):
(see capa_module)
"""
answer_map = dict()
for responder in self.responders.values():
results = responder.get_answers()
for response in self.responders.keys():
results = self.responder_answers[response]
answer_map.update(results) # dict of (id,correct_answer)
# include solutions from <solution>...</solution> stanzas
@@ -228,8 +224,9 @@ class LoncapaProblem(object):
the dicts returned by grade_answers and get_question_answers. (Though
get_question_answers may only return a subset of these."""
answer_ids = []
for responder in self.responders.values():
answer_ids.append(responder.get_answers().keys())
for response in self.responders.keys():
results = self.responder_answers[response]
answer_ids.append(results.keys())
return answer_ids
def get_html(self):
@@ -382,6 +379,8 @@ class LoncapaProblem(object):
In-place transformation
Also create capa Response instances for each responsetype and save as self.responders
Obtain all responder answers and save as self.responder_answers dict (key = response)
'''
response_id = 1
self.responders = {}
@@ -402,6 +401,15 @@ class LoncapaProblem(object):
responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
self.responders[response] = responder # save in list in self
# get responder answers (do this only once, since there may be a performance cost, eg with externalresponse)
self.responder_answers = {}
for response in self.responders.keys():
try:
self.responder_answers[response] = responder.get_answers()
except:
log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME
raise
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
solution_id = 1

View File

@@ -38,8 +38,8 @@
<div id="display_${id}" class="equation">`{::}`</div>
</div>
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"> </textarea>
% if msg:
<span class="message">${msg|n}</span>
% endif
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"> </textarea>
% if msg:
<span class="message">${msg|n}</span>
% endif
</section>

View File

@@ -4,5 +4,5 @@ setup(
name="capa",
version="0.1",
packages=find_packages(exclude=["tests"]),
install_requires=['distribute'],
install_requires=['distribute', 'pyparsing'],
)

View File

@@ -0,0 +1,160 @@
'''
Progress class for modules. Represents where a student is in a module.
Useful things to know:
- Use Progress.to_js_status_str() to convert a progress into a simple
status string to pass to js.
- Use Progress.to_js_detail_str() to convert a progress into a more detailed
string to pass to js.
In particular, these functions have a canonical handing of None.
For most subclassing needs, you should only need to reimplement
frac() and __str__().
'''
from collections import namedtuple
import numbers
class Progress(object):
'''Represents a progress of a/b (a out of b done)
a and b must be numeric, but not necessarily integer, with
0 <= a <= b and b > 0.
Progress can only represent Progress for modules where that makes sense. Other
modules (e.g. html) should return None from get_progress().
TODO: add tag for module type? Would allow for smarter merging.
'''
def __init__(self, a, b):
'''Construct a Progress object. a and b must be numbers, and must have
0 <= a <= b and b > 0
'''
# Want to do all checking at construction time, so explicitly check types
if not (isinstance(a, numbers.Number) and
isinstance(b, numbers.Number)):
raise TypeError('a and b must be numbers. Passed {0}/{1}'.format(a, b))
if not (0 <= a <= b and b > 0):
raise ValueError(
'fraction a/b = {0}/{1} must have 0 <= a <= b and b > 0'.format(a, b))
self._a = a
self._b = b
def frac(self):
''' Return tuple (a,b) representing progress of a/b'''
return (self._a, self._b)
def percent(self):
''' Returns a percentage progress as a float between 0 and 100.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return 100.0 * a / b
def started(self):
''' Returns True if fractional progress is greater than 0.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
return self.frac()[0] > 0
def inprogress(self):
''' Returns True if fractional progress is strictly between 0 and 1.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return a > 0 and a < b
def done(self):
''' Return True if this represents done.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return a==b
def ternary_str(self):
''' Return a string version of this progress: either
"none", "in_progress", or "done".
subclassing note: implemented in terms of frac()
'''
(a, b) = self.frac()
if a == 0:
return "none"
if a < b:
return "in_progress"
return "done"
def __eq__(self, other):
''' Two Progress objects are equal if they have identical values.
Implemented in terms of frac()'''
if not isinstance(other, Progress):
return False
(a, b) = self.frac()
(a2, b2) = other.frac()
return a == a2 and b == b2
def __ne__(self, other):
''' The opposite of equal'''
return not self.__eq__(other)
def __str__(self):
''' Return a string representation of this string.
subclassing note: implemented in terms of frac().
'''
(a, b) = self.frac()
return "{0}/{1}".format(a, b)
@staticmethod
def add_counts(a, b):
'''Add two progress indicators, assuming that each represents items done:
(a / b) + (c / d) = (a + c) / (b + d).
If either is None, returns the other.
'''
if a is None:
return b
if b is None:
return a
# get numerators + denominators
(n, d) = a.frac()
(n2, d2) = b.frac()
return Progress(n + n2, d + d2)
@staticmethod
def to_js_status_str(progress):
'''
Return the "status string" version of the passed Progress
object that should be passed to js. Use this function when
sending Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return progress.ternary_str()
@staticmethod
def to_js_detail_str(progress):
'''
Return the "detail string" version of the passed Progress
object that should be passed to js. Use this function when
passing Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return str(progress)

View File

@@ -20,7 +20,7 @@ setup(
"abtest = xmodule.abtest_module:ABTestDescriptor",
"book = xmodule.translation_module:TranslateCustomTagDescriptor",
"chapter = xmodule.seq_module:SequenceDescriptor",
"course = xmodule.seq_module:SequenceDescriptor",
"course = xmodule.course_module:CourseDescriptor",
"customtag = xmodule.template_module:CustomTagDescriptor",
"discuss = xmodule.translation_module:TranslateCustomTagDescriptor",
"html = xmodule.html_module:HtmlDescriptor",

View File

@@ -0,0 +1,45 @@
<problem>
<script type="loncapa/python">
# from loncapa import *
x1 = 4 # lc_random(2,4,1)
y1 = 5 # lc_random(3,7,1)
x2 = 10 # lc_random(x1+1,9,1)
y2 = 20 # lc_random(y1+1,15,1)
m = (y2-y1)/(x2-x1)
b = y1 - m*x1
answer = "%s*x+%s" % (m,b)
answer = answer.replace('+-','-')
inverted_m = (x2-x1)/(y2-y1)
inverted_b = b
wrongans = "%s*x+%s" % (inverted_m,inverted_b)
wrongans = wrongans.replace('+-','-')
</script>
<text>
<p>Hints can be provided to students, based on the last response given, as well as the history of responses given. Here is an example of a hint produced by a Formula Response problem.</p>
<p>
What is the equation of the line which passess through ($x1,$y1) and
($x2,$y2)?</p>
<p>The correct answer is <tt>$answer</tt>. A common error is to invert the equation for the slope. Enter <tt>
$wrongans</tt> to see a hint.</p>
</text>
<formularesponse samples="x@-5:5#11" id="11" answer="$answer">
<responseparam description="Numerical Tolerance" type="tolerance" default="0.001" name="tol" />
<text>y = <textline size="25" /></text>
<hintgroup>
<formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad">
</formulahint>
<hintpart on="inversegrad">
<text>You have inverted the slope in the question.</text>
</hintpart>
</hintgroup>
</formularesponse>
</problem>

View File

@@ -0,0 +1,25 @@
<problem >
<text><h2>Example: String Response Problem</h2>
<br/>
</text>
<text>Which US state has Lansing as its capital?</text>
<stringresponse answer="Michigan" type="ci">
<textline size="20" />
<hintgroup>
<stringhint answer="wisconsin" type="cs" name="wisc">
</stringhint>
<stringhint answer="minnesota" type="cs" name="minn">
</stringhint>
<hintpart on="wisc">
<text>The state capital of Wisconsin is Madison.</text>
</hintpart>
<hintpart on="minn">
<text>The state capital of Minnesota is St. Paul.</text>
</hintpart>
<hintpart on="default">
<text>The state you are looking for is also known as the 'Great Lakes State'</text>
</hintpart>
</hintgroup>
</stringresponse>
</problem>

View File

@@ -0,0 +1,29 @@
<problem>
<text>
<h2>Example: Symbolic Math Response Problem</h2>
<p>
A symbolic math response problem presents one or more symbolic math
input fields for input. Correctness of input is evaluated based on
the symbolic properties of the expression entered. The student enters
text, but sees a proper symbolic rendition of the entered formula, in
real time, next to the input box.
</p>
<p>This is a correct answer which may be entered below: </p>
<p><tt>cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]</tt></p>
<script>
from symmath import *
</script>
<text>Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 &amp; 1 \\ 1 &amp; 0 \end{matrix} \right] \right) [/mathjax]
and give the resulting \(2 \times 2\) matrix. <br/>
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>. <br/>
[mathjax]U=[/mathjax] <symbolicresponse cfn="symmath_check" answer="[[cos(theta),I*sin(theta)],[I*sin(theta),cos(theta)]]" options="matrix,imaginaryi" id="filenamedogi0VpEBOWedxsymmathresponse_1" state="unsubmitted">
<textline size="80" math="1" response_id="2" answer_id="1" id="filenamedogi0VpEBOWedxsymmathresponse_2_1"/>
</symbolicresponse>
<br/>
</text>
</text>
</problem>

View File

@@ -411,13 +411,13 @@ class GraderTest(unittest.TestCase):
self.assertAlmostEqual( graded['percent'], 0.7688095238095238 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 )
graded = zeroWeightsGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.2525 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
self.assertEqual( len(graded['grade_breakdown']), 3 )
graded = allZeroWeightsGrader.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.0 )
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
@@ -436,8 +436,6 @@ class GraderTest(unittest.TestCase):
self.assertAlmostEqual( graded['percent'], 0.0 )
self.assertEqual( len(graded['section_breakdown']), 0 )
self.assertEqual( len(graded['grade_breakdown']), 0 )
def test_graderFromConf(self):
@@ -486,12 +484,12 @@ class GraderTest(unittest.TestCase):
graded = homeworkGrader2.grade(self.test_gradesheet)
self.assertAlmostEqual( graded['percent'], 0.11 )
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
#TODO: How do we test failure cases? The parser only logs an error when it can't parse something. Maybe it should throw exceptions?
# --------------------------------------------------------------------------
# Module progress tests
class ProgressTest(unittest.TestCase):
''' Test that basic Progress objects work. A Progress represents a
fraction between 0 and 1.
@@ -501,7 +499,7 @@ class ProgressTest(unittest.TestCase):
half_done = Progress(3, 6)
also_half_done = Progress(1, 2)
done = Progress(7, 7)
def test_create_object(self):
# These should work:
p = Progress(0, 2)

View File

@@ -107,7 +107,7 @@ class CapaModule(XModule):
else:
self.max_attempts = None
self.show_answer = self.metadata.get('showanwser', 'closed')
self.show_answer = self.metadata.get('showanswer', 'closed')
if self.show_answer == "":
self.show_answer = "closed"
@@ -135,7 +135,7 @@ class CapaModule(XModule):
try:
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), instance_state, seed=seed, system=self.system)
except Exception:
msg = 'cannot create LoncapaProblem %s' % self.location.url
msg = 'cannot create LoncapaProblem %s' % self.location.url()
log.exception(msg)
if self.system.DEBUG:
msg = '<p>%s</p>' % msg.replace('<', '&lt;')
@@ -182,7 +182,7 @@ class CapaModule(XModule):
try:
return Progress(score, total)
except Exception as err:
if self.DEBUG:
if self.system.DEBUG:
return None
raise
return None
@@ -201,9 +201,9 @@ class CapaModule(XModule):
try:
html = self.lcp.get_html()
except Exception, err:
if self.DEBUG:
if self.system.DEBUG:
log.exception(err)
msg = '[courseware.capa.capa_module] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.filename)
msg = '[courseware.capa.capa_module] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.location.url())
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<','&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','&lt;')
html = msg
@@ -274,7 +274,7 @@ class CapaModule(XModule):
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
return html
return self.system.replace_urls(html, self.metadata['data_dir'])
def handle_ajax(self, dispatch, get):
'''
@@ -418,7 +418,7 @@ class CapaModule(XModule):
# TODO: why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
if self.DEBUG:
if self.system.DEBUG:
msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc()
return {'success':msg}

View File

@@ -0,0 +1,95 @@
from fs.errors import ResourceNotFoundError
import logging
from path import path
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule
from fs.errors import ResourceNotFoundError
log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
@classmethod
def id_to_location(cls, course_id):
org, course, name = course_id.split('/')
return Location('i4x', org, course, 'course', name)
@property
def id(self):
return "/".join([self.location.org, self.location.course, self.location.name])
@property
def title(self):
self.metadata['display_name']
@property
def instructors(self):
return self.get_about_section("instructors").split("\n")
def get_about_section(self, section_key):
"""
This returns the snippet of html to be rendered on the course about page, given the key for the section.
Valid keys:
- title
- university
- number
- short_description
- description
- key_dates (includes start, end, exams, etc)
- video
- course_staff_short
- course_staff_extended
- requirements
- syllabus
- textbook
- faq
- more_info
"""
# Many of these are stored as html files instead of some semantic markup. This can change without effecting
# this interface when we find a good format for defining so many snippets of text/html.
# TODO: Remove number, instructors from this list
if section_key in ['short_description', 'description', 'key_dates', 'video', 'course_staff_short', 'course_staff_extended',
'requirements', 'syllabus', 'textbook', 'faq', 'more_info', 'number', 'instructors']:
try:
with self.system.resources_fs.open(path("about") / section_key + ".html") as htmlFile:
return htmlFile.read()
except ResourceNotFoundError:
log.exception("Missing about section {key} in course {url}".format(key=section_key, url=self.location.url()))
return "! About section missing !"
elif section_key == "title":
return self.metadata.get('display_name', self.name)
elif section_key == "university":
return self.location.org
elif section_key == "number":
return self.number
raise KeyError("Invalid about key " + str(section_key))
def get_info_section(self, section_key):
"""
This returns the snippet of html to be rendered on the course info page, given the key for the section.
Valid keys:
- handouts
- guest_handouts
- updates
- guest_updates
"""
# Many of these are stored as html files instead of some semantic markup. This can change without effecting
# this interface when we find a good format for defining so many snippets of text/html.
if section_key in ['handouts', 'guest_handouts', 'updates', 'guest_updates']:
try:
with self.system.resources_fs.open(path("info") / section_key + ".html") as htmlFile:
return htmlFile.read()
except ResourceNotFoundError:
log.exception("Missing info section {key} in course {url}".format(key=section_key, url=self.location.url()))
return "! Info section missing !"
raise KeyError("Invalid about key " + str(section_key))

View File

@@ -128,6 +128,10 @@ class Location(_LocationBase):
return "-".join(str(v) for v in self.list() if v is not None).replace('.', '_')
def dict(self):
"""
Return an OrderedDict of this locations keys and values. The order is
tag, org, course, category, name, revision
"""
return self._asdict()
def list(self):

View File

@@ -4,18 +4,21 @@ from importlib import import_module
from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem
from mitxmako.shortcuts import render_to_string
from bson.son import SON
from itertools import repeat
from . import ModuleStore, Location
from .exceptions import ItemNotFoundError, InsufficientSpecificationError
# TODO (cpennington): This code currently operates under the assumption that
# there is only one revision for each item. Once we start versioning inside the CMS,
# that assumption will have to change
def location_to_query(loc):
query = {}
query = SON()
# Location dict is ordered by specificity, and SON
# will preserve that order for queries
for key, val in Location(loc).dict().iteritems():
if val is not None:
query['_id.{key}'.format(key=key)] = val
@@ -35,6 +38,10 @@ class MongoModuleStore(ModuleStore):
# Force mongo to report errors, at the expense of performance
self.collection.safe = True
# Force mongo to maintain an index over _id.* that is in the same order
# that is used when querying by a location
self.collection.ensure_index(zip(('_id.' + field for field in Location._fields), repeat(1)))
module_path, _, class_name = default_class.rpartition('.')
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
@@ -77,7 +84,6 @@ class MongoModuleStore(ModuleStore):
return self._load_item(item)
def get_items(self, location, default_class=None):
print location_to_query(location)
items = self.collection.find(
location_to_query(location),
sort=[('revision', pymongo.ASCENDING)],

View File

@@ -5,6 +5,7 @@ from lxml import etree
from path import path
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
from xmodule.mako_module import MakoDescriptorSystem
import os
from . import ModuleStore, Location
from .exceptions import ItemNotFoundError
@@ -19,17 +20,21 @@ class XMLModuleStore(ModuleStore):
"""
An XML backed ModuleStore
"""
def __init__(self, org, course, data_dir, default_class=None, eager=False):
def __init__(self, data_dir, default_class=None, eager=False, course_dirs=None):
"""
Initialize an XMLModuleStore from data_dir
org, course: Strings to be used in module keys
data_dir: path to data directory containing course.xml
data_dir: path to data directory containing the course directories
default_class: dot-separated string defining the default descriptor class to use if non is specified in entry_points
eager: If true, load the modules children immediately to force the entire course tree to be parsed
course_dirs: If specified, the list of course_dirs to load. Otherwise, load
all course dirs
"""
self.eager = eager
self.data_dir = path(data_dir)
self.modules = {}
self.courses = {}
if default_class is None:
self.default_class = None
@@ -42,7 +47,40 @@ class XMLModuleStore(ModuleStore):
log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager,self.data_dir))
log.debug('default_class = %s' % self.default_class)
with open(self.data_dir / "course.xml") as course_file:
for course_dir in os.listdir(self.data_dir):
if course_dirs is not None and course_dir not in course_dirs:
continue
if not os.path.exists(self.data_dir / course_dir / "course.xml"):
continue
course_descriptor = self.load_course(course_dir)
self.courses[course_dir] = course_descriptor
def load_course(self, course_dir):
"""
Load a course into this module store
course_path: Course directory name
"""
with open(self.data_dir / course_dir / "course.xml") as course_file:
course_data = etree.parse(course_file).getroot()
org = course_data.get('org')
if org is None:
log.error("No 'org' attribute set for course in {dir}. Using default 'edx'".format(dir=course_dir))
org = 'edx'
course = course_data.get('course')
if course is None:
log.error("No 'course' attribute set for course in {dir}. Using default '{default}'".format(
dir=course_dir,
default=course_dir
))
course = course_dir
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, modulestore):
"""
@@ -76,21 +114,23 @@ class XMLModuleStore(ModuleStore):
log.debug('==> importing module location %s' % repr(module.location))
modulestore.modules[module.location] = module
if eager:
if modulestore.eager:
module.get_children()
return module
system_kwargs = dict(
render_template=lambda: '',
load_item=modulestore.get_item,
resources_fs=OSFS(data_dir),
resources_fs=OSFS(modulestore.data_dir / course_dir),
process_xml=process_xml
)
MakoDescriptorSystem.__init__(self, **system_kwargs)
XMLParsingSystem.__init__(self, **system_kwargs)
self.course = ImportSystem(self).process_xml(course_file.read())
course_descriptor = ImportSystem(self).process_xml(etree.tostring(course_data))
course_descriptor.metadata['data_dir'] = course_dir
log.debug('========> Done with course import')
return course_descriptor
def get_item(self, location):
"""
@@ -110,6 +150,12 @@ class XMLModuleStore(ModuleStore):
except KeyError:
raise ItemNotFoundError(location)
def get_courses(self):
"""
Returns a list of course descriptors
"""
return self.courses.values()
def create_item(self, location):
raise NotImplementedError("XMLModuleStores are read-only")

View File

@@ -1,6 +1,7 @@
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from lxml import etree
from mako.template import Template
class CustomTagModule(XModule):
@@ -30,9 +31,10 @@ class CustomTagModule(XModule):
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data'])
filename = xmltree.find('impl').text
template_name = xmltree.find('impl').text
params = dict(xmltree.items())
self.html = self.system.render_template(filename, params, namespace='custom_tags')
with self.system.filestore.open('custom_tags/{name}'.format(name=template_name)) as template:
self.html = Template(template.read()).render(**params)
def get_html(self):
return self.html

View File

@@ -211,7 +211,13 @@ class XModuleDescriptor(Plugin):
js_module = None
# A list of metadata that this module can inherit from its parent module
inheritable_metadata = ('graded', 'due', 'graceperiod', 'showanswer', 'rerandomize')
inheritable_metadata = (
'graded', 'due', 'graceperiod', 'showanswer', 'rerandomize',
# This is used by the XMLModuleStore to provide for locations for static files,
# and will need to be removed when that code is removed
'data_dir'
)
# A list of descriptor attributes that must be equal for the discriptors to be
# equal
@@ -249,11 +255,11 @@ class XModuleDescriptor(Plugin):
rerandomize (string): When to generate a newly randomized instance of the module data
"""
self.system = system
self.definition = definition if definition is not None else {}
self.name = Location(kwargs.get('location')).name
self.category = Location(kwargs.get('location')).category
self.location = Location(kwargs.get('location'))
self.metadata = kwargs.get('metadata', {})
self.definition = definition if definition is not None else {}
self.location = Location(kwargs.get('location'))
self.name = self.location.name
self.category = self.location.category
self.shared_state_key = kwargs.get('shared_state_key')
self._child_instances = None

71
lms/askbot/skins/README Normal file
View File

@@ -0,0 +1,71 @@
=============================
Customization of Askbot skins
=============================
The default skin at the moment is in the development, however
it is already possible to start customizing your site without
incurring much maintenance overhead.
Current status of templates
===========================
The two busiest templates are - the "main" page and the "question" page,
the main page is more or less complete. "Question" page will be significantly
refactored in the near future.
How skins work in Askbot
========================
The skins reside in up to two directories:
* `askbot/skins` in the source code (contains any stock skins)
* directory pointed to by a ASKBOT_EXTRA_SKINS_DIR in your settings.py
with any other skins
Currently, the skin is selected by the site administrator in the live settings.
Also, at the moment skin default is special - it serves any resources
absent in other skins. In a way - all other skins inherit from the "default".
Templates and media are resolved in the following way:
* check in skin named as in settings.ASKBOT_DEFAULT_SKIN
* then skin named 'default'
How to customize a skin
=======================
There are three options:
* edit custom css via the settings interface - good for small tweaks
(no need to directly log in to the server)
* create a new skin in separate files (need direct access to the server
files, more maintenance overhead)
* directly modify the "default" skin (as in the previous option - need
direct access to the server, less maintenance overhead, some
knowledge of git system is required)
The first option only allows to modify css and add custom javascript.
The latter two options allow changing the templates as well.
If you wish to follow the second option, create a directory named the same
way as the skin you are building and start adding files with the same names
and relative locations as those in the "default" skin.
NO NEED TO CREATE ALL TEMPLATES/MEDIA FILES AT ONCE as your skin will inherit
pieces from the "default".
The disadvantage of thil second approach is that you will be on your own maintaining
the synchrony of your template, stylesheet and the core code.
Third approach is the best, but it requires (the most basic) use of
git source code management software. With git you will easily merge the updates
from the development repository.
Structure of the skin directories
=================================
Todo.
To simplify maintenance of the css as the skin is being developed,
populate css file `media/style/extra.css` with any rules that will
override those in the `media/style/style.css` file. If you do that
media does not have to be composed of files named the same way as in default skin
whatever media you link to from your templates - will be in operation

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 B

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Some files were not shown because too many files have changed in this diff Show More