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())
})
......
// Backbone.js 0.9.2
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);
var readFixtures = function() {
return jasmine.getFixtures().proxyCallTo_('read', arguments)
}
var preloadFixtures = function() {
jasmine.getFixtures().proxyCallTo_('preload', arguments)
}
var loadFixtures = function() {
jasmine.getFixtures().proxyCallTo_('load', arguments)
}
var appendLoadFixtures = function() {
jasmine.getFixtures().proxyCallTo_('appendLoad', arguments)
}
var setFixtures = function(html) {
jasmine.getFixtures().proxyCallTo_('set', arguments)
}
var appendSetFixtures = function() {
jasmine.getFixtures().proxyCallTo_('appendSet', arguments)
}
var sandbox = function(attributes) {
return jasmine.getFixtures().sandbox(attributes)
}
var spyOnEvent = function(selector, eventName) {
jasmine.JQuery.events.spyOn(selector, eventName)
}
jasmine.getFixtures = function() {
return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures()
}
jasmine.Fixtures = function() {
this.containerId = 'jasmine-fixtures'
this.fixturesCache_ = {}
this.fixturesPath = 'spec/javascripts/fixtures'
}
jasmine.Fixtures.prototype.set = function(html) {
this.cleanUp()
this.createContainer_(html)
}
jasmine.Fixtures.prototype.appendSet= function(html) {
this.addToContainer_(html)
}
jasmine.Fixtures.prototype.preload = function() {
this.read.apply(this, arguments)
}
jasmine.Fixtures.prototype.load = function() {
this.cleanUp()
this.createContainer_(this.read.apply(this, arguments))
}
jasmine.Fixtures.prototype.appendLoad = function() {
this.addToContainer_(this.read.apply(this, arguments))
}
jasmine.Fixtures.prototype.read = function() {
var htmlChunks = []
var fixtureUrls = arguments
for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]))
}
return htmlChunks.join('')
}
jasmine.Fixtures.prototype.clearCache = function() {
this.fixturesCache_ = {}
}
jasmine.Fixtures.prototype.cleanUp = function() {
jQuery('#' + this.containerId).remove()
}
jasmine.Fixtures.prototype.sandbox = function(attributes) {
var attributesToSet = attributes || {}
return jQuery('<div id="sandbox" />').attr(attributesToSet)
}
jasmine.Fixtures.prototype.createContainer_ = function(html) {
var container
if(html instanceof jQuery) {
container = jQuery('<div id="' + this.containerId + '" />')
container.html(html)
} else {
container = '<div id="' + this.containerId + '">' + html + '</div>'
}
jQuery('body').append(container)
}
jasmine.Fixtures.prototype.addToContainer_ = function(html){
var container = jQuery('body').find('#'+this.containerId).append(html)
if(!container.length){
this.createContainer_(html)
}
}
jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) {
if (typeof this.fixturesCache_[url] === 'undefined') {
this.loadFixtureIntoCache_(url)
}
return this.fixturesCache_[url]
}
jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
var url = this.makeFixtureUrl_(relativeUrl)
var request = new XMLHttpRequest()
request.open("GET", url + "?" + new Date().getTime(), false)
request.send(null)
this.fixturesCache_[relativeUrl] = request.responseText
}
jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){
return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
}
jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
return this[methodName].apply(this, passedArguments)
}
jasmine.JQuery = function() {}
jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
return jQuery('<div/>').append(html).html()
}
jasmine.JQuery.elementToString = function(element) {
var domEl = $(element).get(0)
if (domEl == undefined || domEl.cloneNode)
return jQuery('<div />').append($(element).clone()).html()
else
return element.toString()
}
jasmine.JQuery.matchersClass = {};
!function(namespace) {
var data = {
spiedEvents: {},
handlers: []
}
namespace.events = {
spyOn: function(selector, eventName) {
var handler = function(e) {
data.spiedEvents[[selector, eventName]] = e
}
jQuery(selector).bind(eventName, handler)
data.handlers.push(handler)
},
wasTriggered: function(selector, eventName) {
return !!(data.spiedEvents[[selector, eventName]])
},
wasPrevented: function(selector, eventName) {
return data.spiedEvents[[selector, eventName]].isDefaultPrevented()
},
cleanUp: function() {
data.spiedEvents = {}
data.handlers = []
}
}
}(jasmine.JQuery)
!function(){
var jQueryMatchers = {
toHaveClass: function(className) {
return this.actual.hasClass(className)
},
toHaveCss: function(css){
for (var prop in css){
if (this.actual.css(prop) !== css[prop]) return false
}
return true
},
toBeVisible: function() {
return this.actual.is(':visible')
},
toBeHidden: function() {
return this.actual.is(':hidden')
},
toBeSelected: function() {
return this.actual.is(':selected')
},
toBeChecked: function() {
return this.actual.is(':checked')
},
toBeEmpty: function() {
return this.actual.is(':empty')
},
toExist: function() {
return $(document).find(this.actual).length
},
toHaveAttr: function(attributeName, expectedAttributeValue) {
return hasProperty(this.actual.attr(attributeName), expectedAttributeValue)
},
toHaveProp: function(propertyName, expectedPropertyValue) {
return hasProperty(this.actual.prop(propertyName), expectedPropertyValue)
},
toHaveId: function(id) {
return this.actual.attr('id') == id
},
toHaveHtml: function(html) {
return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html)
},
toContainHtml: function(html){
var actualHtml = this.actual.html()
var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html)
return (actualHtml.indexOf(expectedHtml) >= 0)
},
toHaveText: function(text) {
var trimmedText = $.trim(this.actual.text())
if (text && jQuery.isFunction(text.test)) {
return text.test(trimmedText)
} else {
return trimmedText == text
}
},
toHaveValue: function(value) {
return this.actual.val() == value
},
toHaveData: function(key, expectedValue) {
return hasProperty(this.actual.data(key), expectedValue)
},
toBe: function(selector) {
return this.actual.is(selector)
},
toContain: function(selector) {
return this.actual.find(selector).length
},
toBeDisabled: function(selector){
return this.actual.is(':disabled')
},
toBeFocused: function(selector) {
return this.actual.is(':focus')
},
toHandle: function(event) {
var events = this.actual.data('events')
if(!events || !event || typeof event !== "string") {
return false
}
var namespaces = event.split(".")
var eventType = namespaces.shift()
var sortedNamespaces = namespaces.slice(0).sort()
var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)")
if(events[eventType] && namespaces.length) {
for(var i = 0; i < events[eventType].length; i++) {
var namespace = events[eventType][i].namespace
if(namespaceRegExp.test(namespace)) {
return true
}
}
} else {
return events[eventType] && events[eventType].length > 0
}
},
// tests the existence of a specific event binding + handler
toHandleWith: function(eventName, eventHandler) {
var stack = this.actual.data("events")[eventName]
for (var i = 0; i < stack.length; i++) {
if (stack[i].handler == eventHandler) return true
}
return false
}
}
var hasProperty = function(actualValue, expectedValue) {
if (expectedValue === undefined) return actualValue !== undefined
return actualValue == expectedValue
}
var bindMatcher = function(methodName) {
var builtInMatcher = jasmine.Matchers.prototype[methodName]
jasmine.JQuery.matchersClass[methodName] = function() {
if (this.actual
&& (this.actual instanceof jQuery
|| jasmine.isDomNode(this.actual))) {
this.actual = $(this.actual)
var result = jQueryMatchers[methodName].apply(this, arguments)
var element;
if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML")
this.actual = jasmine.JQuery.elementToString(this.actual)
return result
}
if (builtInMatcher) {
return builtInMatcher.apply(this, arguments)
}
return false
}
}
for(var methodName in jQueryMatchers) {
bindMatcher(methodName)
}
}()
beforeEach(function() {
this.addMatchers(jasmine.JQuery.matchersClass)
this.addMatchers({
toHaveBeenTriggeredOn: function(selector) {
this.message = function() {
return [
"Expected event " + this.actual + " to have been triggered on " + selector,
"Expected event " + this.actual + " not to have been triggered on " + selector
]
}
return jasmine.JQuery.events.wasTriggered($(selector), this.actual)
}
})
this.addMatchers({
toHaveBeenPreventedOn: function(selector) {
this.message = function() {
return [
"Expected event " + this.actual + " to have been prevented on " + selector,
"Expected event " + this.actual + " not to have been prevented on " + selector
]
}
return jasmine.JQuery.events.wasPrevented(selector, this.actual)
}
})
})
afterEach(function() {
jasmine.getFixtures().cleanUp()
jasmine.JQuery.events.cleanUp()
})
/*
json2.js
2011-10-19
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, regexp: true */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
var JSON;
if (!JSON) {
JSON = {};
}
(function () {
'use strict';
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf())
? this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z'
: null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string'
? c
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0
? '[]'
: gap
? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
: '[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === 'string') {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0
? '{}'
: gap
? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
: '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function'
? walk({'': j}, '')
: j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());
// Underscore.js 1.3.3
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
......@@ -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
......
......@@ -22,10 +22,14 @@ Each input type takes the xml tree as 'element', the previous answer as 'value',
# status is currently the answer for the problem ID for the input element,
# but it will turn into a dict containing both the answer and any associated message for the problem ID for the input element.
import logging
import re
import shlex # for splitting quoted strings
from lxml import etree
import xml.sax.saxutils as saxutils
log = logging.getLogger('mitx.' + __name__)
def get_input_xml_tags():
''' Eventually, this will be for all registered input types '''
......@@ -121,7 +125,7 @@ def optioninput(element, value, status, render_template, msg=''):
eid=element.get('id')
options = element.get('options')
if not options:
raise Exception,"[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element)
raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element))
oset = shlex.shlex(options[1:-1])
oset.quotes = "'"
oset.whitespace = ","
......@@ -159,10 +163,11 @@ def choicegroup(element, value, status, render_template, msg=''):
choices={}
for choice in element:
if not choice.tag=='choice':
raise Exception,"[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag
raise Exception("[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag)
ctext = ""
ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
ctext += choice.text # TODO: fix order?
if choice.text is not None:
ctext += choice.text # TODO: fix order?
choices[choice.get("name")] = ctext
context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
html = render_template("choicegroup.html", context)
......@@ -182,9 +187,18 @@ def textline(element, value, status, render_template, msg=""):
raise Exception(msg)
count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size')
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg}
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
escapedict = {'"': '&quot;'}
value = saxutils.escape(value,escapedict) # otherwise, answers with quotes in them crashes the system!
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg, 'hidden': hidden}
html = render_template("textinput.html", context)
return etree.XML(html)
try:
xhtml = etree.XML(html)
except Exception as err:
if True: # TODO needs to be self.system.DEBUG - but can't access system
log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
raise
return xhtml
#-----------------------------------------------------------------------------
......@@ -195,7 +209,6 @@ def textline_dynamath(element, value, status, render_template, msg=''):
'''
# TODO: Make a wrapper for <formulainput>
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
## TODO: Code should follow PEP8 (4 spaces per indentation level)
'''
textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
uses a <span id=display_eid>`{::}`</span>
......@@ -204,8 +217,9 @@ def textline_dynamath(element, value, status, render_template, msg=''):
eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK
size = element.get('size')
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
'msg':msg,
'msg':msg, 'hidden' : hidden,
}
html = render_template("textinput_dynamath.html", context)
return etree.XML(html)
......@@ -225,14 +239,24 @@ def textbox(element, value, status, render_template, msg=''):
rows = element.get('rows') or '30'
cols = element.get('cols') or '80'
mode = element.get('mode') or 'python' # mode for CodeMirror, eg "python" or "xml"
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
linenumbers = element.get('linenumbers') # for CodeMirror
if not value: value = element.text # if no student input yet, then use the default input given by the problem
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg,
'mode':mode, 'linenumbers':linenumbers,
'rows':rows, 'cols':cols,
'hidden' : hidden,
}
html = render_template("textbox.html", context)
return etree.XML(html)
try:
xhtml = etree.XML(html)
except Exception as err:
newmsg = 'error %s in rendering message' % (str(err).replace('<','&lt;'))
newmsg += '<br/>Original message: %s' % msg.replace('<','&lt;')
context['msg'] = newmsg
html = render_template("textbox.html", context)
xhtml = etree.XML(html)
return xhtml
#-----------------------------------------------------------------------------
@register_render_function
......@@ -288,8 +312,18 @@ def math(element, value, status, render_template, msg=''):
# isinline = True
# html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,element.tail)
xhtml = etree.XML(html)
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,saxutils.escape(element.tail))
try:
xhtml = etree.XML(html)
except Exception as err:
if False: # TODO needs to be self.system.DEBUG - but can't access system
msg = "<html><font color='red'><p>Error %s</p>" % str(err).replace('<','&lt;')
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<','&lt;')
msg += "</font></html>"
log.error(msg)
return etree.XML(msg)
else:
raise
# xhtml.tail = element.tail # don't forget to include the tail!
return xhtml
......@@ -338,14 +372,14 @@ def imageinput(element, value, status, render_template, msg=''):
(gx,gy) = (0,0)
context = {
'id':eid,
'value':value,
'id': eid,
'value': value,
'height': height,
'width' : width,
'src':src,
'gx':gx,
'gy':gy,
'state' : status, # to change
'width': width,
'src': src,
'gx': gx,
'gy': gy,
'state': status, # to change
'msg': msg, # to change
}
html = render_template("imageinput.html", context)
......
......@@ -193,7 +193,7 @@ class LoncapaResponse(object):
try:
self.context[hintfn](self.answer_ids, student_answers, new_cmap, old_cmap)
except Exception, err:
except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err,hintfn)
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
raise ResponseError(msg)
......@@ -556,7 +556,7 @@ def sympy_check2():
idset = sorted(self.answer_ids) # ordered list of answer id's
try:
submission = [student_answers[k] for k in idset] # ordered list of answers
except Exception, err:
except Exception as err:
msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers
msg += '\n idset = %s, error = %s' % (idset,err)
log.error(msg)
......@@ -567,7 +567,7 @@ def sympy_check2():
# if there is only one box, and it's empty, then don't evaluate
if len(idset)==1 and not submission[0]:
return {idset[0]:'no_answer_entered'}
return CorrectMap(idset[0],'incorrect',msg='<font color="red">No answer entered!</font>')
correct = ['unknown'] * len(idset)
messages = [''] * len(idset)
......@@ -594,7 +594,7 @@ def sympy_check2():
if type(self.code)==str:
try:
exec self.code in self.context['global_context'], self.context
except Exception,err:
except Exception as err:
print "oops in customresponse (code) error %s" % err
print "context = ",self.context
print traceback.format_exc()
......@@ -619,7 +619,7 @@ def sympy_check2():
log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs))
ret = fn(*args[:nargs],**kwargs)
except Exception,err:
except Exception as err:
log.error("oops in customresponse (cfn) error %s" % err)
# print "context = ",self.context
log.error(traceback.format_exc())
......@@ -746,12 +746,20 @@ main()
xml = self.xml
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
answer_src = answer.get('src')
if answer_src is not None:
self.code = self.system.filesystem.open('src/'+answer_src).read()
else:
self.code = answer.text
# answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
answer = xml.find('answer')
if answer is not None:
answer_src = answer.get('src')
if answer_src is not None:
self.code = self.system.filesystem.open('src/'+answer_src).read()
else:
self.code = answer.text
else: # no <answer> stanza; get code from <script>
self.code = self.context['script_code']
if not self.code:
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
raise LoncapaProblemError(msg)
self.tests = xml.get('tests')
......@@ -774,7 +782,7 @@ main()
try:
r = requests.post(self.url,data=payload) # call external server
except Exception,err:
except Exception as err:
msg = 'Error %s - cannot connect to external server url=%s' % (err,self.url)
log.error(msg)
raise Exception(msg)
......@@ -786,7 +794,7 @@ main()
try:
rxml = etree.fromstring(r.text) # response is XML; prase it
except Exception,err:
except Exception as err:
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text)
log.error(msg)
raise Exception(msg)
......@@ -798,7 +806,7 @@ main()
cmap = CorrectMap()
try:
submission = [student_answers[k] for k in idset]
except Exception,err:
except Exception as err:
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers))
raise Exception(err)
......@@ -808,7 +816,7 @@ main()
try:
rxml = self.do_external_request('get_score',extra_payload)
except Exception, err:
except Exception as err:
log.error('Error %s' % err)
if self.system.DEBUG:
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset) )))
......@@ -838,7 +846,7 @@ main()
try:
rxml = self.do_external_request('get_answers',{})
exans = json.loads(rxml.find('expected').text)
except Exception,err:
except Exception as err:
log.error('Error %s' % err)
if self.system.DEBUG:
msg = '<font color=red size=+2>%s</font>' % str(err).replace('<','&lt;')
......@@ -935,7 +943,7 @@ class FormulaResponse(LoncapaResponse):
except UndefinedVariable as uv:
log.debug('formularesponse: undefined variable in given=%s' % given)
raise StudentInputError(uv.message+" not permitted in answer")
except Exception, err:
except Exception as err:
#traceback.print_exc()
log.debug('formularesponse: error %s in formula' % err)
raise StudentInputError("Error in formula")
......
<section id="textbox_${id}" class="textbox">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}">${value|h}</textarea>
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}"
% if hidden:
style="display:none;"
% endif
>${value|h}</textarea>
<span id="answer_${id}"></span>
......@@ -12,6 +16,9 @@
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<br/>
<span class="debug">(${state})</span>
<br/>
......
......@@ -8,11 +8,17 @@
% elif state == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
% if hidden:
style="display:none;"
% endif
/>
<p class="status">
......
......@@ -11,8 +11,15 @@
% elif state == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}" class="math" size="${size if size else ''}" />
<input type="text" name="input_${id}" id="input_${id}" value="${value}" class="math" size="${size if size else ''}"
% if hidden:
style="display:none;"
% endif
/>
<p class="status">
% if state == 'unsubmitted':
......
from setuptools import setup, find_packages
setup(
name="capa",
version="0.1",
packages=find_packages(exclude=["tests"]),
install_requires=['distribute'],
)
from setuptools import setup, find_packages
setup(
name="mitxmako",
version="0.1",
packages=find_packages(exclude=["tests"]),
install_requires=['distribute'],
)
......@@ -8,6 +8,10 @@ setup(
package_data={
'xmodule': ['js/module/*']
},
requires=[
'capa',
'mitxmako'
],
# See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points
......
......@@ -180,7 +180,12 @@ class CapaModule(XModule):
score = d['score']
total = d['total']
if total > 0:
return Progress(score, total)
try:
return Progress(score, total)
except Exception as err:
if self.system.DEBUG:
return None
raise
return None
def get_html(self):
......@@ -194,7 +199,18 @@ class CapaModule(XModule):
'''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.'''
html = self.lcp.get_html()
try:
html = self.lcp.get_html()
except Exception, err:
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.location.url())
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<','&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','&lt;')
html = msg
else:
raise
content = {'name': self.metadata['display_name'],
'html': html,
'weight': self.weight,
......@@ -395,14 +411,18 @@ class CapaModule(XModule):
correct_map = self.lcp.grade_answers(answers)
except StudentInputError as inst:
# TODO (vshnayder): why is this line here?
self.lcp = LoncapaProblem(self.definition['data'],
id=lcp_id, state=old_state, system=self.system)
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
traceback.print_exc()
return {'success': inst.message}
except:
except Exception, err:
# TODO: why is this line here?
self.lcp = LoncapaProblem(self.definition['data'],
id=lcp_id, state=old_state, system=self.system)
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
if self.system.DEBUG:
msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc()
return {'success':msg}
traceback.print_exc()
raise Exception("error in capa_module")
......
import logging
from lxml import etree
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
......@@ -26,3 +27,8 @@ class HtmlDescriptor(RawDescriptor):
js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
js_module = 'HTML'
@classmethod
def file_to_xml(cls, file_object):
parser = etree.HTMLParser()
return etree.parse(file_object, parser).getroot()
class @HTML
constructor: (@id) ->
@edit_box = $("##{@id} .edit-box")
@preview = $("##{@id} .preview")
constructor: (@element) ->
@edit_box = $(".edit-box", @element)
@preview = $(".preview", @element)
@edit_box.on('input', =>
@preview.empty().append(@edit_box.val())
)
save: -> {text: @edit_box.val()}
save: -> @edit_box.val()
class @Raw
constructor: (@id) ->
@edit_box = $("##{@id} .edit-box")
@preview = $("##{@id} .preview")
constructor: (@element) ->
@edit_box = $(".edit-box", @element)
@preview = $(".preview", @element)
@edit_box.on('input', =>
@preview.empty().text(@edit_box.val())
)
......
......@@ -6,6 +6,9 @@ that are stored in a database an accessible using their Location as an identifie
import re
from collections import namedtuple
from .exceptions import InvalidLocationError
import logging
log = logging.getLogger('mitx.' + 'modulestore')
URL_RE = re.compile("""
(?P<tag>[^:]+)://
......@@ -74,11 +77,13 @@ class Location(_LocationBase):
def check_list(list_):
for val in list_:
if val is not None and INVALID_CHARS.search(val) is not None:
log.debug('invalid characters val="%s", list_="%s"' % (val,list_))
raise InvalidLocationError(location)
if isinstance(location, basestring):
match = URL_RE.match(location)
if match is None:
log.debug('location is instance of %s but no URL match' % basestring)
raise InvalidLocationError(location)
else:
groups = match.groupdict()
......@@ -86,6 +91,7 @@ class Location(_LocationBase):
return _LocationBase.__new__(_cls, **groups)
elif isinstance(location, (list, tuple)):
if len(location) not in (5, 6):
log.debug('location has wrong length')
raise InvalidLocationError(location)
if len(location) == 5:
......@@ -153,6 +159,18 @@ class ModuleStore(object):
location is found
"""
raise NotImplementedError
def get_items(self, location, default_class=None):
"""
Returns a list of XModuleDescriptor instances for the items
that match location. Any element of location that is None is treated
as a wildcard that matches any value
location: Something that can be passed to Location
default_class: An XModuleDescriptor subclass to use if no plugin matching the
location is found
"""
raise NotImplementedError
# TODO (cpennington): Replace with clone_item
def create_item(self, location, editor):
......
import pymongo
from bson.objectid import ObjectId
from importlib import import_module
from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem
......@@ -8,6 +9,19 @@ 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 = {}
for key, val in Location(loc).dict().iteritems():
if val is not None:
query['_id.{key}'.format(key=key)] = val
return query
class MongoModuleStore(ModuleStore):
"""
A Mongodb backed ModuleStore
......@@ -17,7 +31,6 @@ class MongoModuleStore(ModuleStore):
host=host,
port=port
)[db][collection]
self.collection.ensure_index('location')
# Force mongo to report errors, at the expense of performance
self.collection.safe = True
......@@ -26,6 +39,18 @@ class MongoModuleStore(ModuleStore):
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
# TODO (cpennington): Pass a proper resources_fs to the system
self.system = MakoDescriptorSystem(
load_item=self.get_item,
resources_fs=None,
render_template=render_to_string
)
def _load_item(self, item):
item['location'] = item['_id']
del item['_id']
return XModuleDescriptor.load_from_json(item, self.system, self.default_class)
def get_item(self, location):
"""
Returns an XModuleDescriptor instance for the item at location.
......@@ -39,24 +64,26 @@ class MongoModuleStore(ModuleStore):
location: Something that can be passed to Location
"""
query = {}
for key, val in Location(location).dict().iteritems():
if key != 'revision' and val is None:
raise InsufficientSpecificationError(location)
if val is not None:
query['location.{key}'.format(key=key)] = val
item = self.collection.find_one(
query,
location_to_query(location),
sort=[('revision', pymongo.ASCENDING)],
)
if item is None:
raise ItemNotFoundError(location)
return self._load_item(item)
# TODO (cpennington): Pass a proper resources_fs to the system
return XModuleDescriptor.load_from_json(
item, MakoDescriptorSystem(load_item=self.get_item, resources_fs=None, render_template=render_to_string), self.default_class)
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)],
)
return [self._load_item(item) for item in items]
def create_item(self, location):
"""
......@@ -65,7 +92,7 @@ class MongoModuleStore(ModuleStore):
location: Something that can be passed to Location
"""
self.collection.insert({
'location': Location(location).dict(),
'_id': Location(location).dict(),
})
def update_item(self, location, data):
......@@ -80,8 +107,9 @@ class MongoModuleStore(ModuleStore):
# See http://www.mongodb.org/display/DOCS/Updating for
# atomic update syntax
self.collection.update(
{'location': Location(location).dict()},
{'$set': {'definition.data': data}}
{'_id': Location(location).dict()},
{'$set': {'definition.data': data}},
)
def update_children(self, location, children):
......@@ -96,7 +124,7 @@ class MongoModuleStore(ModuleStore):
# See http://www.mongodb.org/display/DOCS/Updating for
# atomic update syntax
self.collection.update(
{'location': Location(location).dict()},
{'_id': Location(location).dict()},
{'$set': {'definition.children': children}}
)
......@@ -112,6 +140,6 @@ class MongoModuleStore(ModuleStore):
# See http://www.mongodb.org/display/DOCS/Updating for
# atomic update syntax
self.collection.update(
{'location': Location(location).dict()},
{'_id': Location(location).dict()},
{'$set': {'metadata': metadata}}
)
......@@ -13,7 +13,7 @@ from .exceptions import ItemNotFoundError
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True))
log = logging.getLogger(__name__)
log = logging.getLogger('mitx.' + __name__)
class XMLModuleStore(ModuleStore):
......@@ -38,9 +38,13 @@ class XMLModuleStore(ModuleStore):
self.default_class = None
else:
module_path, _, class_name = default_class.rpartition('.')
log.debug('module_path = %s' % module_path)
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager,self.data_dir))
log.debug('default_class = %s' % self.default_class)
for course_dir in os.listdir(self.data_dir):
if not os.path.exists(self.data_dir + "/" + course_dir + "/course.xml"):
continue
......@@ -97,9 +101,11 @@ class XMLModuleStore(ModuleStore):
slug = '{slug}_{count}'.format(slug=slug, count=self.unnamed_modules)
self.used_slugs.add(slug)
# log.debug('-> slug=%s' % slug)
xml_data.set('slug', slug)
module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class)
log.debug('==> importing module location %s' % repr(module.location))
modulestore.modules[module.location] = module
if modulestore.eager:
......@@ -117,6 +123,7 @@ class XMLModuleStore(ModuleStore):
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):
......
......@@ -350,6 +350,8 @@ class XModuleDescriptor(Plugin):
etree.fromstring(xml_data).tag,
default_class
)
# leave next line in code, commented out - useful for low-level debugging
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (etree.fromstring(xml_data).tag,class_))
return class_.from_xml(xml_data, system, org, course)
@classmethod
......
......@@ -91,6 +91,16 @@ class XmlDescriptor(XModuleDescriptor):
del xml_object.attrib[attr]
@classmethod
def file_to_xml(cls, file_object):
"""
Used when this module wants to parse a file object to xml
that will be converted to the definition.
Returns an lxml Element
"""
return etree.parse(file_object).getroot()
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
......@@ -125,9 +135,10 @@ class XmlDescriptor(XModuleDescriptor):
definition_xml = copy.deepcopy(xml_object)
else:
filepath = cls._format_filepath(xml_object.tag, filename)
log.debug('filepath=%s, resources_fs=%s' % (filepath,system.resources_fs))
with system.resources_fs.open(filepath) as file:
try:
definition_xml = etree.parse(file).getroot()
definition_xml = cls.file_to_xml(file)
except:
log.exception("Failed to parse xml in file %s" % filepath)
raise
......@@ -147,8 +158,8 @@ class XmlDescriptor(XModuleDescriptor):
)
@classmethod
def _format_filepath(cls, type, name):
return '{type}/{name}.{ext}'.format(type=type, name=name, ext=cls.filename_extension)
def _format_filepath(cls, category, name):
return u'{category}/{name}.{ext}'.format(category=category, name=name, ext=cls.filename_extension)
def export_to_xml(self, resource_fs):
"""
......
......@@ -8,5 +8,25 @@ Testing is good. Here is some useful info about how we set up tests--
### Frontend code:
- TODO
We're using Jasmine to unit-testing the JavaScript files. All the specs are
written in CoffeeScript for the consistency. To access the test cases, start the
server in debug mode, navigate to `http://127.0.0.1:[port number]/_jasmine` to
see the test result.
All the JavaScript codes must have test coverage. Both CMS and LMS
has its own test directory in `{cms,lms}/static/coffee/spec` If you haven't
written a JavaScript test before, you can look at those example files as a
starting point. Also, these materials might be helpful for you:
CMS Note: For consistency, you're advised to use the same directory structure
for implementation and test. For example, test for `src/views/module.coffee`
should be written in `spec/views/module_spec.coffee`.
* http://pivotal.github.com/jasmine
* http://railscasts.com/episodes/261-testing-javascript-with-jasmine?view=asciicast
* http://a-developer-life.blogspot.com/2011/05/jasmine-part-1-unit-testing-javascript.html
If you're finishing a feature that contains JavaScript code snippets and do not
sure how to test, please feel free to open up a pull request and asking people
for help. (However, the best way to do it would be writing your test first, then
implement your feature - Test Driven Development.)
......@@ -71,6 +71,7 @@ class Settings(object):
# Load the course settings as a dictionary
course_settings = {}
try:
# TODO: this doesn't work with multicourse
with open( settings.DATA_DIR + "/course_settings.json") as course_settings_file:
course_settings_string = course_settings_file.read()
course_settings = json.loads(course_settings_string)
......
......@@ -288,11 +288,24 @@ def add_histogram(module):
histogram = grade_histogram(module_id)
render_histogram = len(histogram) > 0
# TODO: fixme - no filename in module.xml in general (this code block for edx4edx)
# the following if block is for summer 2012 edX course development; it will change when the CMS comes online
if settings.MITX_FEATURES.get('DISPLAY_EDIT_LINK') and settings.DEBUG and module_xml.get('filename') is not None:
coursename = multicourse_settings.get_coursename_from_request(request)
github_url = multicourse_settings.get_course_github_url(coursename)
fn = module_xml.get('filename')
if module_xml.tag=='problem': fn = 'problems/' + fn # grrr
edit_link = (github_url + '/tree/master/' + fn) if github_url is not None else None
if module_xml.tag=='problem': edit_link += '.xml' # grrr
else:
edit_link = False
# Cast module.definition and module.metadata to dicts so that json can dump them
# even though they are lazily loaded
staff_context = {'definition': json.dumps(dict(module.definition), indent=4),
'metadata': json.dumps(dict(module.metadata), indent=4),
'element_id': module.location.html_id(),
'edit_link': edit_link,
'histogram': json.dumps(histogram),
'render_histogram': render_histogram,
'module_content': original_get_html()}
......@@ -301,7 +314,6 @@ def add_histogram(module):
module.get_html = get_html
return module
def modx_dispatch(request, dispatch=None, id=None):
''' Generic view for extensions. This is where AJAX calls go.
......
......@@ -172,12 +172,12 @@ def index(request, course_id=None, chapter=None, section=None,
course_location = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_location)
if not settings.COURSEWARE_ENABLED:
return redirect('/')
chapter = clean(chapter)
section = clean(section)
if settings.ENABLE_MULTICOURSE:
settings.MODULESTORE['default']['OPTIONS']['data_dir'] = settings.DATA_DIR + multicourse_settings.get_course_xmlpath(course)
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section),
......
......@@ -94,6 +94,14 @@ def get_course_title(coursename):
def get_course_number(coursename):
return get_course_property(coursename, 'number')
def get_course_github_url(coursename):
return get_course_property(coursename,'github_url')
def get_course_default_chapter(coursename):
return get_course_property(coursename,'default_chapter')
def get_course_default_section(coursename):
return get_course_property(coursename,'default_section')
def get_course_location(coursename):
return get_course_property(coursename, 'location')
'''
django admin pages for courseware model
'''
from student.models import *
from django.contrib import admin
from django.contrib.auth.models import User
admin.site.register(UserProfile)
admin.site.register(Registration)
admin.site.register(PendingNameChange)
......@@ -214,7 +214,9 @@ def create_account(request, post_override=None):
up.save()
d={'name':post_vars['name'],
'key':r.activation_key}
'key':r.activation_key,
'course_title' : settings.COURSE_TITLE,
}
subject = render_to_string('emails/activation_email_subject.txt',d)
# Email subject *must not* contain newlines
......@@ -222,7 +224,11 @@ def create_account(request, post_override=None):
message = render_to_string('emails/activation_email.txt',d)
try:
if not settings.GENERATE_RANDOM_USER_CREDENTIALS:
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
dest_addr = settings.MITX_FEATURES['REROUTE_ACTIVATION_EMAIL']
message = "Activation for %s (%s): %s\n" % (u,u.email,up.name) + '-'*80 + '\n\n' + message
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False)
elif not settings.GENERATE_RANDOM_USER_CREDENTIALS:
res=u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except:
log.exception(sys.exc_info())
......
......@@ -39,6 +39,8 @@ MITX_FEATURES = {
'SAMPLE' : False,
'USE_DJANGO_PIPELINE' : True,
'DISPLAY_HISTOGRAMS_TO_STAFF' : True,
'REROUTE_ACTIVATION_EMAIL' : False, # nonempty string = address for all activation emails
'DEBUG_LEVEL' : 0, # 0 = lowest level, least verbose, 255 = max level, most verbose
}
# Used for A/B testing
......@@ -72,7 +74,7 @@ MAKO_MODULE_DIR = tempfile.mkdtemp('mako')
MAKO_TEMPLATES = {}
MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
COMMON_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates']
# This is where Django Template lookup is defined. There are a few of these
......@@ -123,11 +125,11 @@ QUICKEDIT = False
###
COURSE_DEFAULT = '6.002_Spring_2012'
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
COURSE_DEFAULT = '6.002x_Fall_2012'
COURSE_SETTINGS = {'6.002x_Fall_2012': {'number' : '6.002x',
'title' : 'Circuits and Electronics',
'xmlpath': '6002x/',
'location': 'i4x://edx/6002xs12/course/6.002_Spring_2012',
'location': 'i4x://edx/6002xs12/course/6.002x_Fall_2012',
}
}
......
......@@ -52,8 +52,8 @@ CACHES = {
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar',)
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
#INSTALLED_APPS += ('debug_toolbar',)
#MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',)
DEBUG_TOOLBAR_PANELS = (
......
"""
This config file runs the simplest dev environment using sqlite, and db-based
sessions. Assumes structure:
/envroot/
/db # This is where it'll write the database file
/mitx # The location of this repo
/log # Where we're going to write log files
"""
import socket
if 'eecs1' in socket.gethostname():
MITX_ROOT_URL = '/mitx2'
from .common import *
from .logsettings import get_logger_config
from .dev import *
if 'eecs1' in socket.gethostname():
MITX_ROOT_URL = '/mitx2'
#-----------------------------------------------------------------------------
# edx4edx content server
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
MITX_FEATURES['REROUTE_ACTIVATION_EMAIL'] = 'ichuang@mitx.mit.edu'
EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
#EMAIL_BACKEND = 'django_ses.SESBackend'
#-----------------------------------------------------------------------------
# ichuang
DEBUG = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = True
MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT ]
#MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
MITX_FEATURES['DISPLAY_HISTOGRAMS_TO_STAFF'] = False
MITX_FEATURES['DISPLAY_EDIT_LINK'] = True
COURSE_DEFAULT = "edx4edx"
COURSE_NAME = "edx4edx"
COURSE_NUMBER = "edX.01"
COURSE_TITLE = "edx4edx: edX Author Course"
SITE_NAME = "ichuang.mitx.mit.edu"
COURSE_SETTINGS = {'edx4edx': {'number' : 'edX.01',
'title' : 'edx4edx: edX Author Course',
'xmlpath': '/edx4edx/',
'github_url': 'https://github.com/MITx/edx4edx',
'active' : True,
'default_chapter' : 'Introduction',
'default_section' : 'edx4edx_Course',
},
}
#-----------------------------------------------------------------------------
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
'ssl_auth.ssl_auth.NginxProxyHeaderMiddleware', # ssl authentication behind nginx proxy
)
AUTHENTICATION_BACKENDS = (
'ssl_auth.ssl_auth.SSLLoginBackend',
'django.contrib.auth.backends.ModelBackend',
)
INSTALLED_APPS = INSTALLED_APPS + (
'ssl_auth',
)
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/'
LOGIN_URL = MITX_ROOT_URL + '/'
......@@ -18,47 +18,114 @@ from .logsettings import get_logger_config
from .dev import *
if 'eecs1' in socket.gethostname():
MITX_ROOT_URL = '/mitx2'
# MITX_ROOT_URL = '/mitx2'
MITX_ROOT_URL = 'https://eecs1.mit.edu/mitx2'
#-----------------------------------------------------------------------------
# edx4edx content server
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
MITX_FEATURES['REROUTE_ACTIVATION_EMAIL'] = 'ichuang@mit.edu'
EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
#EMAIL_BACKEND = 'django_ses.SESBackend'
#-----------------------------------------------------------------------------
# ichuang
DEBUG = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = True
QUICKEDIT = False
# MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT ]
#MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
MITX_FEATURES['DISPLAY_HISTOGRAMS_TO_STAFF'] = False
MITX_FEATURES['DISPLAY_EDIT_LINK'] = True
MITX_FEATURES['DEBUG_LEVEL'] = 10 # 0 = lowest level, least verbose, 255 = max level, most verbose
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
COURSE_SETTINGS = {'6.002x_Fall_2012': {'number' : '6.002x',
'title' : 'Circuits and Electronics',
'xmlpath': '/6002x/',
'xmlpath': '/6002x-fall-2012/',
'active' : True,
'default_chapter' : 'Week_1',
'default_section' : 'Administrivia_and_Circuit_Elements',
'location': 'i4x://edx/6002xs12/course/6.002x_Fall_2012',
},
'8.02_Spring_2013': {'number' : '8.02x',
'title' : 'Electricity &amp; Magnetism',
'xmlpath': '/802x/',
'active' : True,
},
'8.01_Spring_2013': {'number' : '8.01x',
'title' : 'Mechanics',
'xmlpath': '/801x/',
'active' : False,
'github_url': 'https://github.com/MITx/8.02x',
'active' : True,
'default_chapter' : 'Introduction',
'default_section' : 'Introduction_%28Lewin_2002%29',
},
'6.189_Spring_2013': {'number' : '6.189x',
'title' : 'IAP Python Programming',
'xmlpath': '/6189-pytutor/',
'active' : True,
'xmlpath': '/6.189x/',
'github_url': 'https://github.com/MITx/6.189x',
'active' : True,
'default_chapter' : 'Week_1',
'default_section' : 'Variables_and_Binding',
},
'8.01_Summer_2012': {'number' : '8.01x',
'8.01_Fall_2012': {'number' : '8.01x',
'title' : 'Mechanics',
'xmlpath': '/801x-summer/',
'xmlpath': '/8.01x/',
'github_url': 'https://github.com/MITx/8.01x',
'active': True,
'default_chapter' : 'Mechanics_Online_Spring_2012',
'default_section' : 'Introduction_to_the_course',
'location': 'i4x://edx/6002xs12/course/8.01_Fall_2012',
},
'edx4edx': {'number' : 'edX.01',
'title' : 'edx4edx: edX Author Course',
'xmlpath': '/edx4edx/',
'github_url': 'https://github.com/MITx/edx4edx',
'active' : True,
'default_chapter' : 'Introduction',
'default_section' : 'edx4edx_Course',
'location': 'i4x://edx/6002xs12/course/edx4edx',
},
'7.03x_Fall_2012': {'number' : '7.03x',
'title' : 'Genetics',
'xmlpath': '/7.03x/',
'github_url': 'https://github.com/MITx/7.03x',
'active' : True,
'default_chapter' : 'Week_2',
'default_section' : 'ps1_question_1',
},
'3.091x_Fall_2012': {'number' : '3.091x',
'title' : 'Introduction to Solid State Chemistry',
'xmlpath': '/3.091x/',
'github_url': 'https://github.com/MITx/3.091x',
'active' : True,
'default_chapter' : 'Week_1',
'default_section' : 'Problem_Set_1',
},
'18.06x_Linear_Algebra': {'number' : '18.06x',
'title' : 'Linear Algebra',
'xmlpath': '/18.06x/',
'github_url': 'https://github.com/MITx/18.06x',
'default_chapter' : 'Unit_1',
'default_section' : 'Midterm_1',
'active' : True,
},
'6.00x_Fall_2012': {'number' : '6.00x',
'title' : 'Introduction to Computer Science and Programming',
'xmlpath': '/6.00x/',
'github_url': 'https://github.com/MITx/6.00x',
'active' : True,
'default_chapter' : 'Week_0',
'default_section' : 'Problem_Set_0',
'location': 'i4x://edx/6002xs12/course/6.00x_Fall_2012',
},
'7.00x_Fall_2012': {'number' : '7.00x',
'title' : 'Introduction to Biology',
'xmlpath': '/7.00x/',
'github_url': 'https://github.com/MITx/7.00x',
'active' : True,
'default_chapter' : 'Unit 1',
'default_section' : 'Introduction',
},
}
......
# Settings for edx4edx production instance
from .aws import *
COURSE_NAME = "edx4edx"
COURSE_NUMBER = "edX.01"
COURSE_TITLE = "edx4edx: edX Author Course"
EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
### Dark code. Should be enabled in local settings for devel.
QUICKEDIT = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
###
PIPELINE_CSS_COMPRESSOR = None
PIPELINE_JS_COMPRESSOR = None
COURSE_DEFAULT = 'edx4edx'
COURSE_SETTINGS = {'edx4edx': {'number' : 'edX.01',
'title' : 'edx4edx: edX Author Course',
'xmlpath': '/edx4edx/',
'github_url': 'https://github.com/MITx/edx4edx',
'active' : True,
'default_chapter' : 'Introduction',
'default_section' : 'edx4edx_Course',
},
}
STATICFILES_DIRS = [
PROJECT_ROOT / "static",
ASKBOT_ROOT / "askbot" / "skins",
("edx4edx", EDX4EDX_ROOT / "html"),
("circuits", DATA_DIR / "images"),
("handouts", DATA_DIR / "handouts"),
("subs", DATA_DIR / "subs"),
# This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images")
]
MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT ]
......@@ -20,7 +20,8 @@ INSTALLED_APPS = [
# Nose Test Runner
INSTALLED_APPS += ['django_nose']
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive', '--cover-html-dir', os.environ['NOSE_COVER_HTML_DIR']]
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html',
'--cover-inclusive', '--cover-html-dir', os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
......
......@@ -24,7 +24,7 @@ def check_problem_code(ans,the_lcp,correct_answers,false_answers):
pfn += the_lcp.problem_id.replace('filename','') # add problem ID to dogfood problem name
update_problem(pfn,ans,filestore=the_lcp.system.filestore)
msg = '<hr width="100%"/>'
msg += '<iframe src="%s/dogfood/filename%s" width="95%%" frameborder="1">No iframe support!</iframe>' % (settings.MITX_ROOT_URL,pfn)
msg += '<iframe src="%s/dogfood/filename%s" width="95%%" height="400" frameborder="1">No iframe support!</iframe>' % (settings.MITX_ROOT_URL,pfn)
msg += '<hr width="100%"/>'
endmsg = """<p><font size="-1" color="purple">Note: if the code text box disappears after clicking on "Check",
......
......@@ -25,7 +25,7 @@ import track.views
from lxml import etree
from courseware.module_render import make_track_function, I4xSystem
from courseware.module_render import make_track_function, I4xSystem, get_module
from courseware.models import StudentModule
from multicourse import multicourse_settings
from student.models import UserProfile
......@@ -55,6 +55,7 @@ def update_problem(pfn,pxml,coursename=None,overwrite=True,filestore=None):
else:
pfn2 = 'problems/%s.xml' % pfn
fp = filestore.open(pfn2,'w')
log.debug('[dogfood.update_problem] pfn2=%s' % pfn2)
if os.path.exists(pfn2) and not overwrite: return # don't overwrite if already exists and overwrite=False
pxmls = pxml if type(pxml) in [str,unicode] else etree.tostring(pxml,pretty_print=True)
......@@ -71,7 +72,7 @@ def df_capa_problem(request, id=None):
# "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY."
if settings.DEBUG:
print '[lib.dogfood.df_capa_problem] id=%s' % id
log.debug('[lib.dogfood.df_capa_problem] id=%s' % id)
if not 'coursename' in request.session:
coursename = DOGFOOD_COURSENAME
......@@ -86,7 +87,7 @@ def df_capa_problem(request, id=None):
try:
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
except Exception,err:
print "[lib.dogfood.df_capa_problem] error in calling content_parser: %s" % err
log.error("[lib.dogfood.df_capa_problem] error in calling content_parser: %s" % err)
xml = None
# if problem of given ID does not exist, then create it
......@@ -96,7 +97,7 @@ def df_capa_problem(request, id=None):
if not m:
raise Exception,'[lib.dogfood.df_capa_problem] Illegal problem id %s' % id
pfn = m.group(1)
print '[lib.dogfood.df_capa_problem] creating new problem pfn=%s' % pfn
log.debug('[lib.dogfood.df_capa_problem] creating new problem pfn=%s' % pfn)
# add problem to course.xml
fn = settings.DATA_DIR + xp + 'course.xml'
......@@ -126,7 +127,7 @@ def df_capa_problem(request, id=None):
'groups' : groups}
filename = xp + 'course.xml'
cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups))
print '[lib.dogfood.df_capa_problem] cache_key = %s' % cache_key
log.debug('[lib.dogfood.df_capa_problem] cache_key = %s' % cache_key)
#cache.delete(cache_key)
tree = content_parser.course_xml_process(xml) # add ID tags
cache.set(cache_key,etree.tostring(tree),60)
......@@ -134,7 +135,7 @@ def df_capa_problem(request, id=None):
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
if not xml:
print "[lib.dogfood.df_capa_problem] problem xml not found!"
log.debug("[lib.dogfood.df_capa_problem] problem xml not found!")
# add problem ID to list so that is_staff check can be bypassed
request.session['dogfood_id'] = id
......@@ -171,6 +172,31 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
def get_lcp(coursename,id):
# Grab the XML corresponding to the request from course.xml
# create empty student state for this problem, if not previously existing
s = StudentModule.objects.filter(student=request.user,
module_id=id)
student_module_cache = list(s) if s is not None else []
#if len(s) == 0 or s is None:
# smod=StudentModule(student=request.user,
# module_type = 'problem',
# module_id=id,
# state=instance.get_state())
# smod.save()
# student_module_cache = [smod]
module = 'problem'
module_xml = etree.XML(content_parser.module_xml(request.user, module, 'id', id, coursename))
module_id = module_xml.get('id')
log.debug("module_id = %s" % module_id)
(instance,smod,module_type) = get_module(request.user, request, module_xml, student_module_cache, position=None)
log.debug('[dogfood.views] instance=%s' % instance)
lcp = instance.lcp
log.debug('[dogfood.views] lcp=%s' % lcp)
pxml = lcp.tree
pxmls = etree.tostring(pxml,pretty_print=True)
return instance, pxmls
def old_get_lcp(coursename,id):
# Grab the XML corresponding to the request from course.xml
module = 'problem'
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
......@@ -280,7 +306,8 @@ def quickedit_git_reload(request):
if 'gitupdate' in request.POST:
import os # FIXME - put at top?
cmd = "cd ../data%s; git reset --hard HEAD; git pull origin %s" % (xp,xp.replace('/',''))
#cmd = "cd ../data%s; git reset --hard HEAD; git pull origin %s" % (xp,xp.replace('/',''))
cmd = "cd ../data%s; ./GITRELOAD '%s'" % (xp,xp.replace('/',''))
msg += '<p>cmd: %s</p>' % cmd
ret = os.popen(cmd).read()
msg += '<p><pre>%s</pre></p>' % ret.replace('<','&lt;')
......
......@@ -5,7 +5,9 @@
# Python functions which duplicate the standard comparison functions available to LON-CAPA problems.
# Used in translating LON-CAPA problems to i4x problem specification language.
from __future__ import division
import random
import math
def lc_random(lower,upper,stepsize):
'''
......@@ -15,3 +17,20 @@ def lc_random(lower,upper,stepsize):
choices = [lower+x*stepsize for x in range(nstep)]
return random.choice(choices)
def lc_choose(index,*args):
'''
return args[index]
'''
try:
return args[int(index)-1]
except Exception,err:
pass
if len(args):
return args[0]
raise Exception,"loncapa_check.lc_choose error, index=%s, args=%s" % (index,args)
deg2rad = math.pi/180.0
rad2deg = 180.0/math.pi
......@@ -231,6 +231,9 @@ def symmath_check(expect,ans,dynamath=None,options=None,debug=None):
dm = my_evalf(sympy.Matrix(fexpect)-sympy.Matrix(xgiven),chop=True)
if abs(dm.vec().norm().evalf())<threshold:
return {'ok': True,'msg': msg}
except sympy.ShapeError:
msg += "<p>Error - your input vector or matrix has the wrong dimensions"
return {'ok':False,'msg':msg}
except Exception,err:
msg += "<p>Error %s in comparing expected (a list) and your answer</p>" % str(err).replace('<','&lt;')
if DEBUG: msg += "<p/><pre>%s</pre>" % traceback.format_exc()
......
......@@ -205,17 +205,17 @@ section.problem-set {
table {
margin-bottom: lh();
width: 100%;
border: 1px solid #ddd;
// border: 1px solid #ddd;
border-collapse: collapse;
th {
border-bottom: 2px solid #ccc;
// border-bottom: 2px solid #ccc;
font-weight: bold;
text-align: left;
}
td {
border: 1px solid #ddd;
// border: 1px solid #ddd;
}
caption, th, td {
......@@ -253,11 +253,12 @@ section.problem-set {
visibility: hidden;
}
input[type="text"] {
display: inline-block;
width: 50%;
#{$all-text-inputs} {
display: inline;
width: auto;
}
// this supports a deprecated element and should be removed once the center tag is removed
center {
display: block;
margin: lh() 0;
......
......@@ -23,7 +23,7 @@
% endfor
## add a link to allow course.xml to be reloaded from the git content repo
% if settings.QUICKEDIT:
<h3><a href="#">quickedit</a></h3>
<ul><li><a href="${MITX_ROOT_URL}/quickedit/course.xml">gitreload</a></li></ul>
% endif
##% if settings.QUICKEDIT:
## <h3><a href="#">quickedit</a></h3>
## <ul><li><a href="${MITX_ROOT_URL}/quickedit/course.xml">gitreload</a></li></ul>
##% endif
<%inherit file="main.html" />
<%block name="bodyclass">courseware</%block>
<%block name="title"><title>Courseware – MITx 6.002x</title></%block>
<%block name="title"><title>Courseware – edX</title></%block>
<%include file="course_navigation.html" args="active_page='courseware'" />
<section class="main-content">
<section class="outside-app">
<h1>There has been an error on the <em>MITx</em> servers</h1>
<h1>There has been an error on the <em>edX</em> servers</h1>
<p>We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
</section>
</section>
<!-- TODO: Add pattern field to username. See HTML5 cookbook, page 84 for details-->
<div name="enroll_form" id="enroll_form">
<h1>Enroll in 6.002x Circuits &amp; Electronics</h1>
<h1>Enroll in edx4edx</h1>
<!--[if lte IE 8]>
<p class="ie-warning"> Enrollment requires a modern web browser with JavaScript enabled. You don't have this. You can&rsquo;t enroll without upgrading, since you couldn&rsquo;t take the course without upgrading. Feel free to download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Mozilla Firefox</a> or <a href="http://support.google.com/chrome/bin/answer.py?hl=en&answer=95346">Google Chrome</a>, for free, to enroll and take this course.</p>
<![endif]-->
<p class="disclaimer">
Please note that 6.002x has now passed its half-way point. The midterm exam and several assignment due dates for 6.002x have already passed. It is now impossible for newly enrolled students to earn a passing grade and a completion certificate for the course. However, new students have access to all of the course material that has been released for the course, so you are welcome to enroll and browse the course. </p>
</p>
<form name="enroll" id="enroll_form" method="get">
<fieldset><% if 'error' in locals(): e = error %>
......@@ -35,7 +35,7 @@ Please note that 6.002x has now passed its half-way point. The midterm exam and
<label>Full name*<span class="ui-icon ui-icon-help" id="spinner_name" style="display:inline-block;"></span></label>
<input name="name" id="ca_name" type="text" />
<div class="tip" id="sregion_name">If you successfully complete the course, you will receive an electronic certificate of accomplishment from <i>MITx</i> with this name on it.</div>
<div class="tip" id="sregion_name">If you successfully complete the course, you will receive an electronic certificate of accomplishment from <i>edX</i> with this name on it.</div>
</li>
<li class="location">
......
Someone, hopefully you, signed up for an account for MITx's on-line
offering of 6.002 using this email address. If it was you, and you'd
like to activate and use your account, copy and paste this address
into your web browser's address bar:
Someone, hopefully you, signed up for an account for edX's on-line
offering of "${ course_title}" using this email address. If it was
you, and you'd like to activate and use your account, copy and paste
this address into your web browser's address bar:
% if is_secure:
https://${ site }/activate/${ key }
% else:
http://${ site }/activate/${ key }
http://edx4edx.mitx.mit.edu/activate/${ key }
% endif
If you didn't request this, you don't need to do anything; you won't
receive any more email from us. Please do not reply to this e-mail; if
you require assistance, check the help section of the MITx web site.
you require assistance, check the help section of the edX web site.
Your account for MITx's on-line 6.002
Your account for edX's on-line ${course_title} course
......@@ -4,24 +4,24 @@
<div>
<h1> Collaboration Policy </h1>
<p> By enrolling in a course on <i>MITx</i>, you are joining a
<p> By enrolling in a course on <i>edX</i>, you are joining a
special worldwide community of learners. The aspiration
of <i>MITx</i> is to provide anyone in the world who has the
motivation and ability to engage MIT coursework the opportunity
to attain the best MIT-based educational experience that
of <i>edX</i> is to provide anyone in the world who has the
motivation and ability to engage edX coursework the opportunity
to attain the best edX-based educational experience that
Internet technology enables. You are part of the community who
will help <i>MITx</i> achieve this goal.
will help <i>edX</i> achieve this goal.
<p> <i>MITx</i> depends upon your motivation to learn the material
<p> <i>edX</i> depends upon your motivation to learn the material
and to do so with honesty. In order to participate
in <i>MITx</i>, you must agree to the Honor Code below and any
in <i>edX</i>, you must agree to the Honor Code below and any
additional terms specific to a class. This Honor Code, and any
additional terms, will be posted on each class website.
<div style="color:darkred;">
<h2> <i>MITx</i> Honor Code Pledge</h2>
<h2> <i>edX</i> Honor Code Pledge</h2>
<p> By enrolling in an <i>MITx</i> course, I agree that I will:
<p> By enrolling in an <i>edX</i> course, I agree that I will:
<ul>
<li> Complete all mid-terms and final exams with my own work
......@@ -35,14 +35,14 @@
assess student performance.
</ul>
</div>
<p> Unless otherwise indicated by the instructor of an <i>MITx</i>
course, learners on <i>MITx</i> are encouraged to:
<p> Unless otherwise indicated by the instructor of an <i>edX</i>
course, learners on <i>edX</i> are encouraged to:
<ul>
<li> Collaborate with others on the lecture videos, exercises,
homework and labs.
<li> Discuss with others general concepts and materials in
each course.
<li> Present ideas and written work to fellow <i>MITx</i>
<li> Present ideas and written work to fellow <i>edX</i>
learners or others for comment or criticism.
</ul>
</div>
......
<!-- TODO: http://docs.jquery.com/Plugins/Validation -->
<div id="login_div">
<header>
<h1>Log in to MITx</h1>
<h1>Log in to edX</h1>
<p class="no-account">If you don&rsquo;t have an account yet, <a href="#enroll" rel="leanModal">please enroll here</a></p>
</header>
<!--[if lte IE 9]>
<p class="ie-warning">You are using a browser that is not supported by <em>MITx</em>, and you might not be able to complete pieces of the course. Please download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a> or <a href="https://www.google.com/chrome">Chrome</a> to get the full experience.</p>
<p class="ie-warning">You are using a browser that is not supported by <em>edX</em>, and you might not be able to complete pieces of the course. Please download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a> or <a href="https://www.google.com/chrome">Chrome</a> to get the full experience.</p>
<![endif]-->
<form id="login_form" method="post">
......
......@@ -64,7 +64,7 @@
<body class="<%block name='bodyclass'/>">
<%include file="navigation.html" />
<!--[if lte IE 9]>
<p class="ie-warning">You are using a browser that is not supported by <em>MITx</em>, and you might not be able to complete pieces of the course. Please download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a> or <a href="https://www.google.com/chrome">Chrome</a> to get the full experience.</p>
<p class="ie-warning">You are using a browser that is not supported by <em>edX</em>, and you might not be able to complete pieces of the course. Please download the latest version of <a href="http://www.mozilla.org/en-US/firefox/new/">Firefox</a> or <a href="https://www.google.com/chrome">Chrome</a> to get the full experience.</p>
<![endif]-->
${self.body()}
......
......@@ -2,7 +2,7 @@
<%namespace name='static' file='static_content.html'/>
<%namespace name="profile_graphs" file="profile_graphs.js"/>
<%block name="title"><title>Profile - MITx 6.002x</title></%block>
<%block name="title"><title>Profile - edX 6.002x</title></%block>
<%!
from django.core.urlresolvers import reverse
......@@ -234,10 +234,10 @@ $(function() {
<form id="change_name_form">
<div id="change_name_error"> </div>
<fieldset>
<p>To uphold the credibility of MITx certificates, name changes must go through an approval process. A member of the course staff will review your request, and if approved, update your information. Please allow up to a week for your request to be processed. Thank you.</p>
<p>To uphold the credibility of edX certificates, name changes must go through an approval process. A member of the course staff will review your request, and if approved, update your information. Please allow up to a week for your request to be processed. Thank you.</p>
<ul>
<li>
<label>Enter your desired full name, as it will appear on the MITx Certificate: </label>
<label>Enter your desired full name, as it will appear on the edX Certificate: </label>
<input id="new_name_field" value="" type="text" />
</li>
<li>
......@@ -278,7 +278,7 @@ $(function() {
</div>
<div id="deactivate-account" class="leanModal_box">
<h1>Deactivate MITx Account</h1>
<h1>Deactivate edX Account</h1>
<p>Once you deactivate you&rsquo;re MIT<em>x</em> account you will no longer recieve updates and new class announcements from MIT<em>x</em>.</p>
<p>If you&rsquo;d like to still get updates and new class announcements you can just <a href="#unenroll" rel="leanModal">unenroll</a> and keep your account active.</p>
......@@ -287,7 +287,7 @@ $(function() {
<fieldset>
<ul>
<li>
<input type="submit" id="" value="Yes, I don't want an MITx account or hear about any new classes or updates to MITx" />
<input type="submit" id="" value="Yes, I don't want an edX account or hear about any new classes or updates to edX" />
</li>
</ul>
</fieldset>
......@@ -296,14 +296,14 @@ $(function() {
<div id="unenroll" class="leanModal_box">
<h1>Unenroll from 6.002x</h1>
<p>Please note: you will still receive updates and new class announcements from MIT<em>x</em>. If you don&rsquo;t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p>
<p>Please note: you will still receive updates and new class announcements from ed<em>X</em>. If you don&rsquo;t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p>
<form id="unenroll_form">
<div id="unenroll_error"> </div>
<fieldset>
<ul>
<li>
<input type="submit" id="" value="Yes, I want to unenroll from 6.002x but still hear about any new classes or updates to MITx" />
<input type="submit" id="" value="Yes, I want to unenroll from 6.002x but still hear about any new classes or updates to edX" />
</li>
</ul>
</fieldset>
......
......@@ -61,7 +61,7 @@
<%include file="mathjax_include.html" />
</head>
<body class="courseware">
<body class="courseware" style="text-align:left;" >
<style type="text/css">
......
<%inherit file="main.html" />
<section class="outside-app">
<h1>Currently the <em>MITx</em> servers are down</h1>
<h1>Currently the <em>edX</em> servers are down</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
</section>
<%inherit file="main.html" />
<section class="outside-app">
<h1>There has been an error on the <em>MITx</em> servers</h1>
<h1>There has been an error on the <em>edX</em> servers</h1>
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
</section>
......@@ -3,6 +3,9 @@ ${module_content}
definition = ${definition | h}
metadata = ${metadata | h}
</div>
%if edit_link:
<div><a href="${edit_link}">Edit</a></div>
% endif
%if render_histogram:
<div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div>
%endif
......@@ -120,6 +120,22 @@ task :package do
FileUtils.mkdir_p(BUILD_DIR)
Dir.chdir(BUILD_DIR) do
afterremove = Tempfile.new('afterremove')
afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '')
#! /bin/bash
set -e
set -x
# to be a little safer this rm is executed
# as the makeitso user
if [[ -d "#{INSTALL_DIR_PATH}" ]]; then
sudo -u makeitso rm -rf "#{INSTALL_DIR_PATH}"
fi
AFTERREMOVE
afterremove.close()
FileUtils.chmod(0755, afterremove.path)
postinstall = Tempfile.new('postinstall')
postinstall.write <<-POSTINSTALL.gsub(/^\s*/, '')
......@@ -128,15 +144,12 @@ task :package do
set -x
chown -R makeitso:makeitso #{INSTALL_DIR_PATH}
chmod +x #{INSTALL_DIR_PATH}/collect_static_resources
service gunicorn stop || echo "Unable to stop gunicorn. Continuing"
rm -f #{LINK_PATH}
ln -s #{INSTALL_DIR_PATH} #{LINK_PATH}
chown makeitso:makeitso #{LINK_PATH}
/opt/wwc/mitx/collect_static_resources
# Delete mako temp files
rm -rf /tmp/tmp*mako
......@@ -148,6 +161,7 @@ task :package do
args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb",
"--verbose",
"--after-install=#{postinstall.path}",
"--after-remove=#{afterremove.path}",
"--prefix=#{INSTALL_DIR_PATH}",
"--exclude=**/build/**",
"--exclude=**/rakefile",
......
django<1.4
pip
numpy
scipy
matplotlib
markdown
......@@ -24,8 +25,13 @@ sympy
newrelic
glob2
pymongo
-e common/lib/capa
-e common/lib/mitxmako
-e common/lib/xmodule
django_nose
nosexcover
rednose
-e common/lib/xmodule
GitPython >= 0.3
django-override-settings
mock>=0.8, <0.9
PyYAML
local_repo
remote_repo
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