Commit fdd96636 by Calen Pennington

Merge remote-tracking branch 'origin/master' into dormsbee/multicourse

Conflicts:
	common/lib/xmodule/xmodule/capa_module.py
	common/lib/xmodule/xmodule/modulestore/xml.py
	lms/djangoapps/courseware/views.py
	lms/templates/index.html
	lms/templates/info.html
	lms/templates/main.html
	lms/templates/navigation.html
	requirements.txt
parents 74593d18 6965e006
GEM
remote: http://rubygems.org/
specs:
bourbon (1.3.6)
sass (>= 3.1)
rake (0.9.2.2)
sass (3.1.15)
PLATFORMS
ruby
DEPENDENCIES
bourbon (~> 1.3.6)
rake
sass (= 3.1.15)
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
import logging
log = logging.getLogger(__name__)
def import_from_xml(org, course, data_dir):
"""
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)
for module in module_store.modules.itervalues():
# TODO (cpennington): This forces import to overrite the same items.
# This should in the future create new revisions of the items on import
try:
modulestore().create_item(module.location)
except:
log.exception('Item already exists at %s' % module.location.url())
pass
if 'data' in module.definition:
modulestore().update_item(module.location, module.definition['data'])
if 'children' in module.definition:
modulestore().update_children(module.location, module.definition['children'])
modulestore().update_metadata(module.location, dict(module.metadata))
return module_store.course
......@@ -3,8 +3,7 @@
###
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
from contentstore import import_from_xml
unnamed_modules = 0
......@@ -18,12 +17,4 @@ class Command(BaseCommand):
raise CommandError("import requires 3 arguments: <org> <course> <data directory>")
org, course, data_dir = args
module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor', eager=True)
for module in module_store.modules.itervalues():
modulestore().create_item(module.location)
if 'data' in module.definition:
modulestore().update_item(module.location, module.definition['data'])
if 'children' in module.definition:
modulestore().update_children(module.location, module.definition['children'])
modulestore().update_metadata(module.location, dict(module.metadata))
import_from_xml(org, course, data_dir)
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from django_future.csrf import ensure_csrf_cookie
from django.http import HttpResponse
from util.json_request import expect_json
import json
from django.http import HttpResponse
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 mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
@ensure_csrf_cookie
def index(request):
courses = modulestore().get_items(['i4x', None, None, 'course', None])
return render_to_response('index.html', {
'courses': [(course.metadata['display_name'],
reverse('course_index', args=[
course.location.org,
course.location.course,
course.location.name]))
for course in courses]
})
@ensure_csrf_cookie
def course_index(request, org, course, name):
# TODO (cpennington): These need to be read in from the active user
org = 'mit.edu'
course = '6002xs12'
name = '6.002_Spring_2012'
course = modulestore().get_item(['i4x', org, course, 'course', name])
weeks = course.get_children()
return render_to_response('index.html', {'weeks': weeks})
return render_to_response('course_index.html', {'weeks': weeks})
def edit_item(request):
......@@ -28,10 +44,19 @@ def edit_item(request):
})
@expect_json
def save_item(request):
item_id = request.POST['id']
data = json.loads(request.POST['data'])
modulestore().update_item(item_id, data)
# Export the course back to github
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")
return HttpResponse(json.dumps({}))
......
import logging
import os
from django.conf import settings
from fs.osfs import OSFS
from git import Repo, PushInfo
from contentstore import import_from_xml
from xmodule.modulestore import Location
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
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']
if not os.path.isdir(repo_path):
Repo.clone_from(repo_settings['origin'], repo_path)
git_repo = Repo(repo_path)
origin = git_repo.remotes.origin
origin.fetch()
# 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)
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):
fs = OSFS(repo_path)
xml = course.export_to_xml(fs)
with fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml)
git_repo = Repo(repo_path)
if git_repo.is_dirty():
git_repo.git.add(A=True)
git_repo.git.commit(m=commit_message)
origin = git_repo.remotes.origin
if settings.MITX_FEATURES['GITHUB_PUSH']:
push_infos = origin.push()
if len(push_infos) > 1:
log.error('Unexpectedly pushed multiple heads: {infos}'.format(
infos="\n".join(str(info.summary) for info in push_infos)
))
if push_infos[0].flags & PushInfo.ERROR:
log.error('Failed push: flags={p.flags}, local_ref={p.local_ref}, '
'remote_ref_string={p.remote_ref_string}, '
'remote_ref={p.remote_ref}, old_commit={p.old_commit}, '
'summary={p.summary})'.format(p=push_infos[0]))
raise GithubSyncError('Failed to push: {info}'.format(
info=str(push_infos[0].summary)
))
class GithubSyncError(Exception):
pass
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 git import Repo
from django.conf import settings
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from override_settings import override_settings
from github_sync.exceptions import GithubSyncError
class GithubSyncTestCase(TestCase):
def setUp(self):
self.working_dir = path(settings.TEST_ROOT)
self.repo_dir = self.working_dir / 'local_repo'
self.remote_dir = self.working_dir / 'remote_repo'
shutil.copytree('common/test/data/toy', self.remote_dir)
remote = Repo.init(self.remote_dir)
remote.git.add(A=True)
remote.git.commit(m='Initial commit')
remote.git.config("receive.denyCurrentBranch", "ignore")
modulestore().collection.drop()
self.import_revision, self.import_course = import_from_github({
'path': self.repo_dir,
'origin': self.remote_dir,
'branch': 'master',
'org': 'org',
'course': 'course'
})
def tearDown(self):
shutil.rmtree(self.repo_dir)
shutil.rmtree(self.remote_dir)
def test_initialize_repo(self):
"""
Test that importing from github will create a repo if the repo doesn't already exist
"""
self.assertEquals(1, len(Repo(self.repo_dir).head.reference.log()))
def test_import_contents(self):
"""
Test that the import loads the correct course into the modulestore
"""
self.assertEquals('Toy Course', self.import_course.metadata['display_name'])
self.assertIn(
Location('i4x://org/course/chapter/Overview'),
[child.location for child in self.import_course.get_children()])
self.assertEquals(1, len(self.import_course.get_children()))
@override_settings(MITX_FEATURES={'GITHUB_PUSH': False})
def test_export_no_pash(self):
"""
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')
self.assertEquals(1, Repo(self.remote_dir).head.commit.count())
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
def test_export_push(self):
"""
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')
self.assertEquals(2, Repo(self.remote_dir).head.commit.count())
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
def test_export_conflict(self):
"""
Test that if there is a conflict when pushing to the remote repo, nothing is pushed and an exception is raised
"""
self.import_course.metadata['display_name'] = 'Changed display name'
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.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'))
import json
from django.test.client import Client
from django.test import TestCase
from mock import patch, Mock
from override_settings import override_settings
from django.conf import settings
@override_settings(REPOS={'repo': {'path': 'path', 'branch': 'branch'}})
class PostReceiveTestCase(TestCase):
def setUp(self):
self.client = Client()
@patch('github_sync.views.export_to_github')
@patch('github_sync.views.import_from_github')
def test_non_branch(self, import_from_github, export_to_github):
self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/tags/foo'})
})
self.assertFalse(import_from_github.called)
self.assertFalse(export_to_github.called)
@patch('github_sync.views.export_to_github')
@patch('github_sync.views.import_from_github')
def test_non_watched_repo(self, import_from_github, export_to_github):
self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/branch',
'repository': {'name': 'bad_repo'}})
})
self.assertFalse(import_from_github.called)
self.assertFalse(export_to_github.called)
@patch('github_sync.views.export_to_github')
@patch('github_sync.views.import_from_github')
def test_non_tracked_branch(self, import_from_github, export_to_github):
self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/non_branch',
'repository': {'name': 'repo'}})
})
self.assertFalse(import_from_github.called)
self.assertFalse(export_to_github.called)
@patch('github_sync.views.export_to_github')
@patch('github_sync.views.import_from_github', return_value=(Mock(), Mock()))
def test_tracked_branch(self, import_from_github, export_to_github):
self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/branch',
'repository': {'name': 'repo'}})
})
import_from_github.assert_called_with(settings.REPOS['repo'])
mock_revision, mock_course = import_from_github.return_value
export_to_github.assert_called_with(mock_course, 'path', "Changes from cms import of revision %s" % mock_revision)
import logging
import json
from django.http import HttpResponse
from django.conf import settings
from django_future.csrf import csrf_exempt
from . import import_from_github, export_to_github
log = logging.getLogger()
@csrf_exempt
def github_post_receive(request):
"""
This view recieves post-receive requests from github whenever one of
the watched repositiories changes.
It is responsible for updating the relevant local git repo,
importing the new version of the course (if anything changed),
and then pushing back to github any changes that happened as part of the
import.
The github request format is described here: https://help.github.com/articles/post-receive-hooks
"""
payload = json.loads(request.POST['payload'])
ref = payload['ref']
if not ref.startswith('refs/heads/'):
log.info('Ignore changes to non-branch ref %s' % ref)
return HttpResponse('Ignoring non-branch')
branch_name = ref.replace('refs/heads/', '', 1)
repo_name = payload['repository']['name']
if repo_name not in settings.REPOS:
log.info('No repository matching %s found' % repo_name)
return HttpResponse('No Repo Found')
repo = settings.REPOS[repo_name]
if repo['branch'] != branch_name:
log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name))
return HttpResponse('Ignoring non-tracked branch')
revision, course = import_from_github(repo)
export_to_github(course, repo['path'], "Changes from cms import of revision %s" % revision)
return HttpResponse('Push recieved')
......@@ -24,12 +24,14 @@ import tempfile
import os.path
import os
import errno
import glob2
from path import path
############################ FEATURE CONFIGURATION #############################
MITX_FEATURES = {
'USE_DJANGO_PIPELINE': True,
'GITHUB_PUSH': False,
}
############################# SET PATH INFORMATION #############################
......@@ -57,6 +59,10 @@ MAKO_TEMPLATES['main'] = [
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates'
]
TEMPLATE_DIRS = (
PROJECT_ROOT / "templates",
)
MITX_ROOT_URL = ''
TEMPLATE_CONTEXT_PROCESSORS = (
......@@ -67,6 +73,9 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.csrf', # necessary for csrf protection
)
################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
################################# Middleware ###################################
# List of finder classes that know how to find static files in
# various locations.
......@@ -183,12 +192,16 @@ for xmodule in XModuleDescriptor.load_classes() + [RawDescriptor]:
PIPELINE_JS = {
'main': {
'source_filenames': ['coffee/main.coffee', 'coffee/unit.coffee'],
'output_filename': 'js/main.js',
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee')],
'output_filename': 'js/application.js',
},
'module-js': {
'source_filenames': module_js_sources,
'output_filename': 'js/modules.js',
},
'spec': {
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')],
'output_filename': 'js/spec.js'
}
}
......@@ -232,4 +245,7 @@ INSTALLED_APPS = (
# For asset pipelining
'pipeline',
'staticfiles',
# For testing
'django_jasmine',
)
......@@ -29,8 +29,48 @@ DATABASES = {
}
}
REPO_ROOT = ENV_ROOT / "content"
REPOS = {
'edx4edx': {
'path': REPO_ROOT / "edx4edx",
'org': 'edx',
'course': 'edx4edx',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/edx4edx.git',
},
'6002x-fall-2012': {
'path': REPO_ROOT / '6002x-fall-2012',
'org': 'mit.edu',
'course': '6.002x',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/6002x-fall-2012.git',
},
'6.00x': {
'path': REPO_ROOT / '6.00x',
'org': 'mit.edu',
'course': '6.00x',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/6.00x.git',
},
'7.00x': {
'path': REPO_ROOT / '7.00x',
'org': 'mit.edu',
'course': '7.00x',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/7.00x.git',
},
'3.091x': {
'path': REPO_ROOT / '3.091x',
'org': 'mit.edu',
'course': '3.091x',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/3.091x.git',
},
}
CACHES = {
# This is the cache used for most things. Askbot will not work without a
# This is the cache used for most things. Askbot will not work without a
# functioning cache -- it relies on caching to load its settings in places.
# In staging/prod envs, the sessions also live here.
'default': {
......
......@@ -17,10 +17,18 @@ for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
TEST_ROOT = 'test_root'
MODULESTORE = {
'host': 'localhost',
'db': 'mongo_base',
'collection': 'key_store',
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
}
}
}
DATABASES = {
......
{
"js_files": [
"/static/js/vendor/jquery.min.js",
"/static/js/vendor/json2.js",
"/static/js/vendor/underscore-min.js",
"/static/js/vendor/backbone-min.js"
]
}
class @CMS
@setHeight = =>
windowHeight = $(this).height()
@contentHeight = windowHeight - 29
@bind = =>
$('a.module-edit').click ->
CMS.edit_item($(this).attr('id'))
return false
$(window).bind('resize', CMS.setHeight)
@edit_item = (id) =>
$.get('/edit_item', {id: id}, (data) =>
$('#module-html').empty().append(data)
CMS.bind()
$('body.content .cal').css('height', @contentHeight)
$('body').addClass('content')
$('section.edit-pane').show()
new Unit('unit-wrapper', id)
)
$ ->
$.ajaxSetup
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
$('section.main-content').children().hide()
$('.editable').inlineEdit()
$('.editable-textarea').inlineEdit({control: 'textarea'})
heighest = 0
$('.cal ol > li').each ->
heighest = if $(this).height() > heighest then $(this).height() else heighest
$('.cal ol > li').css('height',heighest + 'px')
$('.add-new-section').click -> return false
$('.new-week .close').click ->
$(this).parents('.new-week').hide()
$('p.add-new-week').show()
return false
$('.save-update').click ->
$(this).parent().parent().hide()
return false
# $('html').keypress ->
# $('.wip').css('visibility', 'visible')
setHeight = ->
windowHeight = $(this).height()
contentHeight = windowHeight - 29
$('section.main-content > section').css('min-height', contentHeight)
$('body.content .cal').css('height', contentHeight)
$('.edit-week').click ->
$('body').addClass('content')
$('body.content .cal').css('height', contentHeight)
$('section.edit-pane').show()
return false
$('a.week-edit').click ->
$('body').addClass('content')
$('body.content .cal').css('height', contentHeight)
$('section.edit-pane').show()
return false
$('a.sequence-edit').click ->
$('body').addClass('content')
$('body.content .cal').css('height', contentHeight)
$('section.edit-pane').show()
return false
$('a.module-edit').click ->
$('body.content .cal').css('height', contentHeight)
$(document).ready(setHeight)
$(window).bind('resize', setHeight)
$('.video-new a').click ->
$('section.edit-pane').show()
return false
$('.problem-new a').click ->
$('section.edit-pane').show()
return false
CMS.setHeight()
CMS.bind()
# Stub jQuery.cookie
@stubCookies =
csrftoken: "stubCSRFToken"
jQuery.cookie = (key, value) =>
if value?
@stubCookies[key] = value
else
@stubCookies[key]
# Path Jasmine's `it` method to raise an error when the test is not defined.
# This is helpful when writing the specs first before writing the test.
@it = (desc, func) ->
if func?
jasmine.getEnv().it(desc, func)
else
jasmine.getEnv().it desc, ->
throw "test is undefined"
describe "CMS", ->
beforeEach ->
CMS.unbind()
it "should iniitalize Models", ->
expect(CMS.Models).toBeDefined()
it "should initialize Views", ->
expect(CMS.Views).toBeDefined()
describe "start", ->
beforeEach ->
@element = $("<div>")
spyOn(CMS.Views, "Course").andReturn(jasmine.createSpyObj("Course", ["render"]))
CMS.start(@element)
it "create the Course", ->
expect(CMS.Views.Course).toHaveBeenCalledWith(el: @element)
expect(CMS.Views.Course().render).toHaveBeenCalled()
describe "view stack", ->
beforeEach ->
@currentView = jasmine.createSpy("currentView")
CMS.viewStack = [@currentView]
describe "replaceView", ->
beforeEach ->
@newView = jasmine.createSpy("newView")
CMS.on("content.show", (@expectedView) =>)
CMS.replaceView(@newView)
it "replace the views on the viewStack", ->
expect(CMS.viewStack).toEqual([@newView])
it "trigger content.show on CMS", ->
expect(@expectedView).toEqual(@newView)
describe "pushView", ->
beforeEach ->
@newView = jasmine.createSpy("newView")
CMS.on("content.show", (@expectedView) =>)
CMS.pushView(@newView)
it "push new view onto viewStack", ->
expect(CMS.viewStack).toEqual([@currentView, @newView])
it "trigger content.show on CMS", ->
expect(@expectedView).toEqual(@newView)
describe "popView", ->
it "remove the current view from the viewStack", ->
CMS.popView()
expect(CMS.viewStack).toEqual([])
describe "when there's no view on the viewStack", ->
beforeEach ->
CMS.viewStack = [@currentView]
CMS.on("content.hide", => @eventTriggered = true)
CMS.popView()
it "trigger content.hide on CMS", ->
expect(@eventTriggered).toBeTruthy
describe "when there's previous view on the viewStack", ->
beforeEach ->
@parentView = jasmine.createSpyObj("parentView", ["delegateEvents"])
CMS.viewStack = [@parentView, @currentView]
CMS.on("content.show", (@expectedView) =>)
CMS.popView()
it "trigger content.show with the previous view on CMS", ->
expect(@expectedView).toEqual @parentView
it "re-bind events on the view", ->
expect(@parentView.delegateEvents).toHaveBeenCalled()
describe "main helper", ->
beforeEach ->
@previousAjaxSettings = $.extend(true, {}, $.ajaxSettings)
window.stubCookies["csrftoken"] = "stubCSRFToken"
$(document).ready()
afterEach ->
$.ajaxSettings = @previousAjaxSettings
it "turn on Backbone emulateHTTP", ->
expect(Backbone.emulateHTTP).toBeTruthy()
it "setup AJAX CSRF token", ->
expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken")
describe "CMS.Models.Module", ->
it "set the correct URL", ->
expect(new CMS.Models.Module().url).toEqual("/save_item")
it "set the correct default", ->
expect(new CMS.Models.Module().defaults).toEqual({data: ""})
describe "loadModule", ->
describe "when the module exists", ->
beforeEach ->
@fakeModule = jasmine.createSpy("fakeModuleObject")
window.FakeModule = jasmine.createSpy("FakeModule").andReturn(@fakeModule)
@module = new CMS.Models.Module(type: "FakeModule")
@stubElement = $("<div>")
@module.loadModule(@stubElement)
afterEach ->
window.FakeModule = undefined
it "initialize the module", ->
expect(window.FakeModule).toHaveBeenCalledWith(@stubElement)
expect(@module.module).toEqual(@fakeModule)
describe "when the module does not exists", ->
beforeEach ->
@previousConsole = window.console
window.console = jasmine.createSpyObj("fakeConsole", ["error"])
@module = new CMS.Models.Module(type: "HTML")
@module.loadModule($("<div>"))
afterEach ->
window.console = @previousConsole
it "print out error to log", ->
expect(window.console.error).toHaveBeenCalledWith("Unable to load HTML.")
describe "editUrl", ->
it "construct the correct URL based on id", ->
expect(new CMS.Models.Module(id: "i4x://mit.edu/module/html_123").editUrl())
.toEqual("/edit_item?id=i4x%3A%2F%2Fmit.edu%2Fmodule%2Fhtml_123")
describe "save", ->
beforeEach ->
spyOn(Backbone.Model.prototype, "save")
@module = new CMS.Models.Module()
describe "when the module exists", ->
beforeEach ->
@module.module = jasmine.createSpyObj("FakeModule", ["save"])
@module.module.save.andReturn("module data")
@module.save()
it "set the data and call save on the module", ->
expect(@module.get("data")).toEqual("\"module data\"")
it "call save on the backbone model", ->
expect(Backbone.Model.prototype.save).toHaveBeenCalled()
describe "when the module does not exists", ->
beforeEach ->
@module.save()
it "call save on the backbone model", ->
expect(Backbone.Model.prototype.save).toHaveBeenCalled()
describe "CMS.Views.Course", ->
beforeEach ->
setFixtures """
<section id="main-section">
<section class="main-content"></section>
<ol id="weeks">
<li class="cal week-one" style="height: 50px"></li>
<li class="cal week-two" style="height: 100px"></li>
</ol>
</section>
"""
CMS.unbind()
describe "render", ->
beforeEach ->
spyOn(CMS.Views, "Week").andReturn(jasmine.createSpyObj("Week", ["render"]))
new CMS.Views.Course(el: $("#main-section")).render()
it "create week view for each week",->
expect(CMS.Views.Week.calls[0].args[0])
.toEqual({ el: $(".week-one").get(0), height: 101 })
expect(CMS.Views.Week.calls[1].args[0])
.toEqual({ el: $(".week-two").get(0), height: 101 })
describe "on content.show", ->
beforeEach ->
@view = new CMS.Views.Course(el: $("#main-section"))
@subView = jasmine.createSpyObj("subView", ["render"])
@subView.render.andReturn(el: "Subview Content")
spyOn(@view, "contentHeight").andReturn(100)
CMS.trigger("content.show", @subView)
afterEach ->
$("body").removeClass("content")
it "add content class to body", ->
expect($("body").attr("class")).toEqual("content")
it "replace content in .main-content", ->
expect($(".main-content")).toHaveHtml("Subview Content")
it "set height on calendar", ->
expect($(".cal")).toHaveCss(height: "100px")
it "set minimum height on all sections", ->
expect($("#main-section>section")).toHaveCss(minHeight: "100px")
describe "on content.hide", ->
beforeEach ->
$("body").addClass("content")
@view = new CMS.Views.Course(el: $("#main-section"))
$(".cal").css(height: 100)
$("#main-section>section").css(minHeight: 100)
CMS.trigger("content.hide")
afterEach ->
$("body").removeClass("content")
it "remove content class from body", ->
expect($("body").attr("class")).toEqual("")
it "remove content from .main-content", ->
expect($(".main-content")).toHaveHtml("")
it "reset height on calendar", ->
expect($(".cal")).not.toHaveCss(height: "100px")
it "reset minimum height on all sections", ->
expect($("#main-section>section")).not.toHaveCss(minHeight: "100px")
describe "maxWeekHeight", ->
it "return maximum height of the week element", ->
@view = new CMS.Views.Course(el: $("#main-section"))
expect(@view.maxWeekHeight()).toEqual(101)
describe "contentHeight", ->
beforeEach ->
$("body").append($('<header id="test">').height(100).hide())
afterEach ->
$("body>header#test").remove()
it "return the window height minus the header bar", ->
@view = new CMS.Views.Course(el: $("#main-section"))
expect(@view.contentHeight()).toEqual($(window).height() - 100)
describe "CMS.Views.ModuleEdit", ->
beforeEach ->
@stubModule = jasmine.createSpyObj("Module", ["editUrl", "loadModule"])
spyOn($.fn, "load")
setFixtures """
<div id="module-edit">
<a href="#" class="save-update">save</a>
<a href="#" class="cancel">cancel</a>
<ol>
<li>
<a href="#" class="module-edit" data-id="i4x://mitx.edu/course/module" data-type="html">submodule</a>
</li>
</ol>
</div>
"""
CMS.unbind()
describe "defaults", ->
it "set the correct tagName", ->
expect(new CMS.Views.ModuleEdit(model: @stubModule).tagName).toEqual("section")
it "set the correct className", ->
expect(new CMS.Views.ModuleEdit(model: @stubModule).className).toEqual("edit-pane")
describe "view creation", ->
beforeEach ->
@stubModule.editUrl.andReturn("/edit_item?id=stub_module")
new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule)
it "load the edit from via ajax and pass to the model", ->
expect($.fn.load).toHaveBeenCalledWith("/edit_item?id=stub_module", jasmine.any(Function))
if $.fn.load.mostRecentCall
$.fn.load.mostRecentCall.args[1]()
expect(@stubModule.loadModule).toHaveBeenCalledWith($("#module-edit").get(0))
describe "save", ->
beforeEach ->
@stubJqXHR = jasmine.createSpy("stubJqXHR")
@stubJqXHR.success = jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR)
@stubJqXHR.error= jasmine.createSpy("stubJqXHR.success").andReturn(@stubJqXHR)
@stubModule.save = jasmine.createSpy("stubModule.save").andReturn(@stubJqXHR)
new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule)
spyOn(window, "alert")
$(".save-update").click()
it "call save on the model", ->
expect(@stubModule.save).toHaveBeenCalled()
it "alert user on success", ->
@stubJqXHR.success.mostRecentCall.args[0]()
expect(window.alert).toHaveBeenCalledWith("Your changes have been saved.")
it "alert user on error", ->
@stubJqXHR.error.mostRecentCall.args[0]()
expect(window.alert).toHaveBeenCalledWith("There was an error saving your changes. Please try again.")
describe "cancel", ->
beforeEach ->
spyOn(CMS, "popView")
@view = new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule)
$(".cancel").click()
it "pop current view from viewStack", ->
expect(CMS.popView).toHaveBeenCalled()
describe "editSubmodule", ->
beforeEach ->
@view = new CMS.Views.ModuleEdit(el: $("#module-edit"), model: @stubModule)
spyOn(CMS, "pushView")
spyOn(CMS.Views, "ModuleEdit")
.andReturn(@view = jasmine.createSpy("Views.ModuleEdit"))
spyOn(CMS.Models, "Module")
.andReturn(@model = jasmine.createSpy("Models.Module"))
$(".module-edit").click()
it "push another module editing view into viewStack", ->
expect(CMS.pushView).toHaveBeenCalledWith @view
expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model
expect(CMS.Models.Module).toHaveBeenCalledWith
id: "i4x://mitx.edu/course/module"
type: "html"
describe "CMS.Views.Module", ->
beforeEach ->
setFixtures """
<div id="module" data-id="i4x://mitx.edu/course/module" data-type="html">
<a href="#" class="module-edit">edit</a>
</div>
"""
describe "edit", ->
beforeEach ->
@view = new CMS.Views.Module(el: $("#module"))
spyOn(CMS, "replaceView")
spyOn(CMS.Views, "ModuleEdit")
.andReturn(@view = jasmine.createSpy("Views.ModuleEdit"))
spyOn(CMS.Models, "Module")
.andReturn(@model = jasmine.createSpy("Models.Module"))
$(".module-edit").click()
it "replace the main view with ModuleEdit view", ->
expect(CMS.replaceView).toHaveBeenCalledWith @view
expect(CMS.Views.ModuleEdit).toHaveBeenCalledWith model: @model
expect(CMS.Models.Module).toHaveBeenCalledWith
id: "i4x://mitx.edu/course/module"
type: "html"
describe "CMS.Views.WeekEdit", ->
describe "defaults", ->
it "set the correct tagName", ->
expect(new CMS.Views.WeekEdit().tagName).toEqual("section")
it "set the correct className", ->
expect(new CMS.Views.WeekEdit().className).toEqual("edit-pane")
describe "CMS.Views.Week", ->
beforeEach ->
setFixtures """
<div id="week" data-id="i4x://mitx.edu/course/week">
<div class="editable"></div>
<textarea class="editable-textarea"></textarea>
<a href="#" class="week-edit" >edit</a>
<ul class="modules">
<li id="module-one" class="module"></li>
<li id="module-two" class="module"></li>
</ul>
</div>
"""
CMS.unbind()
describe "render", ->
beforeEach ->
spyOn(CMS.Views, "Module").andReturn(jasmine.createSpyObj("Module", ["render"]))
$.fn.inlineEdit = jasmine.createSpy("$.fn.inlineEdit")
@view = new CMS.Views.Week(el: $("#week"), height: 100).render()
it "set the height of the element", ->
expect(@view.el).toHaveCss(height: "100px")
it "make .editable as inline editor", ->
expect($.fn.inlineEdit.calls[0].object.get(0))
.toEqual($(".editable").get(0))
it "make .editable-test as inline editor", ->
expect($.fn.inlineEdit.calls[1].object.get(0))
.toEqual($(".editable-textarea").get(0))
it "create module subview for each module", ->
expect(CMS.Views.Module.calls[0].args[0])
.toEqual({ el: $("#module-one").get(0) })
expect(CMS.Views.Module.calls[1].args[0])
.toEqual({ el: $("#module-two").get(0) })
describe "edit", ->
beforeEach ->
new CMS.Views.Week(el: $("#week"), height: 100).render()
spyOn(CMS, "replaceView")
spyOn(CMS.Views, "WeekEdit")
.andReturn(@view = jasmine.createSpy("Views.WeekEdit"))
$(".week-edit").click()
it "replace the content with edit week view", ->
expect(CMS.replaceView).toHaveBeenCalledWith @view
expect(CMS.Views.WeekEdit).toHaveBeenCalled()
describe "on content.show", ->
beforeEach ->
@view = new CMS.Views.Week(el: $("#week"), height: 100).render()
@view.$el.height("")
@view.setHeight()
it "set the correct height", ->
expect(@view.el).toHaveCss(height: "100px")
describe "on content.hide", ->
beforeEach ->
@view = new CMS.Views.Week(el: $("#week"), height: 100).render()
@view.$el.height("100px")
@view.resetHeight()
it "remove height from the element", ->
expect(@view.el).not.toHaveCss(height: "100px")
@CMS =
Models: {}
Views: {}
viewStack: []
start: (el) ->
new CMS.Views.Course(el: el).render()
replaceView: (view) ->
@viewStack = [view]
CMS.trigger('content.show', view)
pushView: (view) ->
@viewStack.push(view)
CMS.trigger('content.show', view)
popView: ->
@viewStack.pop()
if _.isEmpty(@viewStack)
CMS.trigger('content.hide')
else
view = _.last(@viewStack)
CMS.trigger('content.show', view)
view.delegateEvents()
_.extend CMS, Backbone.Events
$ ->
Backbone.emulateHTTP = true
$.ajaxSetup
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
CMS.start($('section.main-container'))
class CMS.Models.Module extends Backbone.Model
url: '/save_item'
defaults:
data: ''
loadModule: (element) ->
try
@module = new window[@get('type')](element)
catch TypeError
console.error "Unable to load #{@get('type')}." if console
editUrl: ->
"/edit_item?#{$.param(id: @get('id'))}"
save: (args...) ->
@set(data: JSON.stringify(@module.save())) if @module
super(args...)
class CMS.Views.Course extends Backbone.View
initialize: ->
CMS.on('content.show', @showContent)
CMS.on('content.hide', @hideContent)
render: ->
@$('#weeks > li').each (index, week) =>
new CMS.Views.Week(el: week, height: @maxWeekHeight()).render()
return @
showContent: (subview) =>
$('body').addClass('content')
@$('.main-content').html(subview.render().el)
@$('.cal').css height: @contentHeight()
@$('>section').css minHeight: @contentHeight()
hideContent: =>
$('body').removeClass('content')
@$('.main-content').empty()
@$('.cal').css height: ''
@$('>section').css minHeight: ''
maxWeekHeight: ->
weekElementBorderSize = 1
_.max($('#weeks > li').map -> $(this).height()) + weekElementBorderSize
contentHeight: ->
$(window).height() - $('body>header').outerHeight()
class CMS.Views.Module extends Backbone.View
events:
"click .module-edit": "edit"
edit: (event) =>
event.preventDefault()
CMS.replaceView(new CMS.Views.ModuleEdit(model: new CMS.Models.Module(id: @$el.data('id'), type: @$el.data('type'))))
class CMS.Views.ModuleEdit extends Backbone.View
tagName: 'section'
className: 'edit-pane'
events:
'click .cancel': 'cancel'
'click .module-edit': 'editSubmodule'
'click .save-update': 'save'
initialize: ->
@$el.load @model.editUrl(), =>
@model.loadModule(@el)
save: (event) ->
event.preventDefault()
@model.save().success(->
alert("Your changes have been saved.")
).error(->
alert("There was an error saving your changes. Please try again.")
)
cancel: (event) ->
event.preventDefault()
CMS.popView()
editSubmodule: (event) ->
event.preventDefault()
CMS.pushView(new CMS.Views.ModuleEdit(model: new CMS.Models.Module(id: $(event.target).data('id'), type: $(event.target).data('type'))))
class CMS.Views.Week extends Backbone.View
events:
'click .week-edit': 'edit'
initialize: ->
CMS.on('content.show', @resetHeight)
CMS.on('content.hide', @setHeight)
render: ->
@setHeight()
@$('.editable').inlineEdit()
@$('.editable-textarea').inlineEdit(control: 'textarea')
@$('.modules .module').each ->
new CMS.Views.Module(el: this).render()
return @
edit: (event) ->
event.preventDefault()
CMS.replaceView(new CMS.Views.WeekEdit())
setHeight: =>
@$el.height(@options.height)
resetHeight: =>
@$el.height('')
class CMS.Views.WeekEdit extends Backbone.View
tagName: 'section'
className: 'edit-pane'
......@@ -4,7 +4,7 @@ class @Unit
$("##{@element_id} .save-update").click (event) =>
event.preventDefault()
$.post("save_item", {
$.post("/save_item", {
id: @module_id
data: JSON.stringify(@module.save())
})
......
......@@ -32,11 +32,11 @@ input {
button, input[type="submit"], .button {
background-color: $orange;
color: #fff;
-webkit-font-smoothing: antialiased;
padding: 8px 10px;
border: 0;
color: #fff;
font-weight: bold;
padding: 8px 10px;
-webkit-font-smoothing: antialiased;
&:hover {
background-color: shade($orange, 10%);
......@@ -45,9 +45,22 @@ button, input[type="submit"], .button {
#{$all-text-inputs}, textarea {
border: 1px solid $dark-blue;
@include box-shadow(inset 0 3px 6px $light-blue);
color: lighten($dark-blue, 30%);
font: $body-font-size $body-font-family;
outline: none;
padding: 4px 6px;
@include box-shadow(inset 0 3px 6px $light-blue);
&:hover {
background: lighten($yellow, 13%);
color: $dark-blue;
}
&:focus {
@include box-shadow(inset 0 3px 6px $light-blue, 0 0 3px lighten($bright-blue, 10%));
color: $dark-blue;
background: lighten($yellow, 13%);
}
}
textarea {
......@@ -56,7 +69,6 @@ textarea {
line-height: lh();
padding: 15px;
width: 100%;
}
// Extends
......@@ -87,18 +99,22 @@ textarea {
}
.draggable {
width: 7px;
min-height: 14px;
background: url('../img/drag-handle.png') no-repeat center;
text-indent: -9999px;
display: block;
float: right;
cursor: move;
height: 100%;
padding: 0;
@include position(absolute, 0px 0px 0 0);
width: 30px;
z-index: 99;
}
.editable {
&:hover {
background: lighten($yellow, 10%);
}
button {
padding: 4px 10px;
}
......@@ -107,13 +123,8 @@ textarea {
.wip {
outline: 1px solid #f00 !important;
position: relative;
}
&:after {
content: "WIP";
font-size: 8px;
padding: 2px;
background: #f00;
color: #fff;
@include position(absolute, 0px 0px 0 0);
}
.hidden {
display: none;
}
section.cal {
@include box-sizing(border-box);
padding: 25px;
@include clearfix;
padding: 25px;
> header {
@include clearfix;
......@@ -18,25 +18,44 @@ section.cal {
text-transform: uppercase;
letter-spacing: 1px;
font-size: 14px;
padding: 6px;
padding: 6px 6px 6px 0;
font-size: 12px;
margin: 0;
}
ul {
@include inline-block;
float: right;
margin: 0;
padding: 0;
&.actions {
float: left;
}
li {
@include inline-block;
margin-left: 6px;
border-left: 1px solid #ddd;
padding: 0 6px;
margin-right: 6px;
border-right: 1px solid #ddd;
padding: 0 6px 0 0;
&:last-child {
border-right: 0;
margin-right: 0;
padding-right: 0;
}
a {
@include inline-block();
font-size: 12px;
@include inline-block;
margin: 0 6px;
font-style: italic;
}
ul {
@include inline-block();
margin: 0;
li {
@include inline-block();
......@@ -55,6 +74,8 @@ section.cal {
border-top: 1px solid lighten($dark-blue, 40%);
width: 100%;
@include box-sizing(border-box);
margin: 0;
padding: 0;
> li {
border-right: 1px solid lighten($dark-blue, 40%);
......@@ -62,8 +83,13 @@ section.cal {
@include box-sizing(border-box);
float: left;
width: flex-grid(3) + ((flex-gutter() * 3) / 4);
background-color: lighten($light-blue, 2%);
background-color: $light-blue;
&:hover {
li.create-module {
opacity: 1;
}
}
header {
border-bottom: 1px solid lighten($dark-blue, 40%);
......@@ -77,103 +103,133 @@ section.cal {
text-transform: uppercase;
border-bottom: 1px solid lighten($dark-blue, 60%);
padding: 6px;
color: $bright-blue;
margin: 0;
a {
color: $bright-blue;
display: block;
padding: 6px;
margin: -6px;
&:hover {
color: darken($bright-blue, 10%);
background: lighten($yellow, 10%);
}
}
}
ul {
margin: 0;
padding: 0;
li {
background: #fff;
color: #888;
border-bottom: 0;
font-size: 12px;
&:hover {
background: #fff;
}
}
}
}
ul {
list-style: none;
margin-bottom: 1px;
margin: 0 0 1px 0;
padding: 0;
li {
border-bottom: 1px solid darken($light-blue, 8%);
padding: 6px;
position: relative;
overflow: hidden;
&:hover {
background: lighten($yellow, 10%);
background-color: lighten($yellow, 14%);
a.draggable {
background-color: lighten($yellow, 14%);
opacity: 1;
}
}
&.editable {
padding: 3px 6px;
}
a {
color: lighten($dark-blue, 10%);
display: block;
padding: 6px 35px 6px 6px;
&:hover {
background-color: lighten($yellow, 10%);
}
&.draggable {
background-color: $light-blue;
opacity: .3;
padding: 0;
&:hover {
background-color: lighten($yellow, 10%);
}
}
}
&.create-module {
position: relative;
opacity: 0;
@include transition(all 3s ease-in-out);
background: darken($light-blue, 2%);
> div {
background: $dark-blue;
@include box-shadow(0 0 5px darken($light-blue, 60%));
@include box-sizing(border-box);
display: none;
margin-left: 3%;
padding: 10px;
@include position(absolute, 30px 0 0 0);
width: 90%;
background: rgba(#000, .9);
padding: 10px;
@include box-sizing(border-box);
@include border-radius(3px);
z-index: 99;
&:before {
content: " ";
display: block;
background: rgba(#000, .8);
width: 10px;
height: 10px;
@include position(absolute, -5px 0 0 50%);
@include transform(rotate(45deg));
}
ul {
li {
border-bottom: 0;
background: none;
input {
width: 100%;
@include box-sizing(border-box);
border-color: #000;
padding: 6px;
width: 100%;
}
select {
width: 100%;
@include box-sizing(border-box);
width: 100%;
option {
font-size: 14px;
}
}
div {
margin-top: 10px;
}
a {
color: $light-blue;
float: right;
&:first-child {
float: left;
}
&:hover {
color: #fff;
}
}
}
}
}
&:hover {
div {
display: block;
}
}
}
}
}
......@@ -181,7 +237,7 @@ section.cal {
}
section.new-section {
margin-top: 10px;
margin: 10px 0 40px;
@include inline-block();
position: relative;
......@@ -247,30 +303,48 @@ section.cal {
}
}
}
&:hover {
section {
display: block;
}
}
}
}
body.content
section.cal {
width: flex-grid(3) + flex-gutter();
width: flex-grid(3);
float: left;
overflow: scroll;
@include box-sizing(border-box);
opacity: .4;
@include transition();
> header ul {
display: none;
}
background: darken($light-blue, 2%);
&:hover {
opacity: 1;
width: flex-grid(5) + flex-gutter();
background-color: transparent;
+ section.main-content {
width: flex-grid(7);
opacity: .6;
}
}
> header {
@include transition;
overflow: hidden;
> a {
display: none;
}
ul {
float: none;
display: block;
li {
ul {
display: inline;
}
}
}
}
ol {
......
.content-type {
padding-left: 34px;
background-position: 8px center;
background-repeat: no-repeat;
}
.videosequence a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/videosequence.png');
}
.video a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/video.png');
}
.problemset a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/problemset.png');
}
.problem a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/problem.png');
}
.lab a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/lab.png');
}
.tab a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/lab.png');
}
.html a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/html.png');
}
.vertical a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/vertical.png');
}
.sequential a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/sequential.png');
}
.chapter a:first-child {
@extend .content-type;
background-image: url('/static/img/content-types/chapter.png');
}
......@@ -5,6 +5,7 @@ body {
> section {
display: table;
table-layout: fixed;
width: 100%;
}
......@@ -25,7 +26,7 @@ body {
font-size: 14px;
text-transform: uppercase;
float: left;
margin-right: 15px;
margin: 0 15px 0 0;
a {
color: #fff;
......@@ -46,6 +47,7 @@ body {
ul {
float: left;
margin: 0;
&.user-nav {
float: right;
......@@ -72,9 +74,10 @@ body {
section.main-content {
border-left: 2px solid $dark-blue;
@include box-sizing(border-box);
width: flex-grid(9);
width: flex-grid(9) + flex-gutter();
float: left;
@include box-shadow( -2px 0 0 darken($light-blue, 3%));
@include transition();
}
}
}
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin:0;
padding:0;
border:0;
outline:0;
vertical-align:baseline;
background:transparent;
}
html,body {
font-size: 100%;
}
// Corrects block display not defined in IE8/9 & FF3
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section {
display: block;
}
// Corrects inline-block display not defined in IE8/9 & FF3
audio, canvas, video {
display: inline-block;
}
// Prevents modern browsers from displaying 'audio' without controls
audio:not([controls]) {
display: none;
}
// Addresses styling for 'hidden' attribute not present in IE8/9, FF3, S4
[hidden] {
display: none;
}
// Prevents iOS text size adjust after orientation change, without disabling user zoom
// www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/
html {
font-size: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
// Addresses font-family inconsistency between 'textarea' and other form elements.
html, button, input, select, textarea {
font-family: sans-serif;
}
a {
// Addresses outline displayed oddly in Chrome
&:focus {
outline: thin dotted;
// Webkit
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
// Improves readability when focused and also mouse hovered in all browsers
// people.opera.com/patrickl/experiments/keyboard/test
&:hover, &:active {
outline: 0;
}
}
// Addresses styling not present in IE8/9, S5, Chrome
abbr[title] {
border-bottom: 1px dotted;
}
// Addresses style set to 'bolder' in FF3+, S4/5, Chrome
b, strong {
font-weight: bold;
}
blockquote {
margin: 1em 40px;
}
// Addresses styling not present in S5, Chrome
dfn {
font-style: italic;
}
// Addresses styling not present in IE8/9
mark {
background: #ff0;
color: #000;
}
// Corrects font family set oddly in S4/5, Chrome
// en.wikipedia.org/wiki/User:Davidgothberg/Test59
pre, code, kbd, samp {
font-family: monospace, serif;
_font-family: 'courier new', monospace;
font-size: 1em;
}
// Improves readability of pre-formatted text in all browsers
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
// Addresses quote property not supported in S4
blockquote, q {
quotes: none;
&:before, &:after {
content: '';
content: none;
}
}
small {
font-size: 75%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
nav {
ul, ol {
list-style: none;
list-style-image: none;
}
}
// Removes border when inside 'a' element in IE8/9, FF3
img {
border: 0;
height: auto;
max-width: 100%;
-ms-interpolation-mode: bicubic;
}
// Corrects overflow displayed oddly in IE9
svg:not(:root) {
overflow: hidden;
}
// Define consistent border, margin, and padding
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
border: 0; // Corrects color not being inherited in IE8/9
padding: 0;
white-space: normal; // Corrects text not wrapping in FF3
}
button, input, select, textarea {
font-size: 100%; // Corrects font size not being inherited in all browsers
margin: 0; // Addresses margins set differently in FF3+, S5, Chrome
vertical-align: baseline; // Improves appearance and consistency in all browsers
}
// Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet
button, input {
line-height: normal;
}
button, input[type="button"], input[type="reset"], input[type="submit"] {
cursor: pointer; // Improves usability and consistency of cursor style between image-type 'input' and others
-webkit-appearance: button; // Corrects inability to style clickable 'input' types in iOS
}
// Re-set default cursor for disabled elements
button[disabled], input[disabled] {
cursor: default;
}
input[type="checkbox"], input[type="radio"] {
box-sizing: border-box; // Addresses box sizing set to content-box in IE8/9
padding: 0; //Removes excess padding in IE8/9
}
input[type="search"] {
-webkit-appearance: textfield; // Addresses appearance set to searchfield in S5, Chrome
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; // Addresses box-sizing set to border-box in S5, Chrome (-moz to future-proof)
box-sizing: content-box;
}
// Removes inner padding and search cancel button in S5, Chrome on OS X
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
// Removes inner padding and border in FF3+
// www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/
button::-moz-focus-inner, input::-moz-focus-inner {
border: 0;
padding: 0;
}
textarea {
overflow: auto; // Removes default vertical scrollbar in IE8/9
vertical-align: top; // Improves readability and alignment in all browsers
}
// Remove most spacing between table cells
table {
border-collapse: collapse;
border-spacing: 0;
}
section#unit-wrapper {
section.filters {
@include clearfix;
margin-bottom: 10px;
opacity: .4;
margin-bottom: 10px;
@include transition;
&:hover {
opacity: 1;
}
h2 {
@include inline-block();
text-transform: uppercase;
letter-spacing: 1px;
font-size: 14px;
padding: 6px 6px 6px 0;
font-size: 12px;
margin: 0;
}
ul {
@include clearfix();
list-style: none;
padding: 6px;
margin: 0;
padding: 0;
li {
@include inline-block();
@include inline-block;
margin-right: 6px;
border-right: 1px solid #ddd;
padding-right: 6px;
&.advanced {
&.search {
float: right;
border: 0;
}
a {
&.more {
font-size: 12px;
@include inline-block;
margin: 0 6px;
font-style: italic;
}
}
}
}
......@@ -39,11 +63,13 @@ section#unit-wrapper {
@include clearfix;
h2 {
text-transform: uppercase;
letter-spacing: 1px;
font-size: 12px;
float: left;
color: $bright-blue;
float: left;
font-size: 12px;
letter-spacing: 1px;
line-height: 19px;
text-transform: uppercase;
margin: 0;
}
}
......@@ -66,6 +92,8 @@ section#unit-wrapper {
ol {
list-style: none;
margin: 0;
padding: 0;
li {
border-bottom: 1px solid lighten($dark-blue, 60%);
......@@ -76,15 +104,20 @@ section#unit-wrapper {
ol {
list-style: none;
margin: 0;
padding: 0;
li {
padding: 6px;
position: relative;
&:last-child {
border-bottom: 0;
}
&:hover {
background-color: lighten($yellow, 10%);
a.draggable {
opacity: 1;
}
......@@ -92,7 +125,7 @@ section#unit-wrapper {
a.draggable {
float: right;
opacity: .5;
opacity: .4;
}
&.group {
......@@ -104,13 +137,15 @@ section#unit-wrapper {
h3 {
font-size: 14px;
margin: 0;
}
}
ol {
border-left: 4px solid #999;
border-bottom: 0;
margin: 0;
padding: 0;
li {
&:last-child {
......@@ -133,31 +168,52 @@ section#unit-wrapper {
ol {
list-style: none;
margin: 0;
padding: 0;
li {
border-bottom: 1px solid darken($light-blue, 8%);
background: lighten($light-blue, 2%);
background: $light-blue;
&:last-child {
border-bottom: 0;
}
&.new-module a {
background-color: darken($light-blue, 2%);
&:hover {
background-color: lighten($yellow, 10%);
}
}
a {
color: $dark-blue;
}
ul {
list-style: none;
margin: 0;
padding: 0;
li {
padding: 6px;
border-collapse: collapse;
position: relative;
&:last-child {
border-bottom: 0;
border-bottom: 1px solid darken($light-blue, 8%);
}
&:hover {
background-color: lighten($yellow, 10%);
a.draggable {
opacity: 1;
}
}
&.empty {
padding: 12px;
......@@ -169,16 +225,10 @@ section#unit-wrapper {
}
a.draggable {
float: right;
opacity: .3;
}
a {
color: #000;
}
}
}
}
}
}
......
......@@ -14,14 +14,19 @@ section#unit-wrapper {
letter-spacing: 1px;
@include inline-block();
color: $bright-blue;
margin: 0;
}
p {
@include inline-block();
margin-left: 10px;
color: #999;
font-size: 12px;
font-style: italic;
margin: 0;
a {
text-indent: -9999px;
@include inline-block();
width: 1px;
height: 100%;
}
}
}
......@@ -41,7 +46,7 @@ section#unit-wrapper {
&.save-update {
@extend .button;
margin: -6px -25px -6px 0;
margin: -6px -21px -6px 0;
}
}
}
......@@ -51,42 +56,68 @@ section#unit-wrapper {
padding: 20px;
section.meta {
background: $light-blue;
border-bottom: 1px solid lighten($dark-blue, 40%);
padding: 10px 20px;
margin: -20px -20px 10px;
opacity: .7;
@include transition;
&:hover {
opacity: 1;
padding: 20px;
margin: -20px -20px 10px;
}
section {
&.status-settings {
float: left;
margin-bottom: 10px;
color: $dark-blue;
@include clearfix;
ul {
border: 1px solid darken($light-blue, 15%);
@include clearfix();
float: left;
list-style: none;
border: 1px solid lighten($dark-blue, 40%);
@include inline-block();
margin: 0;
padding: 0;
li {
@include inline-block();
border-right: 1px solid lighten($dark-blue, 40%);
padding: 6px;
border-right: 1px solid darken($light-blue, 15%);
float: left;
&:last-child {
border-right: 0;
}
&.current {
background: #eee;
}
a {
color: $dark-blue;
padding: 6px;
display: block;
&.current {
background: darken($light-blue, 5%);
}
&:hover {
background-color: lighten($yellow, 13%);
}
}
}
}
a.settings {
@include inline-block();
float: left;
margin: 0 20px;
padding: 6px;
border: 1px solid lighten($dark-blue, 40%);
border: 1px solid darken($light-blue, 15%);
color: $dark-blue;
&:hover {
background-color: lighten($yellow, 13%);
}
}
select {
......@@ -110,10 +141,7 @@ section#unit-wrapper {
}
&.tags {
background: $light-blue;
color: lighten($dark-blue, 6%);
padding: 10px;
margin: 0 0 20px;
@include clearfix();
clear: both;
......@@ -124,10 +152,12 @@ section#unit-wrapper {
h2 {
font-size: 14px;
@include inline-block();
margin: 0;
}
p {
@include inline-block();
margin: 0;
}
}
}
......@@ -192,6 +222,8 @@ section#unit-wrapper {
ul {
list-style: none;
margin: 0;
padding: 0;
li {
margin-bottom: 20px;
......
@import 'bourbon/bourbon';
@import 'reset';
@import 'vendor/normalize';
@import 'base', 'layout';
@import 'base', 'layout', 'content-types';
@import 'calendar';
@import 'section', 'unit';
/*! normalize.css 2012-03-11T12:53 UTC - http://github.com/necolas/normalize.css */
/* =============================================================================
HTML5 display definitions
========================================================================== */
/*
* Corrects block display not defined in IE6/7/8/9 & FF3
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section,
summary {
display: block;
}
/*
* Corrects inline-block display not defined in IE6/7/8/9 & FF3
*/
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
}
/*
* Prevents modern browsers from displaying 'audio' without controls
* Remove excess height in iOS5 devices
*/
audio:not([controls]) {
display: none;
height: 0;
}
/*
* Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4
* Known issue: no IE6 support
*/
[hidden] {
display: none;
}
/* =============================================================================
Base
========================================================================== */
/*
* 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units
* http://clagnut.com/blog/348/#c790
* 2. Prevents iOS text size adjust after orientation change, without disabling user zoom
* www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/
*/
html {
font-size: 100%; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
}
/*
* Addresses font-family inconsistency between 'textarea' and other form elements.
*/
html,
button,
input,
select,
textarea {
font-family: sans-serif;
}
/*
* Addresses margins handled incorrectly in IE6/7
*/
body {
margin: 0;
}
/* =============================================================================
Links
========================================================================== */
/*
* Addresses outline displayed oddly in Chrome
*/
a:focus {
outline: thin dotted;
}
/*
* Improves readability when focused and also mouse hovered in all browsers
* people.opera.com/patrickl/experiments/keyboard/test
*/
a:hover,
a:active {
outline: 0;
}
/* =============================================================================
Typography
========================================================================== */
/*
* Addresses font sizes and margins set differently in IE6/7
* Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
h2 {
font-size: 1.5em;
margin: 0.83em 0;
}
h3 {
font-size: 1.17em;
margin: 1em 0;
}
h4 {
font-size: 1em;
margin: 1.33em 0;
}
h5 {
font-size: 0.83em;
margin: 1.67em 0;
}
h6 {
font-size: 0.75em;
margin: 2.33em 0;
}
/*
* Addresses styling not present in IE7/8/9, S5, Chrome
*/
abbr[title] {
border-bottom: 1px dotted;
}
/*
* Addresses style set to 'bolder' in FF3+, S4/5, Chrome
*/
b,
strong {
font-weight: bold;
}
blockquote {
margin: 1em 40px;
}
/*
* Addresses styling not present in S5, Chrome
*/
dfn {
font-style: italic;
}
/*
* Addresses styling not present in IE6/7/8/9
*/
mark {
background: #ff0;
color: #000;
}
/*
* Addresses margins set differently in IE6/7
*/
p,
pre {
margin: 1em 0;
}
/*
* Corrects font family set oddly in IE6, S4/5, Chrome
* en.wikipedia.org/wiki/User:Davidgothberg/Test59
*/
pre,
code,
kbd,
samp {
font-family: monospace, serif;
_font-family: 'courier new', monospace;
font-size: 1em;
}
/*
* Improves readability of pre-formatted text in all browsers
*/
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
/*
* 1. Addresses CSS quotes not supported in IE6/7
* 2. Addresses quote property not supported in S4
*/
/* 1 */
q {
quotes: none;
}
/* 2 */
q:before,
q:after {
content: '';
content: none;
}
small {
font-size: 75%;
}
/*
* Prevents sub and sup affecting line-height in all browsers
* gist.github.com/413930
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* =============================================================================
Lists
========================================================================== */
/*
* Addresses margins set differently in IE6/7
*/
dl,
menu,
ol,
ul {
margin: 1em 0;
}
dd {
margin: 0 0 0 40px;
}
/*
* Addresses paddings set differently in IE6/7
*/
menu,
ol,
ul {
padding: 0 0 0 40px;
}
/*
* Corrects list images handled incorrectly in IE7
*/
nav ul,
nav ol {
list-style: none;
list-style-image: none;
}
/* =============================================================================
Embedded content
========================================================================== */
/*
* 1. Removes border when inside 'a' element in IE6/7/8/9, FF3
* 2. Improves image quality when scaled in IE7
* code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/
*/
img {
border: 0; /* 1 */
-ms-interpolation-mode: bicubic; /* 2 */
}
/*
* Corrects overflow displayed oddly in IE9
*/
svg:not(:root) {
overflow: hidden;
}
/* =============================================================================
Figures
========================================================================== */
/*
* Addresses margin not present in IE6/7/8/9, S5, O11
*/
figure {
margin: 0;
}
/* =============================================================================
Forms
========================================================================== */
/*
* Corrects margin displayed oddly in IE6/7
*/
form {
margin: 0;
}
/*
* Define consistent border, margin, and padding
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/*
* 1. Corrects color not being inherited in IE6/7/8/9
* 2. Corrects text not wrapping in FF3
* 3. Corrects alignment displayed oddly in IE6/7
*/
legend {
border: 0; /* 1 */
padding: 0;
white-space: normal; /* 2 */
*margin-left: -7px; /* 3 */
}
/*
* 1. Corrects font size not being inherited in all browsers
* 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome
* 3. Improves appearance and consistency in all browsers
*/
button,
input,
select,
textarea {
font-size: 100%; /* 1 */
margin: 0; /* 2 */
vertical-align: baseline; /* 3 */
*vertical-align: middle; /* 3 */
}
/*
* Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet
*/
button,
input {
line-height: normal; /* 1 */
}
/*
* 1. Improves usability and consistency of cursor style between image-type 'input' and others
* 2. Corrects inability to style clickable 'input' types in iOS
* 3. Removes inner spacing in IE7 without affecting normal text inputs
* Known issue: inner spacing remains in IE6
*/
button,
input[type="button"],
input[type="reset"],
input[type="submit"] {
cursor: pointer; /* 1 */
-webkit-appearance: button; /* 2 */
*overflow: visible; /* 3 */
}
/*
* Re-set default cursor for disabled elements
*/
button[disabled],
input[disabled] {
cursor: default;
}
/*
* 1. Addresses box sizing set to content-box in IE8/9
* 2. Removes excess padding in IE8/9
* 3. Removes excess padding in IE7
Known issue: excess padding remains in IE6
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
*height: 13px; /* 3 */
*width: 13px; /* 3 */
}
/*
* 1. Addresses appearance set to searchfield in S5, Chrome
* 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof)
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/*
* Removes inner padding and search cancel button in S5, Chrome on OS X
*/
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
/*
* Removes inner padding and border in FF3+
* www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/*
* 1. Removes default vertical scrollbar in IE6/7/8/9
* 2. Improves readability and alignment in all browsers
*/
textarea {
overflow: auto; /* 1 */
vertical-align: top; /* 2 */
}
/* =============================================================================
Tables
========================================================================== */
/*
* Remove most spacing between table cells
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
......@@ -9,23 +9,26 @@
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
<%static:css group='base-style'/>
% else:
<link rel="stylesheet" href="${ STATIC_URL }/css/base-style.css">
<link rel="stylesheet" href="${static.url('css/base-style.css')}">
% endif
<link rel="stylesheet" type="text/css" href="${ STATIC_URL }/js/markitup/skins/simple/style.css" />
<link rel="stylesheet" type="text/css" href="${ STATIC_URL }/js/markitup/sets/wiki/style.css" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
<title><%block name="title"></%block></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
<%include file="widgets/header.html"/>
<%include file="widgets/header.html"/>
<%block name="content"></%block>
<%block name="content"></%block>
<script type="text/javascript" src="${ STATIC_URL}/js/jquery.min.js"></script>
<script type="text/javascript" src="${ STATIC_URL }/js/markitup/jquery.markitup.js"></script>
<script type="text/javascript" src="${ STATIC_URL }/js/markitup/sets/wiki/set.js"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js"')}></script>
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
<%static:js group='main'/>
% else:
......@@ -33,10 +36,10 @@
% endif
<%static:js group='module-js'/>
<script src="${ STATIC_URL }/js/jquery.inlineedit.js"></script>
<script src="${ STATIC_URL }/js/jquery.cookie.js"></script>
<script src="${ STATIC_URL }/js/jquery.leanModal.min.js"></script>
<script src="${ STATIC_URL }/js/jquery.tablednd.js"></script>
<script src="${static.url('js/vendor/jquery.inlineedit.js')}"></script>
<script src="${static.url('js/vendor/jquery.cookie.js')}"></script>
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
</body>
</html>
......
<%inherit file="base.html" />
<%block name="title">Course Manager</%block>
<%block name="content">
<section class="main-container">
<%include file="widgets/navigation.html"/>
<section class="main-content">
</section>
</section>
</%block>
<%inherit file="base.html" />
<%block name="title">Course Manager</%block>
<%block name="title">Courses</%block>
<%block name="content">
<section class="main-container">
<%include file="widgets/navigation.html"/>
<section class="main-content">
<section class="edit-pane">
<div id="module-html"/>
</section>
</section>
<ol>
%for course, url in courses:
<li><a href="${url}">${course}</a></li>
%endfor
</ol>
</section>
</%block>
<!doctype html>
<html>
<head>
<title>Jasmine Spec Runner</title>
{% load staticfiles %}
<link rel="stylesheet" href="{% static 'jasmine-latest/jasmine.css' %}" media="screen">
{# core files #}
<script src="{% static 'jasmine-latest/jasmine.js' %}"></script>
<script src="{% static 'jasmine-latest/jasmine-html.js' %}"></script>
<script src="{% static 'js/vendor/jasmine-jquery.js' %}"></script>
{# source files #}
{% for url in suite.js_files %}
<script src="{{ url }}"></script>
{% endfor %}
{% load compressed %}
{# static files #}
{% compressed_js 'main' %}
{# spec files #}
{% compressed_js 'spec' %}
</head>
<body>
<h1>Jasmine Spec Runner</h1>
<script>
{% block jasmine %}
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
var trivialReporter = new jasmine.TrivialReporter();
jasmineEnv.addReporter(trivialReporter);
jasmineEnv.specFilter = function(spec) {
return trivialReporter.specFilter(spec);
};
// Additional configuration can be done in this block
{% block jasmine_extra %}{% endblock %}
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
})();
{% endblock %}
</script>
</body>
</html>
......@@ -2,7 +2,7 @@
<header>
<section>
<h1 class="editable">${name}</h1>
<p>${category}</p>
<p class="${category}"><a href="#">${category}</a></p>
</section>
<div class="actions">
......
......@@ -3,16 +3,16 @@
<h2><a href="/">6.002x circuits and electronics</a></h2>
<ul>
<li>
<a href="#" class="new-module">New Section</a>
<a href="#" class="new-module wip">New Module</a>
</li>
<li>
<a href="#" class="new-module">New Unit</a>
<a href="#" class="new-module wip">New Unit</a>
</li>
</ul>
<ul class="user-nav">
<li><a href="#">Tasks</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#" class="wip">Tasks</a></li>
<li><a href="#" class="wip">Settings</a></li>
</ul>
</nav>
</header>
......@@ -36,7 +36,7 @@
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea>
<div class="preview">${data}</div>
<div class="actions wip">
<div class="actions">
<a href="" class="save-update">Save &amp; Update</a>
<a href="#" class="cancel">Cancel</a>
</div>
......
......@@ -2,7 +2,8 @@
<a href="#" class="new-module">
+ Add new module
</a>
<div>
<div class="hidden">
<form>
<ul>
<li>
......
<section class="cal">
<header class="wip">
<h2>Filter content:</h2>
<ul class="actions">
<li><a href="#">Timeline view</a></li>
<li><a href="#">Multi-Module edit</a></li>
</ul>
<ul>
<li>
<a href="#">Sequences</a>
<ul>
<li>Hide all</li>
<li>Lectures</li>
<li>Labs</li>
<li>Homeworks</li>
<li>Exams</li>
</ul>
<h2>Sort:</h2>
<select>
<option value="">Linear Order</option>
<option value="">Recently Modified</option>
<option value="">Type</option>
<option value="">Alphabetically</option>
</select>
</li>
<li>
<a href="#">Deadlines</a>
<ul>
<li>Today</li>
<li>Tomorrow</li>
<li>This week</li>
<li>In 2 weeks</li>
<li>This month</li>
</ul>
<h2>Filter:</h2>
<select>
<option value="">All content</option>
<option value="">Videos</option>
<option value="">Problems</option>
<option value="">Labs</option>
<option value="">Tutorials</option>
<option value="">HTML</option>
</select>
<a href="#" class="more">More</a>
</li>
<li>
<a href="#">Goals</a>
<ul>
<li>Hide</li>
</ul>
<a href="#">Hide goals</a>
</li>
<li class="search">
<input type="search" name="" id="" value="" placeholder="Search" />
</li>
</ul>
</header>
<ol>
<ol id="weeks">
% for week in weeks:
<li>
<li class="week" data-id="${week.location.url()}">
<header>
<h1><a href="#" class="module-edit" id="${week.location.url()}">${week.name}</a></h1>
<h1><a href="#" class="week-edit">${week.name}</a></h1>
<ul>
% if 'goals' in week.metadata:
% for goal in week.metadata['goals']:
......@@ -50,10 +53,10 @@
</ul>
</header>
<ul>
<ul class="modules">
% for module in week.get_children():
<li class="${module.category}">
<a href="#" class="module-edit" id="${module.location.url()}">${module.name}</a>
<li class="module" data-id="${module.location.url()}" data-type="${module.js_module_name()}">
<a href="#" class="module-edit">${module.name}</a>
<a href="#" class="draggable">handle</a>
</li>
% endfor
......@@ -61,7 +64,7 @@
</ul>
</li>
%endfor
<li>
<li class="wip">
<header>
<h1>Course Scratch Pad</h1>
</header>
......@@ -83,14 +86,15 @@
<a href="" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
<%include file="module-dropdown.html"/>
</ul>
</li>
</ol>
<section class="new-section">
<a href="#" >+ Add New Section</a>
<a href="#" class="wip" >+ Add New Section</a>
<section>
<section class="hidden">
<form>
<ul>
<li>
......
<section class="sequence-edit">
<section class="filters">
<section class="filters wip">
<ul>
<li>
<label for="">Sort by</label>
<h2>Sort:</h2>
<select>
<option value="">Linear Order</option>
<option value="">Recently Modified</option>
<option value="">Type</option>
<option value="">Alphabetically</option>
</select>
</li>
<li>
<label for="">Display</label>
<h2>Filter:</h2>
<select>
<option value="">All content</option>
<option value="">Videos</option>
<option value="">Problems</option>
<option value="">Labs</option>
<option value="">Tutorials</option>
<option value="">HTML</option>
</select>
<a href="#" class="more">More</a>
</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 class="search">
<input type="search" name="" id="" value="" placeholder="Search" />
</li>
</ul>
</section>
......@@ -36,8 +35,8 @@
<li>
<ol>
% for child in module.get_children():
<li>
<a href="#" class="module-edit" id="${child.location.url()}">${child.name}</a>
<li class="${module.category}">
<a href="#" class="module-edit" data-id="${child.location.url()}" data-type="${child.js_module_name()}">${child.name}</a>
<a href="#" class="draggable">handle</a>
</li>
%endfor
......@@ -47,7 +46,7 @@
</ol>
</section>
<section class="scratch-pad">
<section class="scratch-pad wip">
<ol>
<li class="new-module">
<%include file="new-module.html"/>
......
from django.conf.urls.defaults import patterns, url
from django.conf import settings
from django.conf.urls.defaults import patterns, include, url
# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()
urlpatterns = patterns('',
urlpatterns = ('',
url(r'^$', 'contentstore.views.index', name='index'),
url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'),
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^temp_force_export$', 'contentstore.views.temp_force_export')
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$', 'contentstore.views.course_index', name='course_index'),
url(r'^temp_force_export$', 'contentstore.views.temp_force_export'),
url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
)
if settings.DEBUG:
## Jasmine
urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
urlpatterns = patterns(*urlpatterns)
#! /bin/bash
cd $(dirname $0) && django-admin.py collectstatic --noinput --settings=envs.aws --pythonpath=.
from functools import wraps
import copy
import json
def expect_json(view_function):
@wraps(view_function)
def expect_json_with_cloned_request(request, *args, **kwargs):
if request.META['CONTENT_TYPE'] == "application/json":
cloned_request = copy.copy(request)
cloned_request.POST = cloned_request.POST.copy()
cloned_request.POST.update(json.loads(request.raw_post_data))
return view_function(cloned_request, *args, **kwargs)
else:
return view_function(request, *args, **kwargs)
return expect_json_with_cloned_request
......@@ -251,7 +251,7 @@ class LoncapaProblem(object):
if file is not None:
try:
ifp = self.system.filestore.open(file) # open using I4xSystem OSFS filestore
except Exception,err:
except Exception as err:
log.error('Error %s in problem xml include: %s' % (err,etree.tostring(inc,pretty_print=True)))
log.error('Cannot find file %s in %s' % (file,self.system.filestore))
if not self.system.get('DEBUG'): # if debugging, don't fail - just log error
......@@ -259,7 +259,7 @@ class LoncapaProblem(object):
else: continue
try:
incxml = etree.XML(ifp.read()) # read in and convert to XML
except Exception,err:
except Exception as err:
log.error('Error %s in problem xml include: %s' % (err,etree.tostring(inc,pretty_print=True)))
log.error('Cannot parse XML in %s' % (file))
if not self.system.get('DEBUG'): # if debugging, don't fail - just log error
......@@ -283,6 +283,7 @@ class LoncapaProblem(object):
context.update(global_context) # initialize context to have stuff in global_context
context['__builtins__'] = globals()['__builtins__'] # put globals there also
context['the_lcp'] = self # pass instance of LoncapaProblem in
context['script_code'] = ''
for script in tree.findall('.//script'):
stype = script.get('type')
......@@ -295,10 +296,12 @@ class LoncapaProblem(object):
code = script.text
XMLESC = {"&apos;": "'", "&quot;": '"'}
code = unescape(code, XMLESC)
context['script_code'] += code # store code source in context
try:
exec code in context, context # use "context" for global context; thus defs in code are global within code
exec code in context, context # use "context" for global context; thus defs in code are global within code
except Exception:
log.exception("Error while execing code: " + code)
log.exception("Error while execing script code: " + code)
raise responsetypes.LoncapaProblemError("Error while executing script code")
return context
def _extract_html(self, problemtree): # private
......@@ -311,6 +314,10 @@ class LoncapaProblem(object):
Used by get_html.
'''
if problemtree.tag=='script' and problemtree.get('type') and 'javascript' in problemtree.get('type'):
# leave javascript intact.
return problemtree
if problemtree.tag in html_problem_semantics:
return
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment