Merge pull request #163 from MITx/dormsbee/multicourse
Enable multicourse
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({}))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
128
cms/static/sass/_module-header.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
cms/static/sass/_problem.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
cms/static/sass/_video.scss
Normal 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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
242
cms/templates/widgets/captions.html
Normal 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'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's start with the\ngraphical method.",
|
||||
"And my circuit is on the\nright-hand side.",
|
||||
"And I'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'm going to\nsolve these using the",
|
||||
"graphical method.",
|
||||
"In order to do so here's what\nI'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'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'm going to",
|
||||
"move vD minus V divided by\nr do the right hand side.",
|
||||
"So let'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've simply\nnot done much here.",
|
||||
"I just have done some\nrearranging of the terms in",
|
||||
"equation 1.",
|
||||
"And since I haven't done\nanything unique and different,",
|
||||
"I'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'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're trying to\ndo is find a solution, find",
|
||||
"a value for vD and iD that\nsatisfies both equations, 1",
|
||||
"prime and 2.",
|
||||
"that's all we're trying to do.",
|
||||
"It's just math here.",
|
||||
"There's no circuits here.",
|
||||
"We're just doing some relatively\nsimple math.",
|
||||
"We just have to figure out what\niD and vD are, and we'll",
|
||||
"use the graphical method.",
|
||||
"So to do the graphical method,\nwhat I'm going to do is I'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'm going\nto plot iD equals a",
|
||||
"raised to ae bvD.",
|
||||
"I'm going to plot iD equals\na times e raised to bvD.",
|
||||
"That's my equation 2.",
|
||||
"And this is the plot\nthat I get for it.",
|
||||
"It's a plot that I had before.",
|
||||
"So that'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'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'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'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'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'm\nnot going to let vD and iD be",
|
||||
"anywhere else but\non this curve.",
|
||||
"That's it.",
|
||||
"Both of them are fighting with\neach other and telling each",
|
||||
"other, nope, I'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'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'll pick the same parameters\nas I had done when",
|
||||
"I did the analytical method.",
|
||||
"Next, what I'll do is I'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'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'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'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'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'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'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's a name\nfor this line.",
|
||||
"So this line is called the "load\nline." You will see more",
|
||||
"reasons for this later.",
|
||||
"But this line is called\nthe "load line."",
|
||||
"Let me show you one other little\ntrick, just in case you",
|
||||
"didn'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'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'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>
|
||||
|
||||
3
cms/templates/widgets/raw-videos.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<li>
|
||||
<img src="http://placehold.it/300x180" alt="" /><h5>Video-file-name</h5>
|
||||
</li>
|
||||
4
cms/templates/widgets/save-captions.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<section class="caption-save">
|
||||
<a href="#" class="close-box">Cancel</a>
|
||||
<button class="close-box">Save changes</button>
|
||||
</section>
|
||||
187
cms/templates/widgets/sequnce-edit.html
Normal 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>
|
||||
|
||||
7
cms/templates/widgets/speed-tooltip.html
Normal 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>
|
||||
38
cms/templates/widgets/video-box-unused.html
Normal 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>
|
||||
|
||||
35
cms/templates/widgets/video-box.html
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,5 +4,5 @@ setup(
|
||||
name="capa",
|
||||
version="0.1",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
install_requires=['distribute'],
|
||||
install_requires=['distribute', 'pyparsing'],
|
||||
)
|
||||
|
||||
160
common/lib/xmodule/progress.py
Normal 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)
|
||||
@@ -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",
|
||||
|
||||
45
common/lib/xmodule/test_files/formularesponse_with_hint.xml
Normal 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>
|
||||
|
||||
25
common/lib/xmodule/test_files/stringresponse_with_hint.xml
Normal 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>
|
||||
29
common/lib/xmodule/test_files/symbolicresponse.xml
Normal 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 & 1 \\ 1 & 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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('<', '<')
|
||||
@@ -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('<','<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','<')
|
||||
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}
|
||||
|
||||
95
common/lib/xmodule/xmodule/course_module.py
Normal 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))
|
||||
@@ -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):
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
0
lms/askbot/skins/__init__.py
Normal file
BIN
lms/askbot/skins/common/media/images/anon.png
Normal file
|
After Width: | Height: | Size: 687 B |
BIN
lms/askbot/skins/common/media/images/bigbutton.png
Normal file
|
After Width: | Height: | Size: 263 B |
BIN
lms/askbot/skins/common/media/images/bigbuttonhover.png
Normal file
|
After Width: | Height: | Size: 236 B |
BIN
lms/askbot/skins/common/media/images/blue-up-arrow-h18px.png
Executable file
|
After Width: | Height: | Size: 593 B |
BIN
lms/askbot/skins/common/media/images/box-arrow.gif
Executable file
|
After Width: | Height: | Size: 69 B |
BIN
lms/askbot/skins/common/media/images/bullet_green.gif
Executable file
|
After Width: | Height: | Size: 64 B |
BIN
lms/askbot/skins/common/media/images/cc-88x31.png
Executable file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
lms/askbot/skins/common/media/images/cc-by-sa.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
lms/askbot/skins/common/media/images/close-small-dark.png
Executable file
|
After Width: | Height: | Size: 226 B |
BIN
lms/askbot/skins/common/media/images/close-small-hover.png
Executable file
|
After Width: | Height: | Size: 337 B |
BIN
lms/askbot/skins/common/media/images/close-small.png
Executable file
|
After Width: | Height: | Size: 293 B |
BIN
lms/askbot/skins/common/media/images/contributorsback.png
Normal file
|
After Width: | Height: | Size: 714 B |
BIN
lms/askbot/skins/common/media/images/dash.gif
Executable file
|
After Width: | Height: | Size: 44 B |
BIN
lms/askbot/skins/common/media/images/dialog-warning-off.png
Normal file
|
After Width: | Height: | Size: 419 B |
BIN
lms/askbot/skins/common/media/images/dialog-warning.png
Normal file
|
After Width: | Height: | Size: 603 B |
BIN
lms/askbot/skins/common/media/images/djangomade124x25_grey.gif
Executable file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
lms/askbot/skins/common/media/images/dot-g.gif
Executable file
|
After Width: | Height: | Size: 61 B |
BIN
lms/askbot/skins/common/media/images/dot-list.gif
Executable file
|
After Width: | Height: | Size: 56 B |
BIN
lms/askbot/skins/common/media/images/edit.png
Executable file
|
After Width: | Height: | Size: 758 B |
BIN
lms/askbot/skins/common/media/images/expander-arrow-hide.gif
Executable file
|
After Width: | Height: | Size: 126 B |
BIN
lms/askbot/skins/common/media/images/expander-arrow-show.gif
Executable file
|
After Width: | Height: | Size: 135 B |
BIN
lms/askbot/skins/common/media/images/favicon.gif
Normal file
|
After Width: | Height: | Size: 78 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
lms/askbot/skins/common/media/images/feed-icon-small.png
Normal file
|
After Width: | Height: | Size: 669 B |
BIN
lms/askbot/skins/common/media/images/flags/ad.gif
Executable file
|
After Width: | Height: | Size: 371 B |
BIN
lms/askbot/skins/common/media/images/flags/ae.gif
Executable file
|
After Width: | Height: | Size: 361 B |
BIN
lms/askbot/skins/common/media/images/flags/af.gif
Executable file
|
After Width: | Height: | Size: 369 B |
BIN
lms/askbot/skins/common/media/images/flags/ag.gif
Executable file
|
After Width: | Height: | Size: 361 B |
BIN
lms/askbot/skins/common/media/images/flags/ai.gif
Executable file
|
After Width: | Height: | Size: 369 B |
BIN
lms/askbot/skins/common/media/images/flags/al.gif
Executable file
|
After Width: | Height: | Size: 370 B |
BIN
lms/askbot/skins/common/media/images/flags/am.gif
Executable file
|
After Width: | Height: | Size: 363 B |
BIN
lms/askbot/skins/common/media/images/flags/an.gif
Executable file
|
After Width: | Height: | Size: 368 B |
BIN
lms/askbot/skins/common/media/images/flags/ao.gif
Normal file
|
After Width: | Height: | Size: 244 B |
BIN
lms/askbot/skins/common/media/images/flags/ar.gif
Executable file
|
After Width: | Height: | Size: 366 B |
BIN
lms/askbot/skins/common/media/images/flags/as.gif
Executable file
|
After Width: | Height: | Size: 365 B |
BIN
lms/askbot/skins/common/media/images/flags/at.gif
Executable file
|
After Width: | Height: | Size: 361 B |
BIN
lms/askbot/skins/common/media/images/flags/au.gif
Executable file
|
After Width: | Height: | Size: 378 B |
BIN
lms/askbot/skins/common/media/images/flags/aw.gif
Executable file
|
After Width: | Height: | Size: 365 B |
BIN
lms/askbot/skins/common/media/images/flags/ax.gif
Executable file
|
After Width: | Height: | Size: 376 B |
BIN
lms/askbot/skins/common/media/images/flags/az.gif
Executable file
|
After Width: | Height: | Size: 370 B |
BIN
lms/askbot/skins/common/media/images/flags/ba.gif
Executable file
|
After Width: | Height: | Size: 363 B |
BIN
lms/askbot/skins/common/media/images/flags/bb.gif
Executable file
|
After Width: | Height: | Size: 368 B |
BIN
lms/askbot/skins/common/media/images/flags/bd.gif
Executable file
|
After Width: | Height: | Size: 361 B |
BIN
lms/askbot/skins/common/media/images/flags/be.gif
Executable file
|
After Width: | Height: | Size: 359 B |
BIN
lms/askbot/skins/common/media/images/flags/bf.gif
Executable file
|
After Width: | Height: | Size: 358 B |
BIN
lms/askbot/skins/common/media/images/flags/bg.gif
Executable file
|
After Width: | Height: | Size: 360 B |
BIN
lms/askbot/skins/common/media/images/flags/bh.gif
Executable file
|
After Width: | Height: | Size: 367 B |
BIN
lms/askbot/skins/common/media/images/flags/bi.gif
Executable file
|
After Width: | Height: | Size: 374 B |
BIN
lms/askbot/skins/common/media/images/flags/bj.gif
Executable file
|
After Width: | Height: | Size: 368 B |
BIN
lms/askbot/skins/common/media/images/flags/bm.gif
Executable file
|
After Width: | Height: | Size: 367 B |
BIN
lms/askbot/skins/common/media/images/flags/bn.gif
Executable file
|
After Width: | Height: | Size: 373 B |
BIN
lms/askbot/skins/common/media/images/flags/bo.gif
Executable file
|
After Width: | Height: | Size: 359 B |
BIN
lms/askbot/skins/common/media/images/flags/br.gif
Executable file
|
After Width: | Height: | Size: 367 B |
BIN
lms/askbot/skins/common/media/images/flags/bs.gif
Executable file
|
After Width: | Height: | Size: 351 B |
BIN
lms/askbot/skins/common/media/images/flags/bt.gif
Executable file
|
After Width: | Height: | Size: 377 B |
BIN
lms/askbot/skins/common/media/images/flags/bv.gif
Executable file
|
After Width: | Height: | Size: 376 B |
BIN
lms/askbot/skins/common/media/images/flags/bw.gif
Executable file
|
After Width: | Height: | Size: 364 B |
BIN
lms/askbot/skins/common/media/images/flags/by.gif
Executable file
|
After Width: | Height: | Size: 361 B |
BIN
lms/askbot/skins/common/media/images/flags/bz.gif
Executable file
|
After Width: | Height: | Size: 368 B |
BIN
lms/askbot/skins/common/media/images/flags/ca.gif
Executable file
|
After Width: | Height: | Size: 376 B |
BIN
lms/askbot/skins/common/media/images/flags/catalonia.gif
Normal file
|
After Width: | Height: | Size: 238 B |
BIN
lms/askbot/skins/common/media/images/flags/cc.gif
Executable file
|
After Width: | Height: | Size: 371 B |
BIN
lms/askbot/skins/common/media/images/flags/cd.gif
Normal file
|
After Width: | Height: | Size: 243 B |