Merge branch 'dogfood' of github.com:MITx/mitx into dogfood
Conflicts: templates/mathjax_include.html
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,4 +18,5 @@ coverage.xml
|
||||
cover/
|
||||
log/
|
||||
reports/
|
||||
\#*\#
|
||||
/src/
|
||||
\#*\#
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -1,5 +1,5 @@
|
||||
source :rubygems
|
||||
|
||||
gem 'rake', '0.8.3'
|
||||
gem 'rake'
|
||||
gem 'sass', '3.1.15'
|
||||
gem 'bourbon', '~> 1.3.6'
|
||||
|
||||
@@ -3,7 +3,7 @@ GEM
|
||||
specs:
|
||||
bourbon (1.3.6)
|
||||
sass (>= 3.1)
|
||||
rake (0.8.3)
|
||||
rake (0.9.2.2)
|
||||
sass (3.1.15)
|
||||
|
||||
PLATFORMS
|
||||
@@ -11,5 +11,5 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
bourbon (~> 1.3.6)
|
||||
rake (= 0.8.3)
|
||||
rake
|
||||
sass (= 3.1.15)
|
||||
|
||||
8
brew-formulas.txt
Normal file
8
brew-formulas.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
readline
|
||||
sqlite
|
||||
gdbm
|
||||
pkg-config
|
||||
gfortran
|
||||
python
|
||||
yuicompressor
|
||||
node
|
||||
273
create-dev-env.sh
Executable file
273
create-dev-env.sh
Executable file
@@ -0,0 +1,273 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
trap "ouch" ERR
|
||||
|
||||
ouch() {
|
||||
printf '\E[31m'
|
||||
|
||||
cat<<EOL
|
||||
|
||||
!! ERROR !!
|
||||
|
||||
The last command did not complete successfully,
|
||||
see $LOG for more details or trying running the
|
||||
script again with the -v flag.
|
||||
|
||||
EOL
|
||||
|
||||
}
|
||||
error() {
|
||||
printf '\E[31m'; echo "$@"; printf '\E[0m'
|
||||
}
|
||||
output() {
|
||||
printf '\E[36m'; echo "$@"; printf '\E[0m'
|
||||
}
|
||||
usage() {
|
||||
cat<<EO
|
||||
|
||||
Usage: $PROG [-c] [-v] [-h]
|
||||
|
||||
-c compile scipy and numpy
|
||||
-v set -x + spew
|
||||
-h this
|
||||
|
||||
EO
|
||||
info
|
||||
}
|
||||
|
||||
info() {
|
||||
|
||||
cat<<EO
|
||||
MITx base dir : $BASE
|
||||
Python dir : $PYTHON_DIR
|
||||
Ruby dir : $RUBY_DIR
|
||||
Ruby ver : $RUBY_VER
|
||||
|
||||
EO
|
||||
}
|
||||
|
||||
clone_repos() {
|
||||
cd "$BASE"
|
||||
output "Cloning mitx"
|
||||
if [[ -d "$BASE/mitx" ]]; then
|
||||
mv "$BASE/mitx" "${BASE}/mitx.bak.$$"
|
||||
fi
|
||||
git clone git@github.com:MITx/mitx.git >>$LOG
|
||||
output "Cloning askbot-devel"
|
||||
if [[ -d "$BASE/askbot-devel" ]]; then
|
||||
mv "$BASE/askbot-devel" "${BASE}/askbot-devel.bak.$$"
|
||||
fi
|
||||
git clone git@github.com:MITx/askbot-devel >>$LOG
|
||||
output "Cloning data"
|
||||
if [[ -d "$BASE/data" ]]; then
|
||||
mv "$BASE/data" "${BASE}/data.bak.$$"
|
||||
fi
|
||||
hg clone ssh://hg-content@gp.mitx.mit.edu/data >>$LOG
|
||||
}
|
||||
|
||||
PROG=${0##*/}
|
||||
BASE="$HOME/mitx_all"
|
||||
PYTHON_DIR="$BASE/python"
|
||||
RUBY_DIR="$BASE/ruby"
|
||||
RUBY_VER="1.9.3"
|
||||
NUMPY_VER="1.6.2"
|
||||
SCIPY_VER="0.10.1"
|
||||
BREW_FILE="$BASE/mitx/brew-formulas.txt"
|
||||
LOG="/var/tmp/install.log"
|
||||
APT_PKGS="curl git mercurial python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript"
|
||||
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
error "This script should not be run using sudo or as the root user"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
ARGS=$(getopt "cvh" "$*")
|
||||
if [[ $? != 0 ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
eval set -- "$ARGS"
|
||||
while true; do
|
||||
case $1 in
|
||||
-c)
|
||||
compile=true
|
||||
shift
|
||||
;;
|
||||
-v)
|
||||
set -x
|
||||
verbose=true
|
||||
shift
|
||||
;;
|
||||
-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cat<<EO
|
||||
|
||||
This script will setup a local MITx environment, this
|
||||
includes
|
||||
|
||||
* Django
|
||||
* A local copy of Python and library dependencies
|
||||
* A local copy of Ruby and library dependencies
|
||||
|
||||
It will also attempt to install operating system dependencies
|
||||
with apt(debian) or brew(OSx).
|
||||
|
||||
To compile scipy and numpy from source use the -c option
|
||||
|
||||
STDOUT is redirected to /var/tmp/install.log, run
|
||||
$ tail -f /var/tmp/install.log
|
||||
to monitor progress
|
||||
|
||||
EO
|
||||
info
|
||||
output "Press return to begin or control-C to abort"
|
||||
read dummy
|
||||
|
||||
if [[ -f $HOME/.rvmrc ]]; then
|
||||
output "$HOME/.rvmrc alredy exists, not adding $RUBY_DIR"
|
||||
else
|
||||
output "Creating $HOME/.rmrc so rvm uses $RUBY_DIR"
|
||||
echo "export rvm_path=$RUBY_DIR" > $HOME/.rvmrc
|
||||
fi
|
||||
mkdir -p $BASE
|
||||
rm -f $LOG
|
||||
case `uname -s` in
|
||||
[Ll]inux)
|
||||
command -v lsb_release &>/dev/null || {
|
||||
error "Please install lsb-release."
|
||||
exit 1
|
||||
}
|
||||
distro=`lsb_release -cs`
|
||||
case $distro in
|
||||
lisa|natty|oneiric|precise)
|
||||
output "Installing ubuntu requirements"
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install $APT_PKGS
|
||||
clone_repos
|
||||
;;
|
||||
*)
|
||||
error "Unsupported distribution - $distro"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
Darwin)
|
||||
command -v brew &>/dev/null || {
|
||||
output "Installing brew"
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)"
|
||||
}
|
||||
command -v git &>/dev/null || {
|
||||
output "Installing git"
|
||||
brew install git >> $LOG
|
||||
}
|
||||
command -v hg &>/dev/null || {
|
||||
output "Installaing mercurial"
|
||||
brew install mercurial >> $LOG
|
||||
}
|
||||
|
||||
clone_repos
|
||||
|
||||
output "Installing OSX requirements"
|
||||
if [[ ! -r $BREW_FILE ]]; then
|
||||
error "$BREW_FILE does not exist, needed to install brew deps"
|
||||
exit 1
|
||||
fi
|
||||
# brew errors if the package is already installed
|
||||
for pkg in $(cat $BREW_FILE); do
|
||||
grep $pkg <(brew list) &>/dev/null || {
|
||||
output "Installing $pkg"
|
||||
brew install $pkg >>$LOG
|
||||
}
|
||||
done
|
||||
command -v pip &>/dev/null || {
|
||||
output "Installing pip"
|
||||
sudo easy_install pip >>$LOG
|
||||
}
|
||||
command -v virtualenv &>/dev/null || {
|
||||
output "Installing virtualenv"
|
||||
sudo pip install virtualenv virtualenvwrapper >> $LOG
|
||||
}
|
||||
command -v coffee &>/dev/null || {
|
||||
output "Installing coffee script"
|
||||
curl http://npmjs.org/install.sh | sh
|
||||
npm install -g coffee-script
|
||||
}
|
||||
;;
|
||||
*)
|
||||
error "Unsupported platform"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
output "Installing rvm and ruby"
|
||||
curl -sL get.rvm.io | bash -s stable
|
||||
source $RUBY_DIR/scripts/rvm
|
||||
rvm install $RUBY_VER
|
||||
virtualenv "$PYTHON_DIR"
|
||||
source $PYTHON_DIR/bin/activate
|
||||
output "Installing gem bundler"
|
||||
gem install bundler
|
||||
output "Installing ruby packages"
|
||||
# hack :(
|
||||
cd $BASE/mitx || true
|
||||
bundle install
|
||||
|
||||
cd $BASE
|
||||
|
||||
if [[ -n $compile ]]; then
|
||||
output "Downloading numpy and scipy"
|
||||
curl -sL -o numpy.tar.gz http://downloads.sourceforge.net/project/numpy/NumPy/${NUMPY_VER}/numpy-${NUMPY_VER}.tar.gz
|
||||
curl -sL -o scipy.tar.gz http://downloads.sourceforge.net/project/scipy/scipy/${SCIPY_VER}/scipy-${SCIPY_VER}.tar.gz
|
||||
tar xf numpy.tar.gz
|
||||
tar xf scipy.tar.gz
|
||||
rm -f numpy.tar.gz scipy.tar.gz
|
||||
output "Compiling numpy"
|
||||
cd "$BASE/numpy-${NUMPY_VER}"
|
||||
python setup.py install >>$LOG 2>&1
|
||||
output "Compiling scipy"
|
||||
cd "$BASE/scipy-${SCIPY_VER}"
|
||||
python setup.py install >>$LOG 2>&1
|
||||
cd "$BASE"
|
||||
rm -rf numpy-${NUMPY_VER} scipy-${SCIPY_VER}
|
||||
fi
|
||||
|
||||
output "Installing askbot requirements"
|
||||
pip install -r askbot-devel/askbot_requirements.txt >>$LOG
|
||||
pip install -r askbot-devel/askbot_requirements_dev.txt >>$LOG
|
||||
output "Installing MITx requirements"
|
||||
pip install -r mitx/pre-requirements.txt >> $LOG
|
||||
pip install -r mitx/requirements.txt >>$LOG
|
||||
|
||||
mkdir "$BASE/log" || true
|
||||
mkdir "$BASE/db" || true
|
||||
|
||||
cat<<END
|
||||
|
||||
Success!!
|
||||
|
||||
To start using Django you will need
|
||||
to activate the local Python and Ruby
|
||||
environment:
|
||||
|
||||
$ source $RUBY_DIR/scripts/rvm
|
||||
$ source $PYTHON_DIR/bin/activate
|
||||
|
||||
To initialize and start a local instance of Django:
|
||||
|
||||
$ cd $BASE/mitx
|
||||
$ django-admin.py syncdb --settings=envs.dev --pythonpath=.
|
||||
$ django-admin.py migrate --settings=envs.dev --pythonpath=.
|
||||
$ django-admin.py runserver --settings=envs.dev --pythonpath=.
|
||||
|
||||
END
|
||||
exit 0
|
||||
|
||||
@@ -93,7 +93,7 @@ class LoncapaProblem(object):
|
||||
self.problem_id = id
|
||||
self.system = system
|
||||
|
||||
if seed != None:
|
||||
if seed is not None:
|
||||
self.seed = seed
|
||||
|
||||
if state:
|
||||
|
||||
@@ -133,7 +133,7 @@ class SimpleInput():# XModule
|
||||
|
||||
|
||||
def register_render_function(fn, names=None, cls=SimpleInput):
|
||||
if names == None:
|
||||
if names is None:
|
||||
SimpleInput.xml_tags[fn.__name__] = fn
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -70,10 +70,8 @@ class GenericResponse(object):
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class MultipleChoiceResponse(GenericResponse):
|
||||
'''
|
||||
Example:
|
||||
|
||||
<multiplechoiceresponse direction="vertical" randomize="yes">
|
||||
# TODO: handle direction and randomize
|
||||
snippets = [{'snippet': '''<multiplechoiceresponse direction="vertical" randomize="yes">
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice location="random" correct="false"><span>`a+b`<br/></span></choice>
|
||||
<choice location="random" correct="true"><span><math>a+b^2</math><br/></span></choice>
|
||||
@@ -81,10 +79,7 @@ class MultipleChoiceResponse(GenericResponse):
|
||||
<choice location="bottom" correct="false"><math>a+b+d</math></choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
TODO: handle direction and randomize
|
||||
|
||||
'''
|
||||
'''}]
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]',
|
||||
@@ -118,7 +113,7 @@ class MultipleChoiceResponse(GenericResponse):
|
||||
if rtype not in ["MultipleChoice"]:
|
||||
response.set("type", "MultipleChoice") # force choicegroup to be MultipleChoice if not valid
|
||||
for choice in list(response):
|
||||
if choice.get("name") == None:
|
||||
if choice.get("name") is None:
|
||||
choice.set("name", "choice_"+str(i))
|
||||
i+=1
|
||||
else:
|
||||
@@ -130,7 +125,7 @@ class TrueFalseResponse(MultipleChoiceResponse):
|
||||
for response in self.xml.xpath("choicegroup"):
|
||||
response.set("type", "TrueFalse")
|
||||
for choice in list(response):
|
||||
if choice.get("name") == None:
|
||||
if choice.get("name") is None:
|
||||
choice.set("name", "choice_"+str(i))
|
||||
i+=1
|
||||
else:
|
||||
@@ -149,16 +144,13 @@ class TrueFalseResponse(MultipleChoiceResponse):
|
||||
|
||||
class OptionResponse(GenericResponse):
|
||||
'''
|
||||
Example:
|
||||
|
||||
<optionresponse direction="vertical" randomize="yes">
|
||||
TODO: handle direction and randomize
|
||||
'''
|
||||
snippets = [{'snippet': '''<optionresponse direction="vertical" randomize="yes">
|
||||
<optioninput options="('Up','Down')" correct="Up"><text>The location of the sky</text></optioninput>
|
||||
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
|
||||
</optionresponse>
|
||||
</optionresponse>'''}]
|
||||
|
||||
TODO: handle direction and randomize
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.answer_fields = xml.findall('optioninput')
|
||||
@@ -227,10 +219,8 @@ class CustomResponse(GenericResponse):
|
||||
'''
|
||||
Custom response. The python code to be run should be in <answer>...</answer>
|
||||
or in a <script>...</script>
|
||||
|
||||
Example:
|
||||
|
||||
<customresponse>
|
||||
'''
|
||||
snippets = [{'snippet': '''<customresponse>
|
||||
<startouttext/>
|
||||
<br/>
|
||||
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
|
||||
@@ -248,11 +238,8 @@ class CustomResponse(GenericResponse):
|
||||
if not(r=="IS*u(t-t0)"):
|
||||
correct[0] ='incorrect'
|
||||
</answer>
|
||||
</customresponse>
|
||||
|
||||
Alternatively, the check function can be defined in <script>...</script> Example:
|
||||
|
||||
<script type="loncapa/python"><![CDATA[
|
||||
</customresponse>'''},
|
||||
{'snippet': '''<script type="loncapa/python"><![CDATA[
|
||||
|
||||
def sympy_check2():
|
||||
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','<'))
|
||||
@@ -265,9 +252,8 @@ def sympy_check2():
|
||||
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
|
||||
<textline size="40" dojs="math" />
|
||||
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
|
||||
</customresponse>
|
||||
</customresponse>'''}]
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.system = system
|
||||
@@ -311,7 +297,7 @@ def sympy_check2():
|
||||
self.code = ''
|
||||
else:
|
||||
answer_src = answer.get('src')
|
||||
if answer_src != None:
|
||||
if answer_src is not None:
|
||||
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
@@ -452,10 +438,8 @@ def sympy_check2():
|
||||
class SymbolicResponse(CustomResponse):
|
||||
"""
|
||||
Symbolic math response checking, using symmath library.
|
||||
|
||||
Example:
|
||||
|
||||
<problem>
|
||||
"""
|
||||
snippets = [{'snippet': '''<problem>
|
||||
<text>Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \]
|
||||
and give the resulting \(2\times 2\) matrix: <br/>
|
||||
<symbolicresponse answer="">
|
||||
@@ -464,8 +448,7 @@ class SymbolicResponse(CustomResponse):
|
||||
<br/>
|
||||
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>.
|
||||
</text>
|
||||
</problem>
|
||||
"""
|
||||
</problem>'''}]
|
||||
def __init__(self, xml, context, system=None):
|
||||
xml.set('cfn','symmath_check')
|
||||
code = "from symmath import *"
|
||||
@@ -481,8 +464,8 @@ class ExternalResponse(GenericResponse):
|
||||
|
||||
Typically used by coding problems.
|
||||
|
||||
Example:
|
||||
<externalresponse tests="repeat:10,generate">
|
||||
'''
|
||||
snippets = [{'snippet', '''<externalresponse tests="repeat:10,generate">
|
||||
<textbox rows="10" cols="70" mode="python"/>
|
||||
<answer><![CDATA[
|
||||
initial_display = """
|
||||
@@ -519,9 +502,8 @@ main()
|
||||
"""
|
||||
]]>
|
||||
</answer>
|
||||
</externalresponse>
|
||||
</externalresponse>'''}]
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
|
||||
@@ -532,7 +514,7 @@ main()
|
||||
id=xml.get('id'))[0]
|
||||
|
||||
answer_src = answer.get('src')
|
||||
if answer_src != None:
|
||||
if answer_src is not None:
|
||||
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
@@ -640,10 +622,8 @@ class StudentInputError(Exception):
|
||||
class FormulaResponse(GenericResponse):
|
||||
'''
|
||||
Checking of symbolic math response using numerical sampling.
|
||||
|
||||
Example:
|
||||
|
||||
<problem>
|
||||
'''
|
||||
snippets = [{'snippet': '''<problem>
|
||||
|
||||
<script type="loncapa/python">
|
||||
I = "m*c^2"
|
||||
@@ -659,9 +639,8 @@ class FormulaResponse(GenericResponse):
|
||||
<textline size="40" math="1" />
|
||||
</formularesponse>
|
||||
|
||||
</problem>
|
||||
</problem>'''}]
|
||||
|
||||
'''
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.correct_answer = contextualize_text(xml.get('answer'), context)
|
||||
@@ -682,7 +661,7 @@ class FormulaResponse(GenericResponse):
|
||||
|
||||
self.context = context
|
||||
ts = xml.get('type')
|
||||
if ts == None:
|
||||
if ts is None:
|
||||
typeslist = []
|
||||
else:
|
||||
typeslist = ts.split(',')
|
||||
@@ -751,7 +730,7 @@ class SchematicResponse(GenericResponse):
|
||||
answer = xml.xpath('//*[@id=$id]//answer',
|
||||
id=xml.get('id'))[0]
|
||||
answer_src = answer.get('src')
|
||||
if answer_src != None:
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used
|
||||
else:
|
||||
self.code = answer.text
|
||||
@@ -780,15 +759,12 @@ class ImageResponse(GenericResponse):
|
||||
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
|
||||
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
|
||||
a rectangle, given as an attribute, defining the correct answer.
|
||||
|
||||
Example:
|
||||
|
||||
<imageresponse>
|
||||
"""
|
||||
snippets = [{'snippet': '''<imageresponse>
|
||||
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" />
|
||||
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
|
||||
</imageresponse>
|
||||
</imageresponse>'''}]
|
||||
|
||||
"""
|
||||
def __init__(self, xml, context, system=None):
|
||||
self.xml = xml
|
||||
self.context = context
|
||||
|
||||
@@ -126,7 +126,7 @@ def propogate_downward_tag(element, attribute_name, parent_attribute = None):
|
||||
child (A) already has that attribute, A will keep the same attribute and
|
||||
all of A's children will inherit A's attribute. This is a recursive call.'''
|
||||
|
||||
if (parent_attribute == None): #This is the entry call. Select all elements with this attribute
|
||||
if (parent_attribute is None): #This is the entry call. Select all elements with this attribute
|
||||
all_attributed_elements = element.xpath("//*[@" + attribute_name +"]")
|
||||
for attributed_element in all_attributed_elements:
|
||||
attribute_value = attributed_element.get(attribute_name)
|
||||
@@ -222,6 +222,7 @@ def section_file(user, section, coursename=None, dironly=False):
|
||||
Given a user and the name of a section, return that section.
|
||||
This is done specific to each course.
|
||||
If dironly=True then return the sections directory.
|
||||
TODO: This is a bit weird; dironly should be scrapped.
|
||||
'''
|
||||
filename = section+".xml"
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ def get_score(user, problem, cache, coursename=None):
|
||||
correct=float(response.grade)
|
||||
|
||||
# Grab max grade from cache, or if it doesn't exist, compute and save to DB
|
||||
if id in cache and response.max_grade != None:
|
||||
if id in cache and response.max_grade is not None:
|
||||
total = response.max_grade
|
||||
else:
|
||||
## HACK 1: We shouldn't specifically reference capa_module
|
||||
|
||||
@@ -33,7 +33,7 @@ class Command(BaseCommand):
|
||||
ajax_url='',
|
||||
state=None,
|
||||
track_function = lambda x,y,z:None,
|
||||
render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'})
|
||||
render_function = lambda x: {'content':'','type':'video'})
|
||||
except:
|
||||
print "==============> Error in ", etree.tostring(module)
|
||||
check = False
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
@@ -17,6 +18,7 @@ from models import StudentModule
|
||||
from multicourse import multicourse_settings
|
||||
|
||||
import courseware.modules
|
||||
import courseware.content_parser as content_parser
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -82,12 +84,11 @@ def grade_histogram(module_id):
|
||||
|
||||
grades = list(cursor.fetchall())
|
||||
grades.sort(key=lambda x:x[0]) # Probably not necessary
|
||||
if (len(grades) == 1 and grades[0][0] == None):
|
||||
if (len(grades) == 1 and grades[0][0] is None):
|
||||
return []
|
||||
return grades
|
||||
|
||||
def get_state_from_module_object_preload(user, xml_module, module_object_preload):
|
||||
# Check if problem has an instance in DB
|
||||
def get_module(user, request, xml_module, module_object_preload, position=None):
|
||||
module_type=xml_module.tag
|
||||
module_class=courseware.modules.get_module_class(module_type)
|
||||
module_id=xml_module.get('id') #module_class.id_attribute) or ""
|
||||
@@ -102,34 +103,6 @@ def get_state_from_module_object_preload(user, xml_module, module_object_preload
|
||||
state=None
|
||||
else:
|
||||
state = smod.state
|
||||
|
||||
return smod, state
|
||||
|
||||
def render_x_module(user, request, xml_module, module_object_preload,position=None):
|
||||
''' Generic module for extensions. This renders to HTML.
|
||||
|
||||
modules include sequential, vertical, problem, video, html
|
||||
|
||||
Note that modules can recurse. problems, video, html, can be inside sequential or vertical.
|
||||
|
||||
Arguments:
|
||||
|
||||
- user : current django User
|
||||
- request : current django HTTPrequest
|
||||
- xml_module : lxml etree of xml subtree for the current module
|
||||
- module_object_preload : list of StudentModule objects, one of which may match this module type and id
|
||||
- position : extra information from URL for user-specified position within module
|
||||
|
||||
Returns:
|
||||
|
||||
- dict which is context for HTML rendering of the specified module
|
||||
|
||||
'''
|
||||
module_type=xml_module.tag
|
||||
module_class=courseware.modules.get_module_class(module_type)
|
||||
module_id=xml_module.get('id') #module_class.id_attribute) or ""
|
||||
|
||||
smod, state = get_state_from_module_object_preload(user, xml_module, module_object_preload)
|
||||
|
||||
# get coursename if stored
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
@@ -144,7 +117,7 @@ def render_x_module(user, request, xml_module, module_object_preload,position=No
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/'
|
||||
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
render_function = lambda x: render_module(user, request, x, module_object_preload),
|
||||
render_function = lambda x: render_x_module(user, request, x, module_object_preload, position),
|
||||
ajax_url = ajax_url,
|
||||
filestore = OSFS(data_root),
|
||||
)
|
||||
@@ -164,45 +137,119 @@ def render_x_module(user, request, xml_module, module_object_preload,position=No
|
||||
smod.save()
|
||||
module_object_preload.append(smod)
|
||||
|
||||
# Grab content
|
||||
content = instance.get_html()
|
||||
init_js = instance.get_init_js()
|
||||
destory_js = instance.get_destroy_js()
|
||||
return (instance, smod, module_type)
|
||||
|
||||
# special extra information about each problem, only for users who are staff
|
||||
if user.is_staff:
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module),
|
||||
'module_id' : module_id,
|
||||
'render_histogram' : render_histogram})
|
||||
if render_histogram:
|
||||
init_js = init_js+render_to_string("staff_problem_histogram.js", {'histogram' : histogram,
|
||||
'module_id' : module_id})
|
||||
|
||||
content = {'content':content,
|
||||
"destroy_js":destory_js,
|
||||
'init_js':init_js,
|
||||
'type':module_type}
|
||||
def render_x_module(user, request, xml_module, module_object_preload, position=None):
|
||||
''' Generic module for extensions. This renders to HTML.
|
||||
|
||||
return content
|
||||
modules include sequential, vertical, problem, video, html
|
||||
|
||||
def render_module(user, request, module, module_object_preload, position=None):
|
||||
''' Generic dispatch for internal modules.
|
||||
Note that modules can recurse. problems, video, html, can be inside sequential or vertical.
|
||||
|
||||
Args:
|
||||
|
||||
- user : django User
|
||||
- request : HTTP request
|
||||
- module : ElementTree (xml) for this module
|
||||
- module_object_preload : list of StudentModule objects, one of which may match this module type and id
|
||||
- position : extra information from URL for user-specified position within module
|
||||
Arguments:
|
||||
|
||||
- user : current django User
|
||||
- request : current django HTTPrequest
|
||||
- xml_module : lxml etree of xml subtree for the current module
|
||||
- module_object_preload : list of StudentModule objects, one of which may match this module type and id
|
||||
- position : extra information from URL for user-specified position within module
|
||||
|
||||
Returns:
|
||||
|
||||
- dict which is context for HTML rendering of the specified module
|
||||
|
||||
'''
|
||||
if module==None :
|
||||
if xml_module==None :
|
||||
return {"content":""}
|
||||
return render_x_module(user, request, module, module_object_preload, position)
|
||||
|
||||
(instance, smod, module_type) = get_module(user, request, xml_module, module_object_preload, position)
|
||||
|
||||
# Grab content
|
||||
content = instance.get_html()
|
||||
|
||||
# special extra information about each problem, only for users who are staff
|
||||
if user.is_staff:
|
||||
module_id = xml_module.get('id')
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module),
|
||||
'module_id' : module_id,
|
||||
'histogram': json.dumps(histogram),
|
||||
'render_histogram' : render_histogram})
|
||||
|
||||
content = {'content':content,
|
||||
'type':module_type}
|
||||
|
||||
return content
|
||||
|
||||
def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
''' Generic view for extensions. This is where AJAX calls go.'''
|
||||
if not request.user.is_authenticated():
|
||||
return redirect('/')
|
||||
|
||||
# Grab the student information for the module from the database
|
||||
s = StudentModule.objects.filter(student=request.user,
|
||||
module_id=id)
|
||||
#s = StudentModule.get_with_caching(request.user, id)
|
||||
if len(s) == 0 or s is None:
|
||||
log.debug("Couldnt find module for user and id " + str(module) + " " + str(request.user) + " "+ str(id))
|
||||
raise Http404
|
||||
s = s[0]
|
||||
|
||||
oldgrade = s.grade
|
||||
oldstate = s.state
|
||||
|
||||
dispatch=dispatch.split('?')[0]
|
||||
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/'
|
||||
|
||||
# get coursename if stored
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
|
||||
if coursename and settings.ENABLE_MULTICOURSE:
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
data_root = settings.DATA_DIR + xp
|
||||
else:
|
||||
data_root = settings.DATA_DIR
|
||||
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
try:
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
except:
|
||||
log.exception("Unable to load module during ajax call. module=%s, dispatch=%s, id=%s" % (module, dispatch, id))
|
||||
if accepts(request, 'text/html'):
|
||||
return render_to_response("module-error.html", {})
|
||||
else:
|
||||
response = HttpResponse(json.dumps({'success': "We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible"}))
|
||||
return response
|
||||
|
||||
# Create the module
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
render_function = None,
|
||||
ajax_url = ajax_url,
|
||||
filestore = OSFS(data_root),
|
||||
)
|
||||
|
||||
try:
|
||||
instance=courseware.modules.get_module_class(module)(system,
|
||||
xml,
|
||||
id,
|
||||
state=oldstate)
|
||||
except:
|
||||
log.exception("Unable to load module instance during ajax call")
|
||||
if accepts(request, 'text/html'):
|
||||
return render_to_response("module-error.html", {})
|
||||
else:
|
||||
response = HttpResponse(json.dumps({'success': "We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible"}))
|
||||
return response
|
||||
|
||||
# Let the module handle the AJAX
|
||||
ajax_return=instance.handle_ajax(dispatch, request.POST)
|
||||
# Save the state back to the database
|
||||
s.state=instance.get_state()
|
||||
if instance.get_score():
|
||||
s.grade=instance.get_score()['score']
|
||||
if s.grade != oldgrade or s.state != oldstate:
|
||||
s.save()
|
||||
# Return whatever the module wanted to return to the client/caller
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
@@ -18,7 +18,7 @@ from lxml import etree
|
||||
## TODO: Abstract out from Django
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from x_module import XModule
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
from courseware.capa.capa_problem import LoncapaProblem, StudentInputError
|
||||
import courseware.content_parser as content_parser
|
||||
from multicourse import multicourse_settings
|
||||
@@ -33,6 +33,9 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
return "{real:.7g}{imag:+.7g}*j".format(real = obj.real,imag = obj.imag)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
''' Interface between capa_problem and x_module. Originally a hack
|
||||
meant to be refactored out, but it seems to be serving a useful
|
||||
@@ -63,12 +66,6 @@ class Module(XModule):
|
||||
'ajax_url':self.ajax_url,
|
||||
})
|
||||
|
||||
def get_init_js(self):
|
||||
return render_to_string('problem.js',
|
||||
{'id':self.item_id,
|
||||
'ajax_url':self.ajax_url,
|
||||
})
|
||||
|
||||
def get_problem_html(self, encapsulate=True):
|
||||
html = self.lcp.get_html()
|
||||
content={'name':self.name,
|
||||
@@ -105,7 +102,7 @@ class Module(XModule):
|
||||
reset_button = False
|
||||
|
||||
# We don't need a "save" button if infinite number of attempts and non-randomized
|
||||
if self.max_attempts == None and self.rerandomize != "always":
|
||||
if self.max_attempts is None and self.rerandomize != "always":
|
||||
save_button = False
|
||||
|
||||
# Check if explanation is available, and if so, give a link
|
||||
@@ -132,8 +129,8 @@ class Module(XModule):
|
||||
|
||||
html=render_to_string('problem.html', context)
|
||||
if encapsulate:
|
||||
html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>"
|
||||
|
||||
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(id=self.item_id)+html+"</div>"
|
||||
|
||||
return html
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
@@ -248,7 +245,7 @@ class Module(XModule):
|
||||
''' Is the student still allowed to submit answers? '''
|
||||
if self.attempts == self.max_attempts:
|
||||
return True
|
||||
if self.close_date != None and datetime.datetime.utcnow() > self.close_date:
|
||||
if self.close_date is not None and datetime.datetime.utcnow() > self.close_date:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -3,12 +3,14 @@ import logging
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from x_module import XModule
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
from lxml import etree
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'filename'
|
||||
|
||||
@@ -4,7 +4,10 @@ import json
|
||||
from django.conf import settings
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from x_module import XModule
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'id'
|
||||
|
||||
@@ -4,12 +4,15 @@ from lxml import etree
|
||||
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from x_module import XModule
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
|
||||
# HACK: This shouldn't be hard-coded to two types
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
class_priority = ['video', 'problem']
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
''' Layout module which lays out content in a temporal sequence
|
||||
'''
|
||||
@@ -20,7 +23,9 @@ class Module(XModule):
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["sequential", 'tab']
|
||||
obsolete_tags = ["sequential", 'tab']
|
||||
modern_tags = ["videosequence"]
|
||||
return obsolete_tags + modern_tags
|
||||
|
||||
def get_html(self):
|
||||
self.render()
|
||||
@@ -44,64 +49,38 @@ class Module(XModule):
|
||||
def render(self):
|
||||
if self.rendered:
|
||||
return
|
||||
def j(m):
|
||||
''' jsonify contents so it can be embedded in a js array
|
||||
We also need to split </script> tags so they don't break
|
||||
mid-string'''
|
||||
if 'init_js' not in m: m['init_js']=""
|
||||
if 'type' not in m: m['init_js']=""
|
||||
content=json.dumps(m['content'])
|
||||
content=content.replace('</script>', '<"+"/script>')
|
||||
|
||||
return {'content':content,
|
||||
"destroy_js":m['destroy_js'],
|
||||
'init_js':m['init_js'],
|
||||
'type': m['type']}
|
||||
|
||||
|
||||
## Returns a set of all types of all sub-children
|
||||
child_classes = [set([i.tag for i in e.iter()]) for e in self.xmltree]
|
||||
|
||||
self.titles = json.dumps(["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \
|
||||
for e in self.xmltree])
|
||||
titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") is not None]) \
|
||||
for e in self.xmltree]
|
||||
|
||||
self.contents = [j(self.render_function(e)) \
|
||||
for e in self.xmltree]
|
||||
self.contents = self.rendered_children()
|
||||
|
||||
print self.titles
|
||||
for contents, title in zip(self.contents, titles):
|
||||
contents['title'] = title
|
||||
|
||||
for (content, element_class) in zip(self.contents, child_classes):
|
||||
new_class = 'other'
|
||||
for c in class_priority:
|
||||
if c in element_class:
|
||||
if c in element_class:
|
||||
new_class = c
|
||||
content['type'] = new_class
|
||||
|
||||
js=""
|
||||
|
||||
params={'items':self.contents,
|
||||
# Split </script> tags -- browsers handle this as end
|
||||
# of script, even if it occurs mid-string. Do this after json.dumps()ing
|
||||
# so that we can be sure of the quotations being used
|
||||
params={'items':json.dumps(self.contents).replace('</script>', '<"+"/script>'),
|
||||
'id':self.item_id,
|
||||
'position': self.position,
|
||||
'titles':self.titles}
|
||||
'titles':titles,
|
||||
'tag':self.xmltree.tag}
|
||||
|
||||
# TODO/BUG: Destroy JavaScript should only be called for the active view
|
||||
# This calls it for all the views
|
||||
#
|
||||
# To fix this, we'd probably want to have some way of assigning unique
|
||||
# IDs to sequences.
|
||||
destroy_js="".join([e['destroy_js'] for e in self.contents if 'destroy_js' in e])
|
||||
|
||||
if self.xmltree.tag == 'sequential':
|
||||
self.init_js=js+render_to_string('seq_module.js',params)
|
||||
self.destroy_js=destroy_js
|
||||
if self.xmltree.tag in ['sequential', 'videosequence']:
|
||||
self.content=render_to_string('seq_module.html',params)
|
||||
if self.xmltree.tag == 'tab':
|
||||
params['id'] = 'tab'
|
||||
self.init_js=js+render_to_string('tab_module.js',params)
|
||||
self.destroy_js=destroy_js
|
||||
self.content=render_to_string('tab_module.html',params)
|
||||
self.rendered = True
|
||||
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
@@ -109,7 +88,7 @@ class Module(XModule):
|
||||
|
||||
self.position = 1
|
||||
|
||||
if state != None:
|
||||
if state is not None:
|
||||
state = json.loads(state)
|
||||
if 'position' in state: self.position = int(state['position'])
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ import os
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from x_module import XModule
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
from lxml import etree
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
def get_state(self):
|
||||
return json.dumps({ })
|
||||
|
||||
@@ -2,9 +2,12 @@ import json
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from x_module import XModule
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
from lxml import etree
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'id'
|
||||
|
||||
@@ -13,22 +16,13 @@ class Module(XModule):
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["vertical"]
|
||||
return ["vertical", "problemset"]
|
||||
|
||||
def get_html(self):
|
||||
return render_to_string('vert_module.html',{'items':self.contents})
|
||||
|
||||
def get_init_js(self):
|
||||
return self.init_js_text
|
||||
|
||||
def get_destroy_js(self):
|
||||
return self.destroy_js_text
|
||||
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
xmltree=etree.fromstring(xml)
|
||||
self.contents=[(e.get("name"),self.render_function(e)) \
|
||||
for e in xmltree]
|
||||
self.init_js_text="".join([e[1]['init_js'] for e in self.contents if 'init_js' in e[1]])
|
||||
self.destroy_js_text="".join([e[1]['destroy_js'] for e in self.contents if 'destroy_js' in e[1]])
|
||||
|
||||
@@ -5,10 +5,13 @@ from lxml import etree
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from x_module import XModule
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
|
||||
log = logging.getLogger("mitx.courseware.modules")
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'youtube'
|
||||
video_time = 0
|
||||
@@ -30,44 +33,27 @@ class Module(XModule):
|
||||
def get_xml_tags(c):
|
||||
'''Tags in the courseware file guaranteed to correspond to the module'''
|
||||
return ["video"]
|
||||
|
||||
|
||||
def video_list(self):
|
||||
l = self.youtube.split(',')
|
||||
l = [i.split(":") for i in l]
|
||||
return json.dumps(dict(l))
|
||||
|
||||
return self.youtube
|
||||
|
||||
def get_html(self):
|
||||
return render_to_string('video.html',{'streams':self.video_list(),
|
||||
'id':self.item_id,
|
||||
'position':self.position,
|
||||
'name':self.name,
|
||||
'position':self.position,
|
||||
'name':self.name,
|
||||
'annotations':self.annotations})
|
||||
|
||||
def get_init_js(self):
|
||||
'''JavaScript code to be run when problem is shown. Be aware
|
||||
that this may happen several times on the same page
|
||||
(e.g. student switching tabs). Common functions should be put
|
||||
in the main course .js files for now. '''
|
||||
log.debug(u"INIT POSITION {0}".format(self.position))
|
||||
return render_to_string('video_init.js',{'streams':self.video_list(),
|
||||
'id':self.item_id,
|
||||
'position':self.position})+self.annotations_init
|
||||
|
||||
def get_destroy_js(self):
|
||||
return "videoDestroy(\"{0}\");".format(self.item_id)+self.annotations_destroy
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
xmltree=etree.fromstring(xml)
|
||||
self.youtube = xmltree.get('youtube')
|
||||
self.name = xmltree.get('name')
|
||||
self.position = 0
|
||||
if state != None:
|
||||
if state is not None:
|
||||
state = json.loads(state)
|
||||
if 'position' in state:
|
||||
self.position = int(float(state['position']))
|
||||
|
||||
self.annotations=[(e.get("name"),self.render_function(e)) \
|
||||
for e in xmltree]
|
||||
self.annotations_init="".join([e[1]['init_js'] for e in self.annotations if 'init_js' in e[1]])
|
||||
self.annotations_destroy="".join([e[1]['destroy_js'] for e in self.annotations if 'destroy_js' in e[1]])
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from lxml import etree
|
||||
|
||||
import courseware.progress
|
||||
|
||||
def dummy_track(event_type, event):
|
||||
@@ -17,6 +19,58 @@ class XModule(object):
|
||||
''' Tags in the courseware file guaranteed to correspond to the module '''
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_usage_tags(c):
|
||||
''' We should convert to a real module system
|
||||
For now, this tells us whether we use this as an xmodule, a CAPA response type
|
||||
or a CAPA input type '''
|
||||
return ['xmodule']
|
||||
|
||||
def get_name():
|
||||
name = self.__xmltree.get(name)
|
||||
if name:
|
||||
return name
|
||||
else:
|
||||
raise "We should iterate through children and find a default name"
|
||||
|
||||
def rendered_children(self):
|
||||
'''
|
||||
Render all children.
|
||||
This really ought to return a list of xmodules, instead of dictionaries
|
||||
'''
|
||||
children = [self.render_function(e) for e in self.__xmltree]
|
||||
return children
|
||||
|
||||
def __init__(self, system = None, xml = None, item_id = None,
|
||||
json = None, track_url=None, state=None):
|
||||
''' In most cases, you must pass state or xml'''
|
||||
if not item_id:
|
||||
raise ValueError("Missing Index")
|
||||
if not xml and not json:
|
||||
raise ValueError("xml or json required")
|
||||
if not system:
|
||||
raise ValueError("System context required")
|
||||
|
||||
self.xml = xml
|
||||
self.json = json
|
||||
self.item_id = item_id
|
||||
self.state = state
|
||||
self.DEBUG = False
|
||||
|
||||
self.__xmltree = etree.fromstring(xml) # PRIVATE
|
||||
|
||||
if system:
|
||||
## These are temporary; we really should go
|
||||
## through self.system.
|
||||
self.ajax_url = system.ajax_url
|
||||
self.tracker = system.track_function
|
||||
self.filestore = system.filestore
|
||||
self.render_function = system.render_function
|
||||
self.DEBUG = system.DEBUG
|
||||
self.system = system
|
||||
|
||||
### Functions used in the LMS
|
||||
|
||||
def get_completion(self):
|
||||
''' This is mostly unimplemented.
|
||||
It gives a progress indication -- e.g. 30 minutes of 1.5 hours watched. 3 of 5 problems done, etc. '''
|
||||
@@ -45,37 +99,45 @@ class XModule(object):
|
||||
'''
|
||||
return "Unimplemented"
|
||||
|
||||
def get_init_js(self):
|
||||
''' JavaScript code to be run when problem is shown. Be aware
|
||||
that this may happen several times on the same page
|
||||
(e.g. student switching tabs). Common functions should be put
|
||||
in the main course .js files for now. '''
|
||||
return ""
|
||||
|
||||
def get_destroy_js(self):
|
||||
''' JavaScript called to destroy the problem (e.g. when a user switches to a different tab).
|
||||
We make an attempt, but not a promise, to call this when the user closes the web page.
|
||||
'''
|
||||
return ""
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
''' dispatch is last part of the URL.
|
||||
get is a dictionary-like object '''
|
||||
return ""
|
||||
|
||||
def __init__(self, system, xml, item_id, track_url=None, state=None):
|
||||
''' In most cases, you must pass state or xml'''
|
||||
self.xml = xml
|
||||
self.item_id = item_id
|
||||
self.state = state
|
||||
self.DEBUG = False
|
||||
|
||||
if system:
|
||||
## These are temporary; we really should go
|
||||
## through self.system.
|
||||
self.ajax_url = system.ajax_url
|
||||
self.tracker = system.track_function
|
||||
self.filestore = system.filestore
|
||||
self.render_function = system.render_function
|
||||
self.DEBUG = system.DEBUG
|
||||
self.system = system
|
||||
class XModuleDescriptor(object):
|
||||
def __init__(self, xml = None, json = None):
|
||||
if not xml and not json:
|
||||
raise "XModuleDescriptor must be initalized with XML or JSON"
|
||||
if not xml:
|
||||
raise NotImplementedError("Code does not have support for JSON yet")
|
||||
|
||||
self.xml = xml
|
||||
self.json = json
|
||||
|
||||
def get_xml(self):
|
||||
''' For conversions between JSON and legacy XML representations.
|
||||
'''
|
||||
if self.xml:
|
||||
return self.xml
|
||||
else:
|
||||
raise NotImplementedError("JSON->XML Translation not implemented")
|
||||
|
||||
def get_json(self):
|
||||
''' For conversions between JSON and legacy XML representations.
|
||||
'''
|
||||
if self.json:
|
||||
raise NotImplementedError
|
||||
return self.json # TODO: Return context as well -- files, etc.
|
||||
else:
|
||||
raise NotImplementedError("XML->JSON Translation not implemented")
|
||||
|
||||
#def handle_cms_json(self):
|
||||
# raise NotImplementedError
|
||||
|
||||
#def render(self, size):
|
||||
# ''' Size: [thumbnail, small, full]
|
||||
# Small ==> what we drag around
|
||||
# Full ==> what we edit
|
||||
# '''
|
||||
# raise NotImplementedError
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import urllib
|
||||
import json
|
||||
|
||||
from fs.osfs import OSFS
|
||||
|
||||
@@ -8,7 +7,7 @@ from django.conf import settings
|
||||
from django.core.context_processors import csrf
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
#from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
@@ -17,10 +16,9 @@ from django.views.decorators.cache import cache_control
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from module_render import render_module, make_track_function, I4xSystem, get_state_from_module_object_preload
|
||||
from module_render import render_x_module, make_track_function, I4xSystem
|
||||
from models import StudentModule
|
||||
from student.models import UserProfile
|
||||
from util.views import accepts
|
||||
from multicourse import multicourse_settings
|
||||
|
||||
import courseware.content_parser as content_parser
|
||||
@@ -58,10 +56,9 @@ def profile(request, student_id = None):
|
||||
''' User profile. Show username, location, etc, as well as grades .
|
||||
We need to allow the user to change some of these settings .'''
|
||||
|
||||
if student_id == None:
|
||||
if student_id is None:
|
||||
student = request.user
|
||||
else:
|
||||
print content_parser.user_groups(request.user)
|
||||
if 'course_admin' not in content_parser.user_groups(request.user):
|
||||
raise Http404
|
||||
student = User.objects.get( id = int(student_id))
|
||||
@@ -131,7 +128,7 @@ def render_section(request, section):
|
||||
module_object_preload = []
|
||||
|
||||
try:
|
||||
module = render_module(user, request, dom, module_object_preload)
|
||||
module = render_x_module(user, request, dom, module_object_preload)
|
||||
except:
|
||||
log.exception("Unable to load module")
|
||||
context.update({
|
||||
@@ -201,17 +198,28 @@ def index(request, course=None, chapter="Using the System", section="Hints",posi
|
||||
return render_to_response('courseware-error.html', {})
|
||||
|
||||
# this is the module's parent's etree
|
||||
dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]/*[1]",
|
||||
dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]",
|
||||
course=course, chapter=chapter, section=section)
|
||||
|
||||
#print "DM", dom_module
|
||||
|
||||
if len(dom_module) == 0:
|
||||
module_wrapper = None
|
||||
else:
|
||||
module_wrapper = dom_module[0]
|
||||
|
||||
if module_wrapper is None:
|
||||
module = None
|
||||
elif module_wrapper.get("src"):
|
||||
module = content_parser.section_file(user=user, section=module_wrapper.get("src"), coursename=course)
|
||||
else:
|
||||
# this is the module's etree
|
||||
module = dom_module[0]
|
||||
module = etree.XML(etree.tostring(module_wrapper[0])) # Copy the element out of the tree
|
||||
|
||||
module_ids = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]//@id",
|
||||
course=course, chapter=chapter, section=section)
|
||||
module_ids = []
|
||||
if module is not None:
|
||||
module_ids = module.xpath("//@id",
|
||||
course=course, chapter=chapter, section=section)
|
||||
|
||||
if user.is_authenticated():
|
||||
module_object_preload = list(StudentModule.objects.filter(student=user,
|
||||
@@ -226,7 +234,7 @@ def index(request, course=None, chapter="Using the System", section="Hints",posi
|
||||
}
|
||||
|
||||
try:
|
||||
module_context = render_module(user, request, module, module_object_preload, position)
|
||||
module_context = render_x_module(user, request, module, module_object_preload, position)
|
||||
except:
|
||||
log.exception("Unable to load module")
|
||||
context.update({
|
||||
@@ -243,81 +251,6 @@ def index(request, course=None, chapter="Using the System", section="Hints",posi
|
||||
result = render_to_response('courseware.html', context)
|
||||
return result
|
||||
|
||||
|
||||
def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
''' Generic view for extensions. This is where AJAX calls go.'''
|
||||
if not request.user.is_authenticated():
|
||||
return redirect('/')
|
||||
|
||||
# Grab the student information for the module from the database
|
||||
s = StudentModule.objects.filter(student=request.user,
|
||||
module_id=id)
|
||||
#s = StudentModule.get_with_caching(request.user, id)
|
||||
if len(s) == 0 or s is None:
|
||||
log.debug("Couldnt find module for user and id " + str(module) + " " + str(request.user) + " "+ str(id))
|
||||
raise Http404
|
||||
s = s[0]
|
||||
|
||||
oldgrade = s.grade
|
||||
oldstate = s.state
|
||||
|
||||
dispatch=dispatch.split('?')[0]
|
||||
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/'
|
||||
|
||||
# get coursename if stored
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
|
||||
if coursename and settings.ENABLE_MULTICOURSE:
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course
|
||||
data_root = settings.DATA_DIR + xp
|
||||
else:
|
||||
data_root = settings.DATA_DIR
|
||||
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
try:
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
except:
|
||||
log.exception("Unable to load module during ajax call")
|
||||
if accepts(request, 'text/html'):
|
||||
return render_to_response("module-error.html", {})
|
||||
else:
|
||||
response = HttpResponse(json.dumps({'success': "We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible"}))
|
||||
return response
|
||||
|
||||
# Create the module
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
render_function = None,
|
||||
ajax_url = ajax_url,
|
||||
filestore = OSFS(data_root),
|
||||
)
|
||||
|
||||
try:
|
||||
instance=courseware.modules.get_module_class(module)(system,
|
||||
xml,
|
||||
id,
|
||||
state=oldstate)
|
||||
except:
|
||||
log.exception("Unable to load module instance during ajax call")
|
||||
log.exception('module=%s, dispatch=%s, id=%s' % (module,dispatch,id))
|
||||
# log.exception('xml = %s' % xml)
|
||||
if accepts(request, 'text/html'):
|
||||
return render_to_response("module-error.html", {})
|
||||
else:
|
||||
response = HttpResponse(json.dumps({'success': "We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible"}))
|
||||
return response
|
||||
|
||||
# Let the module handle the AJAX
|
||||
ajax_return=instance.handle_ajax(dispatch, request.POST)
|
||||
# Save the state back to the database
|
||||
s.state=instance.get_state()
|
||||
if instance.get_score():
|
||||
s.grade=instance.get_score()['score']
|
||||
if s.grade != oldgrade or s.state != oldstate:
|
||||
s.save()
|
||||
# Return whatever the module wanted to return to the client/caller
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
def jump_to(request, probname=None):
|
||||
'''
|
||||
Jump to viewing a specific problem. The problem is specified by a problem name - currently the filename (minus .xml)
|
||||
|
||||
@@ -482,7 +482,7 @@ def check_permissions(request, article, check_read=False, check_write=False, che
|
||||
|
||||
locked_err = check_locked and article.locked
|
||||
|
||||
if revision == None:
|
||||
if revision is None:
|
||||
revision = article.current_revision
|
||||
deleted_err = check_deleted and not (revision.deleted == 0)
|
||||
if (request.user.is_superuser):
|
||||
|
||||
@@ -20,6 +20,7 @@ Longer TODO:
|
||||
"""
|
||||
import sys
|
||||
import tempfile
|
||||
import glob2
|
||||
|
||||
import djcelery
|
||||
from path import path
|
||||
@@ -286,13 +287,12 @@ PIPELINE_CSS = {
|
||||
|
||||
PIPELINE_JS = {
|
||||
'application': {
|
||||
'source_filenames': [
|
||||
'coffee/src/calculator.coffee',
|
||||
'coffee/src/courseware.coffee',
|
||||
'coffee/src/feedback_form.coffee',
|
||||
'coffee/src/main.coffee'
|
||||
],
|
||||
'source_filenames': [path.replace('static/', '') for path in glob2.glob('static/coffee/src/**/*.coffee')],
|
||||
'output_filename': 'js/application.js'
|
||||
},
|
||||
'spec': {
|
||||
'source_filenames': [path.replace('static/', '') for path in glob2.glob('static/coffee/spec/**/*.coffee')],
|
||||
'output_filename': 'js/spec.js'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,9 @@ PIPELINE_YUI_BINARY = 'yui-compressor'
|
||||
PIPELINE_SASS_BINARY = 'sass'
|
||||
PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee'
|
||||
|
||||
# Setting that will only affect the MITx version of django-pipeline until our changes are merged upstream
|
||||
PIPELINE_COMPILE_INPLACE = True
|
||||
|
||||
################################### APPS #######################################
|
||||
INSTALLED_APPS = (
|
||||
# Standard ones that are always installed...
|
||||
|
||||
78
install.txt
Normal file
78
install.txt
Normal file
@@ -0,0 +1,78 @@
|
||||
This document describes how to set up the MITx development environment
|
||||
for both Linux (Ubuntu) and MacOS (OSX Lion).
|
||||
|
||||
There is also a script "create-dev-env.sh" that automates these steps.
|
||||
|
||||
1) Make an mitx_all directory and clone the repos
|
||||
(download and install git and mercurial if you don't have them already)
|
||||
|
||||
mkdir ~/mitx_all
|
||||
cd ~/mitx_all
|
||||
git clone git@github.com:MITx/mitx.git
|
||||
git clone git@github.com:MITx/askbot-devel
|
||||
hg clone ssh://hg-content@gp.mitx.mit.edu/data
|
||||
|
||||
2) Install OSX dependencies (Mac users only)
|
||||
|
||||
a) Install the brew utility if necessary
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)"
|
||||
|
||||
b) Install the brew package list
|
||||
cat ~/mitx_all/mitx/brew-formulas.txt | xargs brew install
|
||||
|
||||
c) Install python pip if necessary
|
||||
sudo easy_install pip
|
||||
|
||||
d) Install python virtualenv if necessary
|
||||
sudo pip install virtualenv virtualenvwrapper
|
||||
|
||||
e) Install coffee script
|
||||
curl http://npmjs.org/install.sh | sh
|
||||
npm install -g coffee-script
|
||||
|
||||
3) Install Ubuntu dependencies (Linux users only)
|
||||
|
||||
sudo apt-get install curl python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript
|
||||
|
||||
|
||||
4) Install rvm, ruby, and libraries
|
||||
|
||||
echo "export rvm_path=$HOME/mitx_all/ruby" > $HOME/.rvmrc
|
||||
curl -sL get.rvm.io | bash -s stable
|
||||
source ~/mitx_all/ruby/scripts/rvm
|
||||
rvm install 1.9.3
|
||||
gem install bundler
|
||||
cd ~/mitx_all/mitx
|
||||
bundle install
|
||||
|
||||
5) Install python libraries
|
||||
|
||||
source ~/mitx_all/python/bin/activate
|
||||
cd ~/mitx_all
|
||||
pip install -r askbot-devel/askbot_requirements.txt
|
||||
pip install -r askbot-devel/askbot_requirements_dev.txt
|
||||
pip install -r mitx/pre-requirements.txt
|
||||
pip install -r mitx/requirements.txt
|
||||
|
||||
6) Create log and db dirs
|
||||
|
||||
mkdir ~/mitx_all/log
|
||||
mkdir ~/mitx_all/db
|
||||
|
||||
7) Start the dev server
|
||||
|
||||
To start using Django you will need
|
||||
to activate the local Python and Ruby
|
||||
environment:
|
||||
|
||||
$ source ~/mitx_all/ruby/scripts/rvm
|
||||
$ source ~/mitx_all/python/bin/activate
|
||||
|
||||
To initialize and start a local instance of Django:
|
||||
|
||||
$ cd ~/mitx_all/mitx
|
||||
$ django-admin.py syncdb --settings=envs.dev --pythonpath=.
|
||||
$ django-admin.py migrate --settings=envs.dev --pythonpath=.
|
||||
$ django-admin.py runserver --settings=envs.dev --pythonpath=.
|
||||
|
||||
|
||||
2
rakefile
2
rakefile
@@ -80,7 +80,7 @@ task :package do
|
||||
chown -R makeitso:makeitso #{INSTALL_DIR_PATH}
|
||||
chmod +x #{INSTALL_DIR_PATH}/collect_static_resources
|
||||
|
||||
service gunicorn stop
|
||||
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}
|
||||
|
||||
@@ -13,7 +13,7 @@ python-memcached
|
||||
django-celery
|
||||
path.py
|
||||
django_debug_toolbar
|
||||
django-pipeline
|
||||
-e git://github.com/MITx/django-pipeline.git@incremental_compile#egg=django-pipeline
|
||||
django-staticfiles>=1.2.1
|
||||
django-masquerade
|
||||
fs
|
||||
@@ -22,3 +22,4 @@ beautifulsoup
|
||||
requests
|
||||
sympy
|
||||
newrelic
|
||||
glob2
|
||||
|
||||
2
run.sh
2
run.sh
@@ -1 +1 @@
|
||||
django-admin.py runserver --settings=envs.dev --pythonpath=.
|
||||
django-admin.py runserver --settings=envs.dev --pythonpath=. || django-admin runserver --settings=envs.dev --pythonpath=.
|
||||
1
static/coffee/.gitignore
vendored
Normal file
1
static/coffee/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.js
|
||||
@@ -2,7 +2,8 @@
|
||||
"js_files": [
|
||||
"/static/js/jquery-1.6.2.min.js",
|
||||
"/static/js/jquery-ui-1.8.16.custom.min.js",
|
||||
"/static/js/jquery.leanModal.js"
|
||||
"/static/js/jquery.leanModal.js",
|
||||
"/static/js/flot/jquery.flot.js"
|
||||
],
|
||||
"static_files": [
|
||||
"js/application.js"
|
||||
|
||||
15
static/coffee/fixtures/items.json
Normal file
15
static/coffee/fixtures/items.json
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"content": "\"Video 1\"",
|
||||
"type": "video",
|
||||
"title": "Video 1"
|
||||
}, {
|
||||
"content": "\"Video 2\"",
|
||||
"type": "video",
|
||||
"title": "Video 2"
|
||||
}, {
|
||||
"content": "\"Sample Problem\"",
|
||||
"type": "problem",
|
||||
"title": "Sample Problem"
|
||||
}
|
||||
]
|
||||
1
static/coffee/fixtures/problem.html
Normal file
1
static/coffee/fixtures/problem.html
Normal file
@@ -0,0 +1 @@
|
||||
<section id="problem_1" class="problems-wrapper" data-url="/problem/url/"></section>
|
||||
16
static/coffee/fixtures/problem_content.html
Normal file
16
static/coffee/fixtures/problem_content.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<h2 class="problem-header">Problem Header</h2>
|
||||
|
||||
<section class="problem">
|
||||
<p>Problem Content</p>
|
||||
|
||||
<section class="action">
|
||||
<input type="hidden" name="problem_id" value="1">
|
||||
|
||||
<input class="check" type="button" value="Check">
|
||||
<input class="reset" type="button" value="Reset">
|
||||
<input class="save" type="button" value="Save">
|
||||
<input class="show" type="button" value="Show Answer">
|
||||
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
|
||||
<section class="submission_feedback"></section>
|
||||
</section>
|
||||
</section>
|
||||
20
static/coffee/fixtures/sequence.html
Normal file
20
static/coffee/fixtures/sequence.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div id="sequence_1" class="sequence">
|
||||
<nav class="sequence-nav">
|
||||
<ol id="sequence-list">
|
||||
</ol>
|
||||
|
||||
<ul class="sequence-nav-buttons">
|
||||
<li class="prev"><a href="#">Previous</a></li>
|
||||
<li class="next"><a href="#">Next</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div id="seq_content"></div>
|
||||
|
||||
<nav class="sequence-bottom">
|
||||
<ul class="sequence-nav-buttons">
|
||||
<li class="prev"><a href="#">Previous</a></li>
|
||||
<li class="next"><a href="#">Next</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
3
static/coffee/fixtures/tab.html
Normal file
3
static/coffee/fixtures/tab.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div id="tab_1" class="tab">
|
||||
<ul class="navigation"></ul>
|
||||
</div>
|
||||
12
static/coffee/fixtures/video.html
Normal file
12
static/coffee/fixtures/video.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<div class="course-content">
|
||||
<div id="video_example" class="video">
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
<div id="example"></div>
|
||||
</section>
|
||||
<section class="video-controls"></section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,6 +24,7 @@ describe 'Calculator', ->
|
||||
expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate
|
||||
|
||||
it 'prevent default behavior on form submit', ->
|
||||
jasmine.stubRequests()
|
||||
$('form#calculator').submit (e) ->
|
||||
expect(e.isDefaultPrevented()).toBeTruthy()
|
||||
e.preventDefault()
|
||||
@@ -55,12 +56,12 @@ describe 'Calculator', ->
|
||||
describe 'calculate', ->
|
||||
beforeEach ->
|
||||
$('#calculator_input').val '1+2'
|
||||
spyOn($, 'getJSON').andCallFake (url, data, callback) ->
|
||||
spyOn($, 'getWithPrefix').andCallFake (url, data, callback) ->
|
||||
callback({ result: 3 })
|
||||
@calculator.calculate()
|
||||
|
||||
it 'send data to /calculate', ->
|
||||
expect($.getJSON).toHaveBeenCalledWith '/calculate',
|
||||
expect($.getWithPrefix).toHaveBeenCalledWith '/calculate',
|
||||
equation: '1+2'
|
||||
, jasmine.any(Function)
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
(function() {
|
||||
|
||||
describe('Calculator', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('calculator.html');
|
||||
return this.calculator = new Calculator;
|
||||
});
|
||||
describe('bind', function() {
|
||||
beforeEach(function() {
|
||||
return Calculator.bind();
|
||||
});
|
||||
it('bind the calculator button', function() {
|
||||
return expect($('.calc')).toHandleWith('click', this.calculator.toggle);
|
||||
});
|
||||
it('bind the help button', function() {
|
||||
expect($('div.help-wrapper a')).toHandleWith('mouseenter', this.calculator.helpToggle);
|
||||
return expect($('div.help-wrapper a')).toHandleWith('mouseleave', this.calculator.helpToggle);
|
||||
});
|
||||
it('prevent default behavior on help button', function() {
|
||||
$('div.help-wrapper a').click(function(e) {
|
||||
return expect(e.isDefaultPrevented()).toBeTruthy();
|
||||
});
|
||||
return $('div.help-wrapper a').click();
|
||||
});
|
||||
it('bind the calculator submit', function() {
|
||||
return expect($('form#calculator')).toHandleWith('submit', this.calculator.calculate);
|
||||
});
|
||||
return it('prevent default behavior on form submit', function() {
|
||||
$('form#calculator').submit(function(e) {
|
||||
expect(e.isDefaultPrevented()).toBeTruthy();
|
||||
return e.preventDefault();
|
||||
});
|
||||
return $('form#calculator').submit();
|
||||
});
|
||||
});
|
||||
describe('toggle', function() {
|
||||
it('toggle the calculator and focus the input', function() {
|
||||
spyOn($.fn, 'focus');
|
||||
this.calculator.toggle();
|
||||
expect($('li.calc-main')).toHaveClass('open');
|
||||
return expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled();
|
||||
});
|
||||
return it('toggle the close button on the calculator button', function() {
|
||||
this.calculator.toggle();
|
||||
expect($('.calc')).toHaveClass('closed');
|
||||
this.calculator.toggle();
|
||||
return expect($('.calc')).not.toHaveClass('closed');
|
||||
});
|
||||
});
|
||||
describe('helpToggle', function() {
|
||||
return it('toggle the help overlay', function() {
|
||||
this.calculator.helpToggle();
|
||||
expect($('.help')).toHaveClass('shown');
|
||||
this.calculator.helpToggle();
|
||||
return expect($('.help')).not.toHaveClass('shown');
|
||||
});
|
||||
});
|
||||
return describe('calculate', function() {
|
||||
beforeEach(function() {
|
||||
$('#calculator_input').val('1+2');
|
||||
spyOn($, 'getJSON').andCallFake(function(url, data, callback) {
|
||||
return callback({
|
||||
result: 3
|
||||
});
|
||||
});
|
||||
return this.calculator.calculate();
|
||||
});
|
||||
it('send data to /calculate', function() {
|
||||
return expect($.getJSON).toHaveBeenCalledWith('/calculate', {
|
||||
equation: '1+2'
|
||||
}, jasmine.any(Function));
|
||||
});
|
||||
return it('update the calculator output', function() {
|
||||
return expect($('#calculator_output').val()).toEqual('3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -1,77 +1,62 @@
|
||||
describe 'Courseware', ->
|
||||
describe 'start', ->
|
||||
it 'create the navigation', ->
|
||||
spyOn(window, 'Navigation')
|
||||
Courseware.start()
|
||||
expect(window.Navigation).toHaveBeenCalled()
|
||||
|
||||
it 'create the calculator', ->
|
||||
spyOn(window, 'Calculator')
|
||||
Courseware.start()
|
||||
expect(window.Calculator).toHaveBeenCalled()
|
||||
|
||||
it 'creates the FeedbackForm', ->
|
||||
spyOn(window, 'FeedbackForm')
|
||||
Courseware.start()
|
||||
expect(window.FeedbackForm).toHaveBeenCalled()
|
||||
|
||||
it 'binds the Logger', ->
|
||||
spyOn(Logger, 'bind')
|
||||
Courseware.start()
|
||||
expect(Logger.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'bind', ->
|
||||
it 'bind the navigation', ->
|
||||
spyOn Courseware.Navigation, 'bind'
|
||||
Courseware.bind()
|
||||
expect(Courseware.Navigation.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'Navigation', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'accordion.html'
|
||||
@navigation = new Courseware.Navigation
|
||||
@courseware = new Courseware
|
||||
setFixtures """
|
||||
<div class="course-content">
|
||||
<div class="sequence"></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
describe 'bind', ->
|
||||
describe 'when the #accordion exists', ->
|
||||
describe 'when there is an active section', ->
|
||||
it 'activate the accordion with correct active section', ->
|
||||
spyOn $.fn, 'accordion'
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>')
|
||||
Courseware.Navigation.bind()
|
||||
expect($('#accordion').accordion).toHaveBeenCalledWith
|
||||
active: 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
it 'binds the content change event', ->
|
||||
@courseware.bind()
|
||||
expect($('.course-content .sequence')).toHandleWith 'contentChanged', @courseware.render
|
||||
|
||||
describe 'when there is no active section', ->
|
||||
it 'activate the accordian with section 1 as active', ->
|
||||
spyOn $.fn, 'accordion'
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>')
|
||||
Courseware.Navigation.bind()
|
||||
expect($('#accordion').accordion).toHaveBeenCalledWith
|
||||
active: 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
describe 'render', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@courseware = new Courseware
|
||||
spyOn(window, 'Histogram')
|
||||
spyOn(window, 'Problem')
|
||||
spyOn(window, 'Video')
|
||||
setFixtures """
|
||||
<div class="course-content">
|
||||
<div id="video_1" class="video" data-streams="1.0:abc1234"></div>
|
||||
<div id="video_2" class="video" data-streams="1.0:def5678"></div>
|
||||
<div id="problem_3" class="problems-wrapper" data-url="/example/url/">
|
||||
<div id="histogram_3" class="histogram" data-histogram="[[0, 1]]" style="height: 20px; display: block;">
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
@courseware.render()
|
||||
|
||||
it 'binds the accordionchange event', ->
|
||||
Courseware.Navigation.bind()
|
||||
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
|
||||
it 'detect the video elements and convert them', ->
|
||||
expect(window.Video).toHaveBeenCalledWith('1', '1.0:abc1234')
|
||||
expect(window.Video).toHaveBeenCalledWith('2', '1.0:def5678')
|
||||
|
||||
it 'bind the navigation toggle', ->
|
||||
Courseware.Navigation.bind()
|
||||
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
|
||||
it 'detect the problem element and convert it', ->
|
||||
expect(window.Problem).toHaveBeenCalledWith('3', '/example/url/')
|
||||
|
||||
describe 'when the #accordion does not exists', ->
|
||||
beforeEach ->
|
||||
$('#accordion').remove()
|
||||
|
||||
it 'does not activate the accordion', ->
|
||||
spyOn $.fn, 'accordion'
|
||||
Courseware.Navigation.bind()
|
||||
expect($('#accordion').accordion).wasNotCalled()
|
||||
|
||||
describe 'toggle', ->
|
||||
it 'toggle closed class on the wrapper', ->
|
||||
$('.course-wrapper').removeClass('closed')
|
||||
|
||||
@navigation.toggle()
|
||||
expect($('.course-wrapper')).toHaveClass('closed')
|
||||
|
||||
@navigation.toggle()
|
||||
expect($('.course-wrapper')).not.toHaveClass('closed')
|
||||
|
||||
describe 'log', ->
|
||||
beforeEach ->
|
||||
window.log_event = ->
|
||||
spyOn window, 'log_event'
|
||||
|
||||
it 'submit event log', ->
|
||||
@navigation.log {}, {
|
||||
newHeader:
|
||||
text: -> "new"
|
||||
oldHeader:
|
||||
text: -> "old"
|
||||
}
|
||||
|
||||
expect(window.log_event).toHaveBeenCalledWith 'accordion',
|
||||
newheader: 'new'
|
||||
oldheader: 'old'
|
||||
it 'detect the histrogram element and convert it', ->
|
||||
expect(window.Histogram).toHaveBeenCalledWith('3', [[0, 1]])
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
(function() {
|
||||
|
||||
describe('Courseware', function() {
|
||||
describe('bind', function() {
|
||||
return it('bind the navigation', function() {
|
||||
spyOn(Courseware.Navigation, 'bind');
|
||||
Courseware.bind();
|
||||
return expect(Courseware.Navigation.bind).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
return describe('Navigation', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('accordion.html');
|
||||
return this.navigation = new Courseware.Navigation;
|
||||
});
|
||||
describe('bind', function() {
|
||||
describe('when the #accordion exists', function() {
|
||||
describe('when there is an active section', function() {
|
||||
return it('activate the accordion with correct active section', function() {
|
||||
spyOn($.fn, 'accordion');
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>');
|
||||
Courseware.Navigation.bind();
|
||||
return expect($('#accordion').accordion).toHaveBeenCalledWith({
|
||||
active: 1,
|
||||
header: 'h3',
|
||||
autoHeight: false
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when there is no active section', function() {
|
||||
return it('activate the accordian with section 1 as active', function() {
|
||||
spyOn($.fn, 'accordion');
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>');
|
||||
Courseware.Navigation.bind();
|
||||
return expect($('#accordion').accordion).toHaveBeenCalledWith({
|
||||
active: 1,
|
||||
header: 'h3',
|
||||
autoHeight: false
|
||||
});
|
||||
});
|
||||
});
|
||||
it('binds the accordionchange event', function() {
|
||||
Courseware.Navigation.bind();
|
||||
return expect($('#accordion')).toHandleWith('accordionchange', this.navigation.log);
|
||||
});
|
||||
return it('bind the navigation toggle', function() {
|
||||
Courseware.Navigation.bind();
|
||||
return expect($('#open_close_accordion a')).toHandleWith('click', this.navigation.toggle);
|
||||
});
|
||||
});
|
||||
return describe('when the #accordion does not exists', function() {
|
||||
beforeEach(function() {
|
||||
return $('#accordion').remove();
|
||||
});
|
||||
return it('does not activate the accordion', function() {
|
||||
spyOn($.fn, 'accordion');
|
||||
Courseware.Navigation.bind();
|
||||
return expect($('#accordion').accordion).wasNotCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('toggle', function() {
|
||||
return it('toggle closed class on the wrapper', function() {
|
||||
$('.course-wrapper').removeClass('closed');
|
||||
this.navigation.toggle();
|
||||
expect($('.course-wrapper')).toHaveClass('closed');
|
||||
this.navigation.toggle();
|
||||
return expect($('.course-wrapper')).not.toHaveClass('closed');
|
||||
});
|
||||
});
|
||||
return describe('log', function() {
|
||||
beforeEach(function() {
|
||||
window.log_event = function() {};
|
||||
return spyOn(window, 'log_event');
|
||||
});
|
||||
return it('submit event log', function() {
|
||||
this.navigation.log({}, {
|
||||
newHeader: {
|
||||
text: function() {
|
||||
return "new";
|
||||
}
|
||||
},
|
||||
oldHeader: {
|
||||
text: function() {
|
||||
return "old";
|
||||
}
|
||||
}
|
||||
});
|
||||
return expect(window.log_event).toHaveBeenCalledWith('accordion', {
|
||||
newheader: 'new',
|
||||
oldheader: 'old'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -2,10 +2,10 @@ describe 'FeedbackForm', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'feedback_form.html'
|
||||
|
||||
describe 'bind', ->
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
FeedbackForm.bind()
|
||||
spyOn($, 'post').andCallFake (url, data, callback, format) ->
|
||||
new FeedbackForm
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, data, callback, format) ->
|
||||
callback()
|
||||
|
||||
it 'binds to the #feedback_button', ->
|
||||
@@ -16,7 +16,7 @@ describe 'FeedbackForm', ->
|
||||
$('#feedback_message').val 'This site is really good.'
|
||||
$('#feedback_button').click()
|
||||
|
||||
expect($.post).toHaveBeenCalledWith '/send_feedback', {
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/send_feedback', {
|
||||
subject: 'Awesome!'
|
||||
message: 'This site is really good.'
|
||||
url: window.location.href
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
(function() {
|
||||
|
||||
describe('FeedbackForm', function() {
|
||||
beforeEach(function() {
|
||||
return loadFixtures('feedback_form.html');
|
||||
});
|
||||
return describe('bind', function() {
|
||||
beforeEach(function() {
|
||||
FeedbackForm.bind();
|
||||
return spyOn($, 'post').andCallFake(function(url, data, callback, format) {
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
it('binds to the #feedback_button', function() {
|
||||
return expect($('#feedback_button')).toHandle('click');
|
||||
});
|
||||
it('post data to /send_feedback on click', function() {
|
||||
$('#feedback_subject').val('Awesome!');
|
||||
$('#feedback_message').val('This site is really good.');
|
||||
$('#feedback_button').click();
|
||||
return expect($.post).toHaveBeenCalledWith('/send_feedback', {
|
||||
subject: 'Awesome!',
|
||||
message: 'This site is really good.',
|
||||
url: window.location.href
|
||||
}, jasmine.any(Function), 'json');
|
||||
});
|
||||
return it('replace the form with a thank you message', function() {
|
||||
$('#feedback_button').click();
|
||||
return expect($('#feedback_div').html()).toEqual('Feedback submitted. Thank you');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@@ -1 +1,76 @@
|
||||
jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/"
|
||||
|
||||
jasmine.stubbedMetadata =
|
||||
abc123:
|
||||
id: 'abc123'
|
||||
duration: 100
|
||||
def456:
|
||||
id: 'def456'
|
||||
duration: 200
|
||||
bogus:
|
||||
duration: 300
|
||||
|
||||
jasmine.stubbedCaption =
|
||||
start: [0, 10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000,
|
||||
100000, 110000, 120000]
|
||||
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000',
|
||||
'Caption at 30000', 'Caption at 40000', 'Caption at 50000', 'Caption at 60000',
|
||||
'Caption at 70000', 'Caption at 80000', 'Caption at 90000', 'Caption at 100000',
|
||||
'Caption at 110000', 'Caption at 120000']
|
||||
|
||||
jasmine.stubRequests = ->
|
||||
spyOn($, 'ajax').andCallFake (settings) ->
|
||||
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
|
||||
settings.success data: jasmine.stubbedMetadata[match[1]]
|
||||
else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/
|
||||
settings.success jasmine.stubbedCaption
|
||||
else if settings.url == '/calculate' ||
|
||||
settings.url == '/6002x/modx/sequence/1/goto_position' ||
|
||||
settings.url.match(/event$/) ||
|
||||
settings.url.match(/6002x\/modx\/problem\/.+\/problem_(check|reset|show|save)$/)
|
||||
# do nothing
|
||||
else
|
||||
throw "External request attempted for #{settings.url}, which is not defined."
|
||||
|
||||
jasmine.stubYoutubePlayer = ->
|
||||
YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
|
||||
'getCurrentTime', 'getPlayerState', 'loadVideoById', 'playVideo', 'pauseVideo', 'seekTo']
|
||||
|
||||
jasmine.stubVideoPlayer = (context, enableParts) ->
|
||||
enableParts = [enableParts] unless $.isArray(enableParts)
|
||||
|
||||
suite = context.suite
|
||||
currentPartName = suite.description while suite = suite.parentSuite
|
||||
enableParts.push currentPartName
|
||||
|
||||
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoProgressSlider']
|
||||
unless $.inArray(part, enableParts) >= 0
|
||||
spyOn window, part
|
||||
|
||||
loadFixtures 'video.html'
|
||||
jasmine.stubRequests()
|
||||
YT.Player = undefined
|
||||
context.video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
jasmine.stubYoutubePlayer()
|
||||
return new VideoPlayer context.video
|
||||
|
||||
spyOn(window, 'onunload')
|
||||
|
||||
# Stub Youtube API
|
||||
window.YT =
|
||||
PlayerState:
|
||||
UNSTARTED: -1
|
||||
ENDED: 0
|
||||
PLAYING: 1
|
||||
PAUSED: 2
|
||||
BUFFERING: 3
|
||||
CUED: 5
|
||||
|
||||
# Stub jQuery.cookie
|
||||
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0'
|
||||
|
||||
# Stub jQuery.qtip
|
||||
$.fn.qtip = jasmine.createSpy 'jQuery.qtip'
|
||||
|
||||
# Stub jQuery.scrollTo
|
||||
$.fn.scrollTo = jasmine.createSpy 'jQuery.scrollTo'
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
(function() {
|
||||
|
||||
jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/";
|
||||
|
||||
}).call(this);
|
||||
46
static/coffee/spec/histogram_spec.coffee
Normal file
46
static/coffee/spec/histogram_spec.coffee
Normal file
@@ -0,0 +1,46 @@
|
||||
describe 'Histogram', ->
|
||||
beforeEach ->
|
||||
spyOn $, 'plot'
|
||||
|
||||
describe 'constructor', ->
|
||||
it 'instantiate the data arrays', ->
|
||||
histogram = new Histogram 1, []
|
||||
expect(histogram.xTicks).toEqual []
|
||||
expect(histogram.yTicks).toEqual []
|
||||
expect(histogram.data).toEqual []
|
||||
|
||||
describe 'calculate', ->
|
||||
beforeEach ->
|
||||
@histogram = new Histogram(1, [[1, 1], [2, 2], [3, 3]])
|
||||
|
||||
it 'store the correct value for data', ->
|
||||
expect(@histogram.data).toEqual [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]
|
||||
|
||||
it 'store the correct value for x ticks', ->
|
||||
expect(@histogram.xTicks).toEqual [[1, '1'], [2, '2'], [3, '3']]
|
||||
|
||||
it 'store the correct value for y ticks', ->
|
||||
expect(@histogram.yTicks).toEqual
|
||||
|
||||
describe 'render', ->
|
||||
it 'call flot with correct option', ->
|
||||
new Histogram(1, [[1, 1], [2, 2], [3, 3]])
|
||||
expect($.plot).toHaveBeenCalledWith $("#histogram_1"), [
|
||||
data: [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]
|
||||
bars:
|
||||
show: true
|
||||
align: 'center'
|
||||
lineWidth: 0
|
||||
fill: 1.0
|
||||
color: "#b72121"
|
||||
],
|
||||
xaxis:
|
||||
min: -1
|
||||
max: 4
|
||||
ticks: [[1, '1'], [2, '2'], [3, '3']]
|
||||
tickLength: 0
|
||||
yaxis:
|
||||
min: 0.0
|
||||
max: Math.log(4) * 1.1
|
||||
ticks: [[Math.log(2), '1'], [Math.log(3), '2'], [Math.log(4), '3']]
|
||||
labelWidth: 50
|
||||
35
static/coffee/spec/logger_spec.coffee
Normal file
35
static/coffee/spec/logger_spec.coffee
Normal file
@@ -0,0 +1,35 @@
|
||||
describe 'Logger', ->
|
||||
it 'expose window.log_event', ->
|
||||
jasmine.stubRequests()
|
||||
expect(window.log_event).toBe Logger.log
|
||||
|
||||
describe 'log', ->
|
||||
it 'send a request to log event', ->
|
||||
spyOn $, 'getWithPrefix'
|
||||
Logger.log 'example', 'data'
|
||||
expect($.getWithPrefix).toHaveBeenCalledWith '/event',
|
||||
event_type: 'example'
|
||||
event: '"data"'
|
||||
page: window.location.href
|
||||
|
||||
describe 'bind', ->
|
||||
beforeEach ->
|
||||
Logger.bind()
|
||||
Courseware.prefix = '/6002x'
|
||||
|
||||
afterEach ->
|
||||
window.onunload = null
|
||||
|
||||
it 'bind the onunload event', ->
|
||||
expect(window.onunload).toEqual jasmine.any(Function)
|
||||
|
||||
it 'send a request to log event', ->
|
||||
spyOn($, 'ajax')
|
||||
$(window).trigger('onunload')
|
||||
expect($.ajax).toHaveBeenCalledWith
|
||||
url: "#{Courseware.prefix}/event",
|
||||
data:
|
||||
event_type: 'page_close'
|
||||
event: ''
|
||||
page: window.location.href
|
||||
async: false
|
||||
250
static/coffee/spec/modules/problem_spec.coffee
Normal file
250
static/coffee/spec/modules/problem_spec.coffee
Normal file
@@ -0,0 +1,250 @@
|
||||
describe 'Problem', ->
|
||||
beforeEach ->
|
||||
# Stub MathJax
|
||||
window.MathJax = { Hub: { Queue: -> } }
|
||||
window.update_schematics = ->
|
||||
|
||||
loadFixtures 'problem.html'
|
||||
spyOn Logger, 'log'
|
||||
spyOn($.fn, 'load').andCallFake (url, callback) ->
|
||||
$(@).html readFixtures('problem_content.html')
|
||||
callback()
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@problem.element).toBe '#problem_1'
|
||||
|
||||
it 'set the content url', ->
|
||||
expect(@problem.content_url).toEqual '/problem/url/problem_get?id=1'
|
||||
|
||||
it 'render the content', ->
|
||||
expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @problem.bind
|
||||
|
||||
describe 'bind', ->
|
||||
beforeEach ->
|
||||
spyOn MathJax.Hub, 'Queue'
|
||||
spyOn window, 'update_schematics'
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
|
||||
it 'set mathjax typeset', ->
|
||||
expect(MathJax.Hub.Queue).toHaveBeenCalled()
|
||||
|
||||
it 'update schematics', ->
|
||||
expect(window.update_schematics).toHaveBeenCalled()
|
||||
|
||||
it 'bind answer refresh on button click', ->
|
||||
expect($('section.action input:button')).toHandleWith 'click', @problem.refreshAnswers
|
||||
|
||||
it 'bind the check button', ->
|
||||
expect($('section.action input.check')).toHandleWith 'click', @problem.check
|
||||
|
||||
it 'bind the reset button', ->
|
||||
expect($('section.action input.reset')).toHandleWith 'click', @problem.reset
|
||||
|
||||
it 'bind the show button', ->
|
||||
expect($('section.action input.show')).toHandleWith 'click', @problem.show
|
||||
|
||||
it 'bind the save button', ->
|
||||
expect($('section.action input.save')).toHandleWith 'click', @problem.save
|
||||
|
||||
describe 'render', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
@bind = @problem.bind
|
||||
spyOn @problem, 'bind'
|
||||
|
||||
describe 'with content given', ->
|
||||
beforeEach ->
|
||||
@problem.render 'Hello World'
|
||||
|
||||
it 'render the content', ->
|
||||
expect(@problem.element.html()).toEqual 'Hello World'
|
||||
|
||||
it 're-bind the content', ->
|
||||
expect(@problem.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'with no content given', ->
|
||||
it 'load the content via ajax', ->
|
||||
expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @bind
|
||||
|
||||
describe 'check', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_check event', ->
|
||||
@problem.check()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_check', 'foo=1&bar=2'
|
||||
|
||||
it 'submit the answer for check', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.check()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_check', 'foo=1&bar=2', jasmine.any(Function)
|
||||
|
||||
describe 'when the response is correct', ->
|
||||
it 'call render with returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'correct', contents: 'Correct!')
|
||||
@problem.check()
|
||||
expect(@problem.element.html()).toEqual 'Correct!'
|
||||
|
||||
describe 'when the response is incorrect', ->
|
||||
it 'call render with returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'incorrect', contents: 'Correct!')
|
||||
@problem.check()
|
||||
expect(@problem.element.html()).toEqual 'Correct!'
|
||||
|
||||
describe 'when the response is undetermined', ->
|
||||
it 'alert the response', ->
|
||||
spyOn window, 'alert'
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'Number Only!')
|
||||
@problem.check()
|
||||
expect(window.alert).toHaveBeenCalledWith 'Number Only!'
|
||||
|
||||
describe 'reset', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
|
||||
it 'log the problem_reset event', ->
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
@problem.reset()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_reset', 'foo=1&bar=2'
|
||||
|
||||
it 'POST to the problem reset page', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.reset()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_reset', { id: 1 }, jasmine.any(Function)
|
||||
|
||||
it 'render the returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback("Reset!")
|
||||
@problem.reset()
|
||||
expect(@problem.element.html()).toEqual 'Reset!'
|
||||
|
||||
describe 'show', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
@problem.element.prepend '<div id="answer_1_1" /><div id="answer_1_2" />'
|
||||
|
||||
describe 'when the answer has not yet shown', ->
|
||||
beforeEach ->
|
||||
@problem.element.removeClass 'showed'
|
||||
|
||||
it 'log the problem_show event', ->
|
||||
@problem.show()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_show', problem: 1
|
||||
|
||||
it 'fetch the answers', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.show()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_show', jasmine.any(Function)
|
||||
|
||||
it 'show the answers', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('1_1': 'One', '1_2': 'Two')
|
||||
@problem.show()
|
||||
expect($('#answer_1_1')).toHaveHtml 'One'
|
||||
expect($('#answer_1_2')).toHaveHtml 'Two'
|
||||
|
||||
it 'toggle the show answer button', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({})
|
||||
@problem.show()
|
||||
expect($('.show')).toHaveValue 'Hide Answer'
|
||||
|
||||
it 'add the showed class to element', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({})
|
||||
@problem.show()
|
||||
expect(@problem.element).toHaveClass 'showed'
|
||||
|
||||
describe 'multiple choice question', ->
|
||||
beforeEach ->
|
||||
@problem.element.prepend '''
|
||||
<label for="input_1_1_1"><input type="checkbox" name="input_1_1" id="input_1_1_1" value="1"> One</label>
|
||||
<label for="input_1_1_2"><input type="checkbox" name="input_1_1" id="input_1_1_2" value="2"> Two</label>
|
||||
<label for="input_1_1_3"><input type="checkbox" name="input_1_1" id="input_1_1_3" value="3"> Three</label>
|
||||
<label for="input_1_2_1"><input type="radio" name="input_1_2" id="input_1_2_1" value="1"> Other</label>
|
||||
'''
|
||||
|
||||
it 'set the correct_answer attribute on the choice', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('1_1': [2, 3])
|
||||
@problem.show()
|
||||
expect($('label[for="input_1_1_1"]')).not.toHaveAttr 'correct_answer', 'true'
|
||||
expect($('label[for="input_1_1_2"]')).toHaveAttr 'correct_answer', 'true'
|
||||
expect($('label[for="input_1_1_3"]')).toHaveAttr 'correct_answer', 'true'
|
||||
expect($('label[for="input_1_2_1"]')).not.toHaveAttr 'correct_answer', 'true'
|
||||
|
||||
describe 'when the answers are alreay shown', ->
|
||||
beforeEach ->
|
||||
@problem.element.addClass 'showed'
|
||||
@problem.element.prepend '''
|
||||
<label for="input_1_1_1" correct_answer="true">
|
||||
<input type="checkbox" name="input_1_1" id="input_1_1_1" value="1" />
|
||||
One
|
||||
</label>
|
||||
'''
|
||||
$('#answer_1_1').html('One')
|
||||
$('#answer_1_2').html('Two')
|
||||
|
||||
it 'hide the answers', ->
|
||||
@problem.show()
|
||||
expect($('#answer_1_1')).toHaveHtml ''
|
||||
expect($('#answer_1_2')).toHaveHtml ''
|
||||
expect($('label[for="input_1_1_1"]')).not.toHaveAttr 'correct_answer'
|
||||
|
||||
it 'toggle the show answer button', ->
|
||||
@problem.show()
|
||||
expect($('.show')).toHaveValue 'Show Answer'
|
||||
|
||||
it 'remove the showed class from element', ->
|
||||
@problem.show()
|
||||
expect(@problem.element).not.toHaveClass 'showed'
|
||||
|
||||
describe 'save', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_save event', ->
|
||||
@problem.save()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_save', 'foo=1&bar=2'
|
||||
|
||||
it 'POST to save problem', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.save()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_save', 'foo=1&bar=2', jasmine.any(Function)
|
||||
|
||||
it 'alert to the user', ->
|
||||
spyOn window, 'alert'
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'OK')
|
||||
@problem.save()
|
||||
expect(window.alert).toHaveBeenCalledWith 'Saved'
|
||||
|
||||
describe 'refreshAnswers', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, '/problem/url/'
|
||||
@problem.element.html '''
|
||||
<textarea class="CodeMirror" />
|
||||
<input id="input_1_1" name="input_1_1" class="schematic" value="one" />
|
||||
<input id="input_1_2" name="input_1_2" value="two" />
|
||||
<input id="input_bogus_3" name="input_bogus_3" value="three" />
|
||||
'''
|
||||
@stubSchematic = { update_value: jasmine.createSpy('schematic') }
|
||||
@stubCodeMirror = { save: jasmine.createSpy('CodeMirror') }
|
||||
$('input.schematic').get(0).schematic = @stubSchematic
|
||||
$('textarea.CodeMirror').get(0).CodeMirror = @stubCodeMirror
|
||||
|
||||
it 'update each schematic', ->
|
||||
@problem.refreshAnswers()
|
||||
expect(@stubSchematic.update_value).toHaveBeenCalled()
|
||||
|
||||
it 'update each code block', ->
|
||||
@problem.refreshAnswers()
|
||||
expect(@stubCodeMirror.save).toHaveBeenCalled()
|
||||
|
||||
it 'serialize all answers', ->
|
||||
@problem.refreshAnswers()
|
||||
expect(@problem.answers).toEqual "input_1_1=one&input_1_2=two"
|
||||
156
static/coffee/spec/modules/sequence_spec.coffee
Normal file
156
static/coffee/spec/modules/sequence_spec.coffee
Normal file
@@ -0,0 +1,156 @@
|
||||
describe 'Sequence', ->
|
||||
beforeEach ->
|
||||
# Stub MathJax
|
||||
window.MathJax = { Hub: { Queue: -> } }
|
||||
spyOn Logger, 'log'
|
||||
|
||||
loadFixtures 'sequence.html'
|
||||
@items = $.parseJSON readFixtures('items.json')
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@sequence = new Sequence '1', @items, 'sequence', 1
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@sequence.element).toEqual $('#sequence_1')
|
||||
|
||||
it 'build the navigation', ->
|
||||
classes = $('#sequence-list li>a').map(-> $(this).attr('class')).get()
|
||||
elements = $('#sequence-list li>a').map(-> $(this).attr('data-element')).get()
|
||||
titles = $('#sequence-list li>a>p').map(-> $(this).html()).get()
|
||||
|
||||
expect(classes).toEqual ['seq_video_active', 'seq_video_inactive', 'seq_problem_inactive']
|
||||
expect(elements).toEqual ['1', '2', '3']
|
||||
expect(titles).toEqual ['Video 1', 'Video 2', 'Sample Problem']
|
||||
|
||||
it 'bind the page events', ->
|
||||
expect(@sequence.element).toHandleWith 'contentChanged', @sequence.toggleArrows
|
||||
expect($('#sequence-list a')).toHandleWith 'click', @sequence.goto
|
||||
|
||||
it 'render the active sequence content', ->
|
||||
expect($('#seq_content').html()).toEqual 'Video 1'
|
||||
|
||||
describe 'toggleArrows', ->
|
||||
beforeEach ->
|
||||
@sequence = new Sequence '1', @items, 'sequence', 1
|
||||
|
||||
describe 'when the first tab is active', ->
|
||||
beforeEach ->
|
||||
@sequence.position = 1
|
||||
@sequence.toggleArrows()
|
||||
|
||||
it 'disable the previous button', ->
|
||||
expect($('.sequence-nav-buttons .prev a')).toHaveClass 'disabled'
|
||||
|
||||
it 'enable the next button', ->
|
||||
expect($('.sequence-nav-buttons .next a')).not.toHaveClass 'disabled'
|
||||
expect($('.sequence-nav-buttons .next a')).toHandleWith 'click', @sequence.next
|
||||
|
||||
describe 'when the middle tab is active', ->
|
||||
beforeEach ->
|
||||
@sequence.position = 2
|
||||
@sequence.toggleArrows()
|
||||
|
||||
it 'enable the previous button', ->
|
||||
expect($('.sequence-nav-buttons .prev a')).not.toHaveClass 'disabled'
|
||||
expect($('.sequence-nav-buttons .prev a')).toHandleWith 'click', @sequence.previous
|
||||
|
||||
it 'enable the next button', ->
|
||||
expect($('.sequence-nav-buttons .next a')).not.toHaveClass 'disabled'
|
||||
expect($('.sequence-nav-buttons .next a')).toHandleWith 'click', @sequence.next
|
||||
|
||||
describe 'when the last tab is active', ->
|
||||
beforeEach ->
|
||||
@sequence.position = 3
|
||||
@sequence.toggleArrows()
|
||||
|
||||
it 'enable the previous button', ->
|
||||
expect($('.sequence-nav-buttons .prev a')).not.toHaveClass 'disabled'
|
||||
expect($('.sequence-nav-buttons .prev a')).toHandleWith 'click', @sequence.previous
|
||||
|
||||
it 'disable the next button', ->
|
||||
expect($('.sequence-nav-buttons .next a')).toHaveClass 'disabled'
|
||||
|
||||
describe 'render', ->
|
||||
beforeEach ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@sequence = new Sequence '1', @items, 'sequence'
|
||||
spyOnEvent @sequence.element, 'contentChanged'
|
||||
|
||||
describe 'with a different position than the current one', ->
|
||||
beforeEach ->
|
||||
@sequence.render 1
|
||||
|
||||
describe 'with no previous position', ->
|
||||
it 'does not save the new position', ->
|
||||
expect($.postWithPrefix).not.toHaveBeenCalled()
|
||||
|
||||
describe 'with previous position', ->
|
||||
beforeEach ->
|
||||
@sequence.position = 2
|
||||
@sequence.render 1
|
||||
|
||||
it 'mark the previous tab as visited', ->
|
||||
expect($('[data-element="2"]')).toHaveClass 'seq_video_visited'
|
||||
|
||||
it 'save the new position', ->
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/sequence/1/goto_position', position: 1
|
||||
|
||||
it 'mark new tab as active', ->
|
||||
expect($('[data-element="1"]')).toHaveClass 'seq_video_active'
|
||||
|
||||
it 'render the new content', ->
|
||||
expect($('#seq_content').html()).toEqual 'Video 1'
|
||||
|
||||
it 'update the position', ->
|
||||
expect(@sequence.position).toEqual 1
|
||||
|
||||
it 'trigger contentChanged event', ->
|
||||
expect('contentChanged').toHaveBeenTriggeredOn @sequence.element
|
||||
|
||||
describe 'with the same position as the current one', ->
|
||||
it 'should not trigger contentChanged event', ->
|
||||
@sequence.position = 2
|
||||
@sequence.render 2
|
||||
expect('contentChanged').not.toHaveBeenTriggeredOn @sequence.element
|
||||
|
||||
describe 'goto', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@sequence = new Sequence '1', @items, 'sequence', 2
|
||||
$('[data-element="3"]').click()
|
||||
|
||||
it 'log the sequence goto event', ->
|
||||
expect(Logger.log).toHaveBeenCalledWith 'seq_goto', old: 2, new: 3, id: '1'
|
||||
|
||||
it 'call render on the right sequence', ->
|
||||
expect($('#seq_content').html()).toEqual 'Sample Problem'
|
||||
|
||||
describe 'next', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@sequence = new Sequence '1', @items, 'sequence', 2
|
||||
$('.sequence-nav-buttons .next a').click()
|
||||
|
||||
it 'log the next sequence event', ->
|
||||
expect(Logger.log).toHaveBeenCalledWith 'seq_next', old: 2, new: 3, id: '1'
|
||||
|
||||
it 'call render on the next sequence', ->
|
||||
expect($('#seq_content').html()).toEqual 'Sample Problem'
|
||||
|
||||
describe 'previous', ->
|
||||
beforeEach ->
|
||||
jasmine.stubRequests()
|
||||
@sequence = new Sequence '1', @items, 'sequence', 2
|
||||
$('.sequence-nav-buttons .prev a').click()
|
||||
|
||||
it 'log the previous sequence event', ->
|
||||
expect(Logger.log).toHaveBeenCalledWith 'seq_prev', old: 2, new: 1, id: '1'
|
||||
|
||||
it 'call render on the previous sequence', ->
|
||||
expect($('#seq_content').html()).toEqual 'Video 1'
|
||||
|
||||
describe 'link_for', ->
|
||||
it 'return a link for specific position', ->
|
||||
sequence = new Sequence '1', @items, 2
|
||||
expect(sequence.link_for(2)).toBe '[data-element="2"]'
|
||||
39
static/coffee/spec/modules/tab_spec.coffee
Normal file
39
static/coffee/spec/modules/tab_spec.coffee
Normal file
@@ -0,0 +1,39 @@
|
||||
describe 'Tab', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'tab.html'
|
||||
@items = $.parseJSON readFixtures('items.json')
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn($.fn, 'tabs')
|
||||
@tab = new Tab 1, @items
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@tab.element).toEqual $('#tab_1')
|
||||
|
||||
it 'build the tabs', ->
|
||||
links = $('.navigation li>a').map(-> $(this).attr('href')).get()
|
||||
expect(links).toEqual ['#tab-1-0', '#tab-1-1', '#tab-1-2']
|
||||
|
||||
it 'build the container', ->
|
||||
containers = $('section').map(-> $(this).attr('id')).get()
|
||||
expect(containers).toEqual ['tab-1-0', 'tab-1-1', 'tab-1-2']
|
||||
|
||||
it 'bind the tabs', ->
|
||||
expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow
|
||||
|
||||
describe 'onShow', ->
|
||||
beforeEach ->
|
||||
@tab = new Tab 1, @items
|
||||
$('[href="#tab-1-0"]').click()
|
||||
|
||||
it 'replace content in the container', ->
|
||||
$('[href="#tab-1-1"]').click()
|
||||
expect($('#tab-1-0').html()).toEqual ''
|
||||
expect($('#tab-1-1').html()).toEqual 'Video 2'
|
||||
expect($('#tab-1-2').html()).toEqual ''
|
||||
|
||||
it 'trigger contentChanged event on the element', ->
|
||||
spyOnEvent @tab.element, 'contentChanged'
|
||||
$('[href="#tab-1-1"]').click()
|
||||
expect('contentChanged').toHaveBeenTriggeredOn @tab.element
|
||||
294
static/coffee/spec/modules/video/video_caption_spec.coffee
Normal file
294
static/coffee/spec/modules/video/video_caption_spec.coffee
Normal file
@@ -0,0 +1,294 @@
|
||||
describe 'VideoCaption', ->
|
||||
beforeEach ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
|
||||
afterEach ->
|
||||
YT.Player = undefined
|
||||
$.fn.scrollTo.reset()
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn($, 'getWithPrefix').andCallThrough()
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
it 'set the player', ->
|
||||
expect(@caption.player).toEqual @player
|
||||
|
||||
it 'set the youtube id', ->
|
||||
expect(@caption.youtubeId).toEqual 'def456'
|
||||
|
||||
it 'create the caption element', ->
|
||||
expect($('.video')).toContain 'ol.subtitles'
|
||||
|
||||
it 'add caption control to video player', ->
|
||||
expect($('.video')).toContain 'a.hide-subtitles'
|
||||
|
||||
it 'fetch the caption', ->
|
||||
expect($.getWithPrefix).toHaveBeenCalledWith @caption.captionURL(), jasmine.any(Function)
|
||||
|
||||
it 'render the caption', ->
|
||||
expect($('.subtitles').html()).toMatch new RegExp('''
|
||||
<li data-index="0" data-start="0">Caption at 0</li>
|
||||
<li data-index="1" data-start="10000">Caption at 10000</li>
|
||||
<li data-index="2" data-start="20000">Caption at 20000</li>
|
||||
<li data-index="3" data-start="30000">Caption at 30000</li>
|
||||
<li data-index="4" data-start="40000">Caption at 40000</li>
|
||||
<li data-index="5" data-start="50000">Caption at 50000</li>
|
||||
<li data-index="6" data-start="60000">Caption at 60000</li>
|
||||
<li data-index="7" data-start="70000">Caption at 70000</li>
|
||||
<li data-index="8" data-start="80000">Caption at 80000</li>
|
||||
<li data-index="9" data-start="90000">Caption at 90000</li>
|
||||
<li data-index="10" data-start="100000">Caption at 100000</li>
|
||||
<li data-index="11" data-start="110000">Caption at 110000</li>
|
||||
<li data-index="12" data-start="120000">Caption at 120000</li>
|
||||
'''.replace(/\n/g, ''))
|
||||
|
||||
it 'add a padding element to caption', ->
|
||||
expect($('.subtitles li:first')).toBe '.spacing'
|
||||
expect($('.subtitles li:last')).toBe '.spacing'
|
||||
|
||||
it 'bind all the caption link', ->
|
||||
$('.subtitles li[data-index]').each (index, link) =>
|
||||
expect($(link)).toHandleWith 'click', @caption.seekPlayer
|
||||
|
||||
it 'bind window resize event', ->
|
||||
expect($(window)).toHandleWith 'resize', @caption.onWindowResize
|
||||
|
||||
it 'bind player resize event', ->
|
||||
expect($(@player)).toHandleWith 'resize', @caption.onWindowResize
|
||||
|
||||
it 'bind player updatePlayTime event', ->
|
||||
expect($(@player)).toHandleWith 'updatePlayTime', @caption.onUpdatePlayTime
|
||||
|
||||
it 'bind the hide caption button', ->
|
||||
expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle
|
||||
|
||||
it 'bind the mouse movement', ->
|
||||
expect($('.subtitles')).toHandleWith 'mouseenter', @caption.onMouseEnter
|
||||
expect($('.subtitles')).toHandleWith 'mouseleave', @caption.onMouseLeave
|
||||
expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement
|
||||
expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement
|
||||
expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement
|
||||
|
||||
describe 'mouse movement', ->
|
||||
beforeEach ->
|
||||
spyOn(window, 'setTimeout').andReturn 100
|
||||
spyOn window, 'clearTimeout'
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
describe 'when cursor is outside of the caption box', ->
|
||||
beforeEach ->
|
||||
$(window).trigger jQuery.Event 'mousemove'
|
||||
|
||||
it 'does not set freezing timeout', ->
|
||||
expect(@caption.frozen).toBeFalsy()
|
||||
|
||||
describe 'when cursor is in the caption box', ->
|
||||
beforeEach ->
|
||||
$('.subtitles').trigger jQuery.Event 'mouseenter'
|
||||
|
||||
it 'set the freezing timeout', ->
|
||||
expect(@caption.frozen).toEqual 100
|
||||
|
||||
describe 'when the cursor is moving', ->
|
||||
beforeEach ->
|
||||
$('.subtitles').trigger jQuery.Event 'mousemove'
|
||||
|
||||
it 'reset the freezing timeout', ->
|
||||
expect(window.clearTimeout).toHaveBeenCalledWith 100
|
||||
|
||||
describe 'when the mouse is scrolling', ->
|
||||
beforeEach ->
|
||||
$('.subtitles').trigger jQuery.Event 'mousewheel'
|
||||
|
||||
it 'reset the freezing timeout', ->
|
||||
expect(window.clearTimeout).toHaveBeenCalledWith 100
|
||||
|
||||
describe 'when cursor is moving out of the caption box', ->
|
||||
beforeEach ->
|
||||
@caption.frozen = 100
|
||||
$.fn.scrollTo.reset()
|
||||
|
||||
describe 'always', ->
|
||||
beforeEach ->
|
||||
$('.subtitles').trigger jQuery.Event 'mouseout'
|
||||
|
||||
it 'reset the freezing timeout', ->
|
||||
expect(window.clearTimeout).toHaveBeenCalledWith 100
|
||||
|
||||
it 'unfreeze the caption', ->
|
||||
expect(@caption.frozen).toBeNull()
|
||||
|
||||
describe 'when the player is playing', ->
|
||||
beforeEach ->
|
||||
spyOn(@player, 'isPlaying').andReturn true
|
||||
$('.subtitles li[data-index]:first').addClass 'current'
|
||||
$('.subtitles').trigger jQuery.Event 'mouseout'
|
||||
|
||||
it 'scroll the caption', ->
|
||||
expect($.fn.scrollTo).toHaveBeenCalled()
|
||||
|
||||
describe 'when the player is not playing', ->
|
||||
beforeEach ->
|
||||
spyOn(@player, 'isPlaying').andReturn false
|
||||
$('.subtitles').trigger jQuery.Event 'mouseout'
|
||||
|
||||
it 'does not scroll the caption', ->
|
||||
expect($.fn.scrollTo).not.toHaveBeenCalled()
|
||||
|
||||
describe 'search', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
it 'return a correct caption index', ->
|
||||
expect(@caption.search(0)).toEqual 0
|
||||
expect(@caption.search(9999)).toEqual 0
|
||||
expect(@caption.search(10000)).toEqual 1
|
||||
expect(@caption.search(15000)).toEqual 1
|
||||
expect(@caption.search(120000)).toEqual 12
|
||||
expect(@caption.search(120001)).toEqual 12
|
||||
|
||||
describe 'onUpdatePlayTime', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
describe 'when the video speed is 1.0x', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '1.0'
|
||||
@caption.onUpdatePlayTime {}, 25.000
|
||||
|
||||
it 'search the caption based on time', ->
|
||||
expect(@caption.currentIndex).toEqual 2
|
||||
|
||||
describe 'when the video speed is not 1.0x', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '0.75'
|
||||
@caption.onUpdatePlayTime {}, 25.000
|
||||
|
||||
it 'search the caption based on 1.0x speed', ->
|
||||
expect(@caption.currentIndex).toEqual 1
|
||||
|
||||
describe 'when the index is not the same', ->
|
||||
beforeEach ->
|
||||
@caption.currentIndex = 1
|
||||
$('.subtitles li[data-index=1]').addClass 'current'
|
||||
@caption.onUpdatePlayTime {}, 25.000
|
||||
|
||||
it 'deactivate the previous caption', ->
|
||||
expect($('.subtitles li[data-index=1]')).not.toHaveClass 'current'
|
||||
|
||||
it 'activate new caption', ->
|
||||
expect($('.subtitles li[data-index=2]')).toHaveClass 'current'
|
||||
|
||||
it 'save new index', ->
|
||||
expect(@caption.currentIndex).toEqual 2
|
||||
|
||||
it 'scroll caption to new position', ->
|
||||
expect($.fn.scrollTo).toHaveBeenCalled()
|
||||
|
||||
describe 'when the index is the same', ->
|
||||
beforeEach ->
|
||||
@caption.currentIndex = 1
|
||||
$('.subtitles li[data-index=1]').addClass 'current'
|
||||
@caption.onUpdatePlayTime {}, 15.000
|
||||
|
||||
it 'does not change current subtitle', ->
|
||||
expect($('.subtitles li[data-index=1]')).toHaveClass 'current'
|
||||
|
||||
describe 'onWindowResize', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
$('.subtitles li[data-index=1]').addClass 'current'
|
||||
@caption.onWindowResize()
|
||||
|
||||
it 'set the height of caption container', ->
|
||||
expect(parseInt($('.subtitles').css('maxHeight'))).toEqual $('.video-wrapper').height()
|
||||
|
||||
it 'set the height of caption spacing', ->
|
||||
expect(parseInt($('.subtitles .spacing:first').css('height'))).toEqual(
|
||||
$('.video-wrapper').height() / 2 - $('.subtitles li:not(.spacing):first').height() / 2)
|
||||
expect(parseInt($('.subtitles .spacing:last').css('height'))).toEqual(
|
||||
$('.video-wrapper').height() / 2 - $('.subtitles li:not(.spacing):last').height() / 2)
|
||||
|
||||
it 'scroll caption to new position', ->
|
||||
expect($.fn.scrollTo).toHaveBeenCalled()
|
||||
|
||||
describe 'scrollCaption', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
|
||||
describe 'when frozen', ->
|
||||
beforeEach ->
|
||||
@caption.frozen = true
|
||||
$('.subtitles li[data-index=1]').addClass 'current'
|
||||
@caption.scrollCaption()
|
||||
|
||||
it 'does not scroll the caption', ->
|
||||
expect($.fn.scrollTo).not.toHaveBeenCalled()
|
||||
|
||||
describe 'when not frozen', ->
|
||||
beforeEach ->
|
||||
@caption.frozen = false
|
||||
|
||||
describe 'when there is no current caption', ->
|
||||
beforeEach ->
|
||||
@caption.scrollCaption()
|
||||
|
||||
it 'does not scroll the caption', ->
|
||||
expect($.fn.scrollTo).not.toHaveBeenCalled()
|
||||
|
||||
describe 'when there is a current caption', ->
|
||||
beforeEach ->
|
||||
$('.subtitles li[data-index=1]').addClass 'current'
|
||||
@caption.scrollCaption()
|
||||
|
||||
it 'scroll to current caption', ->
|
||||
expect($.fn.scrollTo).toHaveBeenCalledWith $('.subtitles .current:first', @player.element),
|
||||
offset: - ($('.video-wrapper').height() / 2 - $('.subtitles .current:first').height() / 2)
|
||||
|
||||
describe 'seekPlayer', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
@time = null
|
||||
$(@player).bind 'seek', (event, time) => @time = time
|
||||
|
||||
describe 'when the video speed is 1.0x', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '1.0'
|
||||
$('.subtitles li[data-start="30000"]').click()
|
||||
|
||||
it 'trigger seek event with the correct time', ->
|
||||
expect(@time).toEqual 30.000
|
||||
|
||||
describe 'when the video speed is not 1.0x', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '0.75'
|
||||
$('.subtitles li[data-start="30000"]').click()
|
||||
|
||||
it 'trigger seek event with the correct time', ->
|
||||
expect(@time).toEqual 40.000
|
||||
|
||||
describe 'toggle', ->
|
||||
beforeEach ->
|
||||
@caption = new VideoCaption @player, 'def456'
|
||||
$('.subtitles li[data-index=1]').addClass 'current'
|
||||
|
||||
describe 'when the caption is visible', ->
|
||||
beforeEach ->
|
||||
@player.element.removeClass 'closed'
|
||||
@caption.toggle jQuery.Event('click')
|
||||
|
||||
it 'hide the caption', ->
|
||||
expect(@player.element).toHaveClass 'closed'
|
||||
|
||||
|
||||
describe 'when the caption is hidden', ->
|
||||
beforeEach ->
|
||||
@player.element.addClass 'closed'
|
||||
@caption.toggle jQuery.Event('click')
|
||||
|
||||
it 'show the caption', ->
|
||||
expect(@player.element).not.toHaveClass 'closed'
|
||||
|
||||
it 'scroll the caption', ->
|
||||
expect($.fn.scrollTo).toHaveBeenCalled()
|
||||
73
static/coffee/spec/modules/video/video_control_spec.coffee
Normal file
73
static/coffee/spec/modules/video/video_control_spec.coffee
Normal file
@@ -0,0 +1,73 @@
|
||||
describe 'VideoControl', ->
|
||||
beforeEach ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@control = new VideoControl @player
|
||||
|
||||
it 'render the video controls', ->
|
||||
expect($('.video-controls').html()).toContain '''
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control play">Play</a></li>
|
||||
<li>
|
||||
<div class="vidtime">0:00 / 0:00</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
|
||||
</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
it 'bind player events', ->
|
||||
expect($(@player)).toHandleWith 'play', @control.onPlay
|
||||
expect($(@player)).toHandleWith 'pause', @control.onPause
|
||||
expect($(@player)).toHandleWith 'ended', @control.onPause
|
||||
|
||||
it 'bind the playback button', ->
|
||||
expect($('.video_control')).toHandleWith 'click', @control.togglePlayback
|
||||
|
||||
describe 'onPlay', ->
|
||||
beforeEach ->
|
||||
@control = new VideoControl @player
|
||||
@control.onPlay()
|
||||
|
||||
it 'switch playback button to play state', ->
|
||||
expect($('.video_control')).not.toHaveClass 'play'
|
||||
expect($('.video_control')).toHaveClass 'pause'
|
||||
expect($('.video_control')).toHaveHtml 'Pause'
|
||||
|
||||
describe 'onPause', ->
|
||||
beforeEach ->
|
||||
@control = new VideoControl @player
|
||||
@control.onPause()
|
||||
|
||||
it 'switch playback button to pause state', ->
|
||||
expect($('.video_control')).not.toHaveClass 'pause'
|
||||
expect($('.video_control')).toHaveClass 'play'
|
||||
expect($('.video_control')).toHaveHtml 'Play'
|
||||
|
||||
describe 'togglePlayback', ->
|
||||
beforeEach ->
|
||||
@control = new VideoControl @player
|
||||
|
||||
describe 'when the video is playing', ->
|
||||
beforeEach ->
|
||||
spyOn(@player, 'isPlaying').andReturn true
|
||||
spyOnEvent @player, 'pause'
|
||||
@control.togglePlayback jQuery.Event('click')
|
||||
|
||||
it 'trigger the pause event', ->
|
||||
expect('pause').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is paused', ->
|
||||
beforeEach ->
|
||||
spyOn(@player, 'isPlaying').andReturn false
|
||||
spyOnEvent @player, 'play'
|
||||
@control.togglePlayback jQuery.Event('click')
|
||||
|
||||
it 'trigger the play event', ->
|
||||
expect('play').toHaveBeenTriggeredOn @player
|
||||
389
static/coffee/spec/modules/video/video_player_spec.coffee
Normal file
389
static/coffee/spec/modules/video/video_player_spec.coffee
Normal file
@@ -0,0 +1,389 @@
|
||||
describe 'VideoPlayer', ->
|
||||
beforeEach ->
|
||||
jasmine.stubVideoPlayer @
|
||||
|
||||
afterEach ->
|
||||
YT.Player = undefined
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn window, 'VideoControl'
|
||||
spyOn YT, 'Player'
|
||||
$.fn.qtip.andCallFake ->
|
||||
$(this).data('qtip', true)
|
||||
$('.video').append $('<div class="hide-subtitles" />')
|
||||
@player = new VideoPlayer @video
|
||||
|
||||
it 'instanticate current time to zero', ->
|
||||
expect(@player.currentTime).toEqual 0
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@player.element).toBe '#video_example'
|
||||
|
||||
it 'create video control', ->
|
||||
expect(window.VideoControl).toHaveBeenCalledWith @player
|
||||
|
||||
it 'create video caption', ->
|
||||
expect(window.VideoCaption).toHaveBeenCalledWith @player, 'def456'
|
||||
|
||||
it 'create video speed control', ->
|
||||
expect(window.VideoSpeedControl).toHaveBeenCalledWith @player, ['0.75', '1.0']
|
||||
|
||||
it 'create video progress slider', ->
|
||||
expect(window.VideoProgressSlider).toHaveBeenCalledWith @player
|
||||
|
||||
it 'create Youtube player', ->
|
||||
expect(YT.Player).toHaveBeenCalledWith 'example'
|
||||
playerVars:
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
videoId: 'def456'
|
||||
events:
|
||||
onReady: @player.onReady
|
||||
onStateChange: @player.onStateChange
|
||||
|
||||
it 'bind to seek event', ->
|
||||
expect($(@player)).toHandleWith 'seek', @player.onSeek
|
||||
|
||||
it 'bind to updatePlayTime event', ->
|
||||
expect($(@player)).toHandleWith 'updatePlayTime', @player.onUpdatePlayTime
|
||||
|
||||
it 'bidn to speedChange event', ->
|
||||
expect($(@player)).toHandleWith 'speedChange', @player.onSpeedChange
|
||||
|
||||
it 'bind to play event', ->
|
||||
expect($(@player)).toHandleWith 'play', @player.onPlay
|
||||
|
||||
it 'bind to paused event', ->
|
||||
expect($(@player)).toHandleWith 'pause', @player.onPause
|
||||
|
||||
it 'bind to ended event', ->
|
||||
expect($(@player)).toHandleWith 'ended', @player.onPause
|
||||
|
||||
it 'bind to key press', ->
|
||||
expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen
|
||||
|
||||
it 'bind to fullscreen switching button', ->
|
||||
expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen
|
||||
|
||||
describe 'when not on a touch based device', ->
|
||||
it 'add the tooltip to fullscreen and subtitle button', ->
|
||||
expect($('.add-fullscreen')).toHaveData 'qtip'
|
||||
expect($('.hide-subtitles')).toHaveData 'qtip'
|
||||
|
||||
describe 'onReady', ->
|
||||
beforeEach ->
|
||||
@video.embed()
|
||||
@player = @video.player
|
||||
spyOnEvent @player, 'ready'
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
@player.onReady()
|
||||
|
||||
it 'reset the progress to zero', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
it 'trigger ready event on the player', ->
|
||||
expect('ready').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when not on a touch based device', ->
|
||||
beforeEach ->
|
||||
window.onTouchBasedDevice = -> false
|
||||
spyOn @player, 'play'
|
||||
@player.onReady()
|
||||
|
||||
it 'autoplay the first video', ->
|
||||
expect(@player.play).toHaveBeenCalled()
|
||||
|
||||
describe 'when on a touch based device', ->
|
||||
beforeEach ->
|
||||
window.onTouchBasedDevice = -> true
|
||||
spyOn @player, 'play'
|
||||
@player.onReady()
|
||||
|
||||
it 'does not autoplay the first video', ->
|
||||
expect(@player.play).not.toHaveBeenCalled()
|
||||
|
||||
describe 'onStateChange', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
|
||||
describe 'when the video is playing', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'play'
|
||||
@player.onStateChange data: YT.PlayerState.PLAYING
|
||||
|
||||
it 'trigger play event', ->
|
||||
expect('play').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is paused', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'pause'
|
||||
@player.onStateChange data: YT.PlayerState.PAUSED
|
||||
|
||||
it 'trigger pause event', ->
|
||||
expect('pause').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is unstarted', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'pause'
|
||||
@player.onStateChange data: YT.PlayerState.UNSTARTED
|
||||
|
||||
it 'trigger pause event', ->
|
||||
expect('pause').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is ended', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'ended'
|
||||
@player.onStateChange data: YT.PlayerState.ENDED
|
||||
|
||||
it 'trigger ended event', ->
|
||||
expect('ended').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'onPlay', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
@anotherPlayer = jasmine.createSpyObj 'AnotherPlayer', ['pauseVideo']
|
||||
window.player = @anotherPlayer
|
||||
spyOn Logger, 'log'
|
||||
spyOn(window, 'setInterval').andReturn 100
|
||||
@player.player.getVideoEmbedCode.andReturn 'embedCode'
|
||||
@player.onPlay()
|
||||
|
||||
it 'log the play_video event', ->
|
||||
expect(Logger.log).toHaveBeenCalledWith 'play_video', id: @player.currentTime, code: 'embedCode'
|
||||
|
||||
it 'pause other video player', ->
|
||||
expect(@anotherPlayer.pauseVideo).toHaveBeenCalled()
|
||||
|
||||
it 'set current video player as active player', ->
|
||||
expect(window.player).toEqual @player.player
|
||||
|
||||
it 'set update interval', ->
|
||||
expect(window.setInterval).toHaveBeenCalledWith @player.update, 200
|
||||
expect(@player.player.interval).toEqual 100
|
||||
|
||||
describe 'onPause', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
window.player = @player.player
|
||||
spyOn Logger, 'log'
|
||||
spyOn window, 'clearInterval'
|
||||
@player.player.interval = 100
|
||||
@player.player.getVideoEmbedCode.andReturn 'embedCode'
|
||||
@player.onPause()
|
||||
|
||||
it 'log the pause_video event', ->
|
||||
expect(Logger.log).toHaveBeenCalledWith 'pause_video', id: @player.currentTime, code: 'embedCode'
|
||||
|
||||
it 'set current video player as inactive', ->
|
||||
expect(window.player).toBeNull()
|
||||
|
||||
it 'clear update interval', ->
|
||||
expect(window.clearInterval).toHaveBeenCalledWith 100
|
||||
expect(@player.player.interval).toBeNull()
|
||||
|
||||
describe 'onSeek', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
spyOn window, 'clearInterval'
|
||||
@player.player.interval = 100
|
||||
@player.onSeek {}, 60
|
||||
|
||||
it 'seek the player', ->
|
||||
expect(@player.player.seekTo).toHaveBeenCalledWith 60, true
|
||||
|
||||
describe 'when the player is playing', ->
|
||||
beforeEach ->
|
||||
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
|
||||
@player.onSeek {}, 60
|
||||
|
||||
it 'reset the update interval', ->
|
||||
expect(window.clearInterval).toHaveBeenCalledWith 100
|
||||
|
||||
describe 'when the player is not playing', ->
|
||||
beforeEach ->
|
||||
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
@player.onSeek {}, 60
|
||||
|
||||
it 'set the current time', ->
|
||||
expect(@player.currentTime).toEqual 60
|
||||
|
||||
it 'trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'onSpeedChange', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
@player.currentTime = 60
|
||||
spyOn(@video, 'setSpeed').andCallThrough()
|
||||
|
||||
describe 'always', ->
|
||||
beforeEach ->
|
||||
@player.onSpeedChange {}, '0.75'
|
||||
|
||||
it 'convert the current time to the new speed', ->
|
||||
expect(@player.currentTime).toEqual '80.000'
|
||||
|
||||
it 'set video speed to the new speed', ->
|
||||
expect(@video.setSpeed).toHaveBeenCalledWith '0.75'
|
||||
|
||||
describe 'when the video is playing', ->
|
||||
beforeEach ->
|
||||
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
@player.onSpeedChange {}, '0.75'
|
||||
|
||||
it 'load the video', ->
|
||||
expect(@player.player.loadVideoById).toHaveBeenCalledWith 'abc123', '80.000'
|
||||
|
||||
it 'trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video is not playing', ->
|
||||
beforeEach ->
|
||||
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
@player.onSpeedChange {}, '0.75'
|
||||
|
||||
it 'cue the video', ->
|
||||
expect(@player.player.cueVideoById).toHaveBeenCalledWith 'abc123', '80.000'
|
||||
|
||||
it 'trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'update', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
spyOnEvent @player, 'updatePlayTime'
|
||||
|
||||
describe 'when the current time is unavailable from the player', ->
|
||||
beforeEach ->
|
||||
@player.player.getCurrentTime.andReturn undefined
|
||||
@player.update()
|
||||
|
||||
it 'does not trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').not.toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the current time is available from the player', ->
|
||||
beforeEach ->
|
||||
@player.player.getCurrentTime.andReturn 60
|
||||
@player.update()
|
||||
|
||||
it 'trigger updatePlayTime event', ->
|
||||
expect('updatePlayTime').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'onUpdatePlaytime', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
spyOn(@video, 'getDuration').andReturn 1800
|
||||
@player.onUpdatePlayTime {}, 60
|
||||
|
||||
it 'update the video playback time', ->
|
||||
expect($('.vidtime')).toHaveHtml '1:00 / 30:00'
|
||||
|
||||
describe 'toggleFullScreen', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
|
||||
describe 'when the video player is not full screen', ->
|
||||
beforeEach ->
|
||||
@player.element.removeClass 'fullscreen'
|
||||
spyOnEvent @player, 'resize'
|
||||
@player.toggleFullScreen(jQuery.Event("click"))
|
||||
|
||||
it 'replace the full screen button tooltip', ->
|
||||
expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser'
|
||||
|
||||
it 'add a new exit from fullscreen button', ->
|
||||
expect(@player.element).toContain 'a.exit'
|
||||
|
||||
it 'add the fullscreen class', ->
|
||||
expect(@player.element).toHaveClass 'fullscreen'
|
||||
|
||||
it 'trigger resize event', ->
|
||||
expect('resize').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when the video player already full screen', ->
|
||||
beforeEach ->
|
||||
@player.element.addClass 'fullscreen'
|
||||
spyOnEvent @player, 'resize'
|
||||
@player.toggleFullScreen(jQuery.Event("click"))
|
||||
|
||||
it 'replace the full screen button tooltip', ->
|
||||
expect($('.add-fullscreen')).toHaveAttr 'title', 'Fill browser'
|
||||
|
||||
it 'remove exit full screen button', ->
|
||||
expect(@player.element).not.toContain 'a.exit'
|
||||
|
||||
it 'remove the fullscreen class', ->
|
||||
expect(@player.element).not.toHaveClass 'fullscreen'
|
||||
|
||||
it 'trigger resize event', ->
|
||||
expect('resize').toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'play', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
|
||||
describe 'when the player is not ready', ->
|
||||
beforeEach ->
|
||||
@player.player.playVideo = undefined
|
||||
@player.play()
|
||||
|
||||
it 'does nothing', ->
|
||||
expect(@player.player.playVideo).toBeUndefined()
|
||||
|
||||
describe 'when the player is ready', ->
|
||||
beforeEach ->
|
||||
@player.player.playVideo.andReturn true
|
||||
@player.play()
|
||||
|
||||
it 'delegate to the Youtube player', ->
|
||||
expect(@player.player.playVideo).toHaveBeenCalled()
|
||||
|
||||
describe 'isPlaying', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
|
||||
describe 'when the video is playing', ->
|
||||
beforeEach ->
|
||||
@player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
|
||||
|
||||
it 'return true', ->
|
||||
expect(@player.isPlaying()).toBeTruthy()
|
||||
|
||||
describe 'when the video is not playing', ->
|
||||
beforeEach ->
|
||||
@player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
|
||||
|
||||
it 'return false', ->
|
||||
expect(@player.isPlaying()).toBeFalsy()
|
||||
|
||||
describe 'pause', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
@player.pause()
|
||||
|
||||
it 'delegate to the Youtube player', ->
|
||||
expect(@player.player.pauseVideo).toHaveBeenCalled()
|
||||
|
||||
describe 'duration', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
spyOn @video, 'getDuration'
|
||||
@player.duration()
|
||||
|
||||
it 'delegate to the video', ->
|
||||
expect(@video.getDuration).toHaveBeenCalled()
|
||||
|
||||
describe 'currentSpeed', ->
|
||||
beforeEach ->
|
||||
@player = new VideoPlayer @video
|
||||
@video.speed = '3.0'
|
||||
|
||||
it 'delegate to the video', ->
|
||||
expect(@player.currentSpeed()).toEqual '3.0'
|
||||
@@ -0,0 +1,122 @@
|
||||
describe 'VideoProgressSlider', ->
|
||||
beforeEach ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
spyOn($.fn, 'slider').andCallThrough()
|
||||
@slider = new VideoProgressSlider @player
|
||||
|
||||
it 'build the slider', ->
|
||||
expect(@slider.slider).toBe '.slider'
|
||||
expect($.fn.slider).toHaveBeenCalledWith
|
||||
range: 'min'
|
||||
change: @slider.onChange
|
||||
slide: @slider.onSlide
|
||||
stop: @slider.onStop
|
||||
|
||||
it 'build the seek handle', ->
|
||||
expect(@slider.handle).toBe '.ui-slider-handle'
|
||||
expect($.fn.qtip).toHaveBeenCalledWith
|
||||
content: "0:00"
|
||||
position:
|
||||
my: 'bottom center'
|
||||
at: 'top center'
|
||||
container: @slider.handle
|
||||
hide:
|
||||
delay: 700
|
||||
style:
|
||||
classes: 'ui-tooltip-slider'
|
||||
widget: true
|
||||
|
||||
it 'bind player events', ->
|
||||
expect($(@player)).toHandleWith 'updatePlayTime', @slider.onUpdatePlayTime
|
||||
|
||||
describe 'onReady', ->
|
||||
beforeEach ->
|
||||
spyOn(@player, 'duration').andReturn 120
|
||||
@slider = new VideoProgressSlider @player
|
||||
@slider.onReady()
|
||||
|
||||
it 'set the max value to the length of video', ->
|
||||
expect(@slider.slider.slider('option', 'max')).toEqual 120
|
||||
|
||||
describe 'onUpdatePlayTime', ->
|
||||
beforeEach ->
|
||||
@slider = new VideoProgressSlider @player
|
||||
spyOn(@player, 'duration').andReturn 120
|
||||
spyOn($.fn, 'slider').andCallThrough()
|
||||
|
||||
describe 'when frozen', ->
|
||||
beforeEach ->
|
||||
@slider.frozen = true
|
||||
@slider.onUpdatePlayTime {}, 20
|
||||
|
||||
it 'does not update the slider', ->
|
||||
expect($.fn.slider).not.toHaveBeenCalled()
|
||||
|
||||
describe 'when not frozen', ->
|
||||
beforeEach ->
|
||||
@slider.frozen = false
|
||||
@slider.onUpdatePlayTime {}, 20
|
||||
|
||||
it 'update the max value of the slider', ->
|
||||
expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120
|
||||
|
||||
it 'update current value of the slider', ->
|
||||
expect($.fn.slider).toHaveBeenCalledWith 'value', 20
|
||||
|
||||
describe 'onSlide', ->
|
||||
beforeEach ->
|
||||
@slider = new VideoProgressSlider @player
|
||||
@time = null
|
||||
$(@player).bind 'seek', (event, time) => @time = time
|
||||
spyOnEvent @player, 'seek'
|
||||
@slider.onSlide {}, value: 20
|
||||
|
||||
it 'freeze the slider', ->
|
||||
expect(@slider.frozen).toBeTruthy()
|
||||
|
||||
it 'update the tooltip', ->
|
||||
expect($.fn.qtip).toHaveBeenCalled()
|
||||
|
||||
it 'trigger seek event', ->
|
||||
expect('seek').toHaveBeenTriggeredOn @player
|
||||
expect(@time).toEqual 20
|
||||
|
||||
describe 'onChange', ->
|
||||
beforeEach ->
|
||||
@slider = new VideoProgressSlider @player
|
||||
@slider.onChange {}, value: 20
|
||||
|
||||
it 'update the tooltip', ->
|
||||
expect($.fn.qtip).toHaveBeenCalled()
|
||||
|
||||
describe 'onStop', ->
|
||||
beforeEach ->
|
||||
@slider = new VideoProgressSlider @player
|
||||
@time = null
|
||||
$(@player).bind 'seek', (event, time) => @time = time
|
||||
spyOnEvent @player, 'seek'
|
||||
spyOn(window, 'setTimeout')
|
||||
@slider.onStop {}, value: 20
|
||||
|
||||
it 'freeze the slider', ->
|
||||
expect(@slider.frozen).toBeTruthy()
|
||||
|
||||
it 'trigger seek event', ->
|
||||
expect('seek').toHaveBeenTriggeredOn @player
|
||||
expect(@time).toEqual 20
|
||||
|
||||
it 'set timeout to unfreeze the slider', ->
|
||||
expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200
|
||||
window.setTimeout.mostRecentCall.args[0]()
|
||||
expect(@slider.frozen).toBeFalsy()
|
||||
|
||||
describe 'updateTooltip', ->
|
||||
beforeEach ->
|
||||
@slider = new VideoProgressSlider @player
|
||||
@slider.updateTooltip 90
|
||||
|
||||
it 'set the tooltip value', ->
|
||||
expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'
|
||||
@@ -0,0 +1,95 @@
|
||||
describe 'VideoSpeedControl', ->
|
||||
beforeEach ->
|
||||
@player = jasmine.stubVideoPlayer @
|
||||
$('.speeds').remove()
|
||||
|
||||
afterEach ->
|
||||
|
||||
describe 'constructor', ->
|
||||
describe 'always', ->
|
||||
beforeEach ->
|
||||
@speedControl = new VideoSpeedControl @player, @video.speeds
|
||||
|
||||
it 'add the video speed control to player', ->
|
||||
expect($('.secondary-controls').html()).toContain '''
|
||||
<div class="speeds">
|
||||
<a href="#">
|
||||
<h3>Speed</h3>
|
||||
<p class="active">1.0x</p>
|
||||
</a>
|
||||
<ol class="video_speeds"><li data-speed="1.0" class="active"><a href="#">1.0x</a></li><li data-speed="0.75"><a href="#">0.75x</a></li></ol>
|
||||
</div>
|
||||
'''
|
||||
|
||||
it 'bind to player speedChange event', ->
|
||||
expect($(@player)).toHandleWith 'speedChange', @speedControl.onSpeedChange
|
||||
|
||||
it 'bind to change video speed link', ->
|
||||
expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed
|
||||
|
||||
describe 'when running on touch based device', ->
|
||||
beforeEach ->
|
||||
spyOn(window, 'onTouchBasedDevice').andReturn true
|
||||
$('.speeds').removeClass 'open'
|
||||
@speedControl = new VideoSpeedControl @player, @video.speeds
|
||||
|
||||
it 'open the speed toggle on click', ->
|
||||
$('.speeds').click()
|
||||
expect($('.speeds')).toHaveClass 'open'
|
||||
$('.speeds').click()
|
||||
expect($('.speeds')).not.toHaveClass 'open'
|
||||
|
||||
describe 'when running on non-touch based device', ->
|
||||
beforeEach ->
|
||||
spyOn(window, 'onTouchBasedDevice').andReturn false
|
||||
$('.speeds').removeClass 'open'
|
||||
@speedControl = new VideoSpeedControl @player, @video.speeds
|
||||
|
||||
it 'open the speed toggle on hover', ->
|
||||
$('.speeds').mouseenter()
|
||||
expect($('.speeds')).toHaveClass 'open'
|
||||
$('.speeds').mouseleave()
|
||||
expect($('.speeds')).not.toHaveClass 'open'
|
||||
|
||||
it 'close the speed toggle on mouse out', ->
|
||||
$('.speeds').mouseenter().mouseleave()
|
||||
expect($('.speeds')).not.toHaveClass 'open'
|
||||
|
||||
it 'close the speed toggle on click', ->
|
||||
$('.speeds').mouseenter().click()
|
||||
expect($('.speeds')).not.toHaveClass 'open'
|
||||
|
||||
describe 'changeVideoSpeed', ->
|
||||
beforeEach ->
|
||||
@speedControl = new VideoSpeedControl @player, @video.speeds
|
||||
@video.setSpeed '1.0'
|
||||
|
||||
describe 'when new speed is the same', ->
|
||||
beforeEach ->
|
||||
spyOnEvent @player, 'speedChange'
|
||||
$('li[data-speed="1.0"] a').click()
|
||||
|
||||
it 'does not trigger speedChange event', ->
|
||||
expect('speedChange').not.toHaveBeenTriggeredOn @player
|
||||
|
||||
describe 'when new speed is not the same', ->
|
||||
beforeEach ->
|
||||
@newSpeed = null
|
||||
$(@player).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed
|
||||
spyOnEvent @player, 'speedChange'
|
||||
$('li[data-speed="0.75"] a').click()
|
||||
|
||||
it 'trigger player speedChange event', ->
|
||||
expect('speedChange').toHaveBeenTriggeredOn @player
|
||||
expect(@newSpeed).toEqual 0.75
|
||||
|
||||
describe 'onSpeedChange', ->
|
||||
beforeEach ->
|
||||
@speedControl = new VideoSpeedControl @player, @video.speeds
|
||||
$('li[data-speed="1.0"] a').addClass 'active'
|
||||
@speedControl.setSpeed '0.75'
|
||||
|
||||
it 'set the new speed as active', ->
|
||||
expect($('.video_speeds li[data-speed="1.0"]')).not.toHaveClass 'active'
|
||||
expect($('.video_speeds li[data-speed="0.75"]')).toHaveClass 'active'
|
||||
expect($('.speeds p.active')).toHaveHtml '0.75x'
|
||||
130
static/coffee/spec/modules/video_spec.coffee
Normal file
130
static/coffee/spec/modules/video_spec.coffee
Normal file
@@ -0,0 +1,130 @@
|
||||
describe 'Video', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'video.html'
|
||||
jasmine.stubRequests()
|
||||
|
||||
afterEach ->
|
||||
window.player = undefined
|
||||
window.onYouTubePlayerAPIReady = undefined
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@stubVideoPlayer = jasmine.createSpy('VideoPlayer')
|
||||
$.cookie.andReturn '0.75'
|
||||
window.player = 100
|
||||
|
||||
describe 'by default', ->
|
||||
beforeEach ->
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
it 'reset the current video player', ->
|
||||
expect(window.player).toBeNull()
|
||||
|
||||
it 'set the elements', ->
|
||||
expect(@video.element).toBe '#video_example'
|
||||
|
||||
it 'parse the videos', ->
|
||||
expect(@video.videos).toEqual
|
||||
'0.75': 'abc123'
|
||||
'1.0': 'def456'
|
||||
|
||||
it 'fetch the video metadata', ->
|
||||
expect(@video.metadata).toEqual
|
||||
abc123:
|
||||
id: 'abc123'
|
||||
duration: 100
|
||||
def456:
|
||||
id: 'def456'
|
||||
duration: 200
|
||||
|
||||
it 'parse available video speeds', ->
|
||||
expect(@video.speeds).toEqual ['0.75', '1.0']
|
||||
|
||||
it 'set current video speed via cookie', ->
|
||||
expect(@video.speed).toEqual '0.75'
|
||||
|
||||
it 'store a reference for this video player in the element', ->
|
||||
expect($('.video').data('video')).toEqual @video
|
||||
|
||||
describe 'when the Youtube API is already available', ->
|
||||
beforeEach ->
|
||||
@originalYT = window.YT
|
||||
window.YT = { Player: true }
|
||||
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
afterEach ->
|
||||
window.YT = @originalYT
|
||||
|
||||
it 'create the Video Player', ->
|
||||
expect(window.VideoPlayer).toHaveBeenCalledWith @video
|
||||
expect(@video.player).toEqual @stubVideoPlayer
|
||||
|
||||
describe 'when the Youtube API is not ready', ->
|
||||
beforeEach ->
|
||||
@originalYT = window.YT
|
||||
window.YT = {}
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
afterEach ->
|
||||
window.YT = @originalYT
|
||||
|
||||
it 'set the callback on the window object', ->
|
||||
expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function)
|
||||
|
||||
describe 'when the Youtube API becoming ready', ->
|
||||
beforeEach ->
|
||||
@originalYT = window.YT
|
||||
window.YT = {}
|
||||
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
window.onYouTubePlayerAPIReady()
|
||||
|
||||
afterEach ->
|
||||
window.YT = @originalYT
|
||||
|
||||
it 'create the Video Player for all video elements', ->
|
||||
expect(window.VideoPlayer).toHaveBeenCalledWith @video
|
||||
expect(@video.player).toEqual @stubVideoPlayer
|
||||
|
||||
describe 'youtubeId', ->
|
||||
beforeEach ->
|
||||
$.cookie.andReturn '1.0'
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
describe 'with speed', ->
|
||||
it 'return the video id for given speed', ->
|
||||
expect(@video.youtubeId('0.75')).toEqual 'abc123'
|
||||
expect(@video.youtubeId('1.0')).toEqual 'def456'
|
||||
|
||||
describe 'without speed', ->
|
||||
it 'return the video id for current speed', ->
|
||||
expect(@video.youtubeId()).toEqual 'def456'
|
||||
|
||||
describe 'setSpeed', ->
|
||||
beforeEach ->
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
describe 'when new speed is available', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '0.75'
|
||||
|
||||
it 'set new speed', ->
|
||||
expect(@video.speed).toEqual '0.75'
|
||||
|
||||
it 'save setting for new speed', ->
|
||||
expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
|
||||
|
||||
describe 'when new speed is not available', ->
|
||||
beforeEach ->
|
||||
@video.setSpeed '1.75'
|
||||
|
||||
it 'set speed to 1.0x', ->
|
||||
expect(@video.speed).toEqual '1.0'
|
||||
|
||||
describe 'getDuration', ->
|
||||
beforeEach ->
|
||||
@video = new Video 'example', '.75:abc123,1.0:def456'
|
||||
|
||||
it 'return duration for current video', ->
|
||||
expect(@video.getDuration()).toEqual 200
|
||||
74
static/coffee/spec/navigation_spec.coffee
Normal file
74
static/coffee/spec/navigation_spec.coffee
Normal file
@@ -0,0 +1,74 @@
|
||||
describe 'Navigation', ->
|
||||
beforeEach ->
|
||||
loadFixtures 'accordion.html'
|
||||
@navigation = new Navigation
|
||||
|
||||
describe 'constructor', ->
|
||||
describe 'when the #accordion exists', ->
|
||||
describe 'when there is an active section', ->
|
||||
beforeEach ->
|
||||
spyOn $.fn, 'accordion'
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>')
|
||||
new Navigation
|
||||
|
||||
it 'activate the accordion with correct active section', ->
|
||||
expect($('#accordion').accordion).toHaveBeenCalledWith
|
||||
active: 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
|
||||
describe 'when there is no active section', ->
|
||||
beforeEach ->
|
||||
spyOn $.fn, 'accordion'
|
||||
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>')
|
||||
new Navigation
|
||||
|
||||
it 'activate the accordian with section 1 as active', ->
|
||||
expect($('#accordion').accordion).toHaveBeenCalledWith
|
||||
active: 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
|
||||
it 'binds the accordionchange event', ->
|
||||
Navigation.bind()
|
||||
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
|
||||
|
||||
it 'bind the navigation toggle', ->
|
||||
Navigation.bind()
|
||||
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
|
||||
|
||||
describe 'when the #accordion does not exists', ->
|
||||
beforeEach ->
|
||||
$('#accordion').remove()
|
||||
|
||||
it 'does not activate the accordion', ->
|
||||
spyOn $.fn, 'accordion'
|
||||
Navigation.bind()
|
||||
expect($('#accordion').accordion).wasNotCalled()
|
||||
|
||||
describe 'toggle', ->
|
||||
it 'toggle closed class on the wrapper', ->
|
||||
$('.course-wrapper').removeClass('closed')
|
||||
|
||||
@navigation.toggle()
|
||||
expect($('.course-wrapper')).toHaveClass('closed')
|
||||
|
||||
@navigation.toggle()
|
||||
expect($('.course-wrapper')).not.toHaveClass('closed')
|
||||
|
||||
describe 'log', ->
|
||||
beforeEach ->
|
||||
window.log_event = ->
|
||||
spyOn window, 'log_event'
|
||||
|
||||
it 'submit event log', ->
|
||||
@navigation.log {}, {
|
||||
newHeader:
|
||||
text: -> "new"
|
||||
oldHeader:
|
||||
text: -> "old"
|
||||
}
|
||||
|
||||
expect(window.log_event).toHaveBeenCalledWith 'accordion',
|
||||
newheader: 'new'
|
||||
oldheader: 'old'
|
||||
18
static/coffee/spec/time_spec.coffee
Normal file
18
static/coffee/spec/time_spec.coffee
Normal file
@@ -0,0 +1,18 @@
|
||||
describe 'Time', ->
|
||||
describe 'format', ->
|
||||
describe 'with duration more than or equal to 1 hour', ->
|
||||
it 'return a correct time format', ->
|
||||
expect(Time.format(3600)).toEqual '1:00:00'
|
||||
expect(Time.format(7272)).toEqual '2:01:12'
|
||||
|
||||
describe 'with duration less than 1 hour', ->
|
||||
it 'return a correct time format', ->
|
||||
expect(Time.format(1)).toEqual '0:01'
|
||||
expect(Time.format(61)).toEqual '1:01'
|
||||
expect(Time.format(3599)).toEqual '59:59'
|
||||
|
||||
describe 'convert', ->
|
||||
it 'return a correct time based on speed modifier', ->
|
||||
expect(Time.convert(0, 1, 1.5)).toEqual '0.000'
|
||||
expect(Time.convert(100, 1, 1.5)).toEqual '66.667'
|
||||
expect(Time.convert(100, 1.5, 1)).toEqual '150.000'
|
||||
@@ -1,10 +1,9 @@
|
||||
class window.Calculator
|
||||
@bind: ->
|
||||
calculator = new Calculator
|
||||
$('.calc').click calculator.toggle
|
||||
$('form#calculator').submit(calculator.calculate).submit (e) ->
|
||||
class @Calculator
|
||||
constructor: ->
|
||||
$('.calc').click @toggle
|
||||
$('form#calculator').submit(@calculate).submit (e) ->
|
||||
e.preventDefault()
|
||||
$('div.help-wrapper a').hover(calculator.helpToggle).click (e) ->
|
||||
$('div.help-wrapper a').hover(@helpToggle).click (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
toggle: ->
|
||||
@@ -21,5 +20,5 @@ class window.Calculator
|
||||
$('.help').toggleClass 'shown'
|
||||
|
||||
calculate: ->
|
||||
$.getJSON '/calculate', { equation: $('#calculator_input').val() }, (data) ->
|
||||
$.getWithPrefix '/calculate', { equation: $('#calculator_input').val() }, (data) ->
|
||||
$('#calculator_output').val(data.result)
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
class window.Courseware
|
||||
@bind: ->
|
||||
@Navigation.bind()
|
||||
class @Courseware
|
||||
@prefix: ''
|
||||
|
||||
class @Navigation
|
||||
@bind: ->
|
||||
if $('#accordion').length
|
||||
navigation = new Navigation
|
||||
active = $('#accordion ul:has(li.active)').index('#accordion ul')
|
||||
$('#accordion').bind('accordionchange', navigation.log).accordion
|
||||
active: if active >= 0 then active else 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
$('#open_close_accordion a').click navigation.toggle
|
||||
constructor: ->
|
||||
Courseware.prefix = $("meta[name='path_prefix']").attr('content')
|
||||
new Navigation
|
||||
new Calculator
|
||||
new FeedbackForm
|
||||
Logger.bind()
|
||||
@bind()
|
||||
@render()
|
||||
|
||||
log: (event, ui) ->
|
||||
log_event 'accordion',
|
||||
newheader: ui.newHeader.text()
|
||||
oldheader: ui.oldHeader.text()
|
||||
@start: ->
|
||||
new Courseware
|
||||
|
||||
toggle: ->
|
||||
$('.course-wrapper').toggleClass('closed')
|
||||
bind: ->
|
||||
$('.course-content .sequence, .course-content .tab')
|
||||
.bind 'contentChanged', @render
|
||||
|
||||
render: ->
|
||||
$('.course-content .video').each ->
|
||||
id = $(this).attr('id').replace(/video_/, '')
|
||||
new Video id, $(this).data('streams')
|
||||
$('.course-content .problems-wrapper').each ->
|
||||
id = $(this).attr('id').replace(/problem_/, '')
|
||||
new Problem id, $(this).data('url')
|
||||
$('.course-content .histogram').each ->
|
||||
id = $(this).attr('id').replace(/histogram_/, '')
|
||||
new Histogram id, $(this).data('histogram')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
class window.FeedbackForm
|
||||
@bind: ->
|
||||
class @FeedbackForm
|
||||
constructor: ->
|
||||
$('#feedback_button').click ->
|
||||
data =
|
||||
subject: $('#feedback_subject').val()
|
||||
message: $('#feedback_message').val()
|
||||
url: window.location.href
|
||||
$.post '/send_feedback', data, ->
|
||||
$.postWithPrefix '/send_feedback', data, ->
|
||||
$('#feedback_div').html 'Feedback submitted. Thank you'
|
||||
,'json'
|
||||
|
||||
35
static/coffee/src/histogram.coffee
Normal file
35
static/coffee/src/histogram.coffee
Normal file
@@ -0,0 +1,35 @@
|
||||
class @Histogram
|
||||
constructor: (@id, @rawData) ->
|
||||
@xTicks = []
|
||||
@yTicks = []
|
||||
@data = []
|
||||
@calculate()
|
||||
@render()
|
||||
|
||||
calculate: ->
|
||||
for [score, count] in @rawData
|
||||
log_count = Math.log(count + 1)
|
||||
@data.push [score, log_count]
|
||||
@xTicks.push [score, score.toString()]
|
||||
@yTicks.push [log_count, count.toString()]
|
||||
|
||||
render: ->
|
||||
$.plot $("#histogram_#{@id}"), [
|
||||
data: @data
|
||||
bars:
|
||||
show: true
|
||||
align: 'center'
|
||||
lineWidth: 0
|
||||
fill: 1.0
|
||||
color: "#b72121"
|
||||
],
|
||||
xaxis:
|
||||
min: -1
|
||||
max: Math.max.apply Math, $.map(@xTicks, (data) -> data[0] + 1)
|
||||
ticks: @xTicks
|
||||
tickLength: 0
|
||||
yaxis:
|
||||
min: 0.0
|
||||
max: Math.max.apply Math, $.map(@yTicks, (data) -> data[0] * 1.1)
|
||||
ticks: @yTicks
|
||||
labelWidth: 50
|
||||
19
static/coffee/src/logger.coffee
Normal file
19
static/coffee/src/logger.coffee
Normal file
@@ -0,0 +1,19 @@
|
||||
class @Logger
|
||||
@log: (event_type, data) ->
|
||||
$.getWithPrefix '/event',
|
||||
event_type: event_type
|
||||
event: JSON.stringify(data)
|
||||
page: window.location.href
|
||||
|
||||
@bind: ->
|
||||
window.onunload = ->
|
||||
$.ajax
|
||||
url: "#{Courseware.prefix}/event"
|
||||
data:
|
||||
event_type: 'page_close'
|
||||
event: ''
|
||||
page: window.location.href
|
||||
async: false
|
||||
|
||||
# Keeping this for conpatibility issue only.
|
||||
@log_event = Logger.log
|
||||
@@ -1,8 +1,33 @@
|
||||
jQuery.postWithPrefix = (url, data, callback, type) ->
|
||||
$.post("#{Courseware.prefix}#{url}", data, callback, type)
|
||||
|
||||
jQuery.getWithPrefix = (url, data, callback, type) ->
|
||||
$.get("#{Courseware.prefix}#{url}", data, callback, type)
|
||||
|
||||
$ ->
|
||||
$.ajaxSetup
|
||||
headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
|
||||
dataType: 'json'
|
||||
|
||||
window.onTouchBasedDevice = ->
|
||||
navigator.userAgent.match /iPhone|iPod|iPad/i
|
||||
|
||||
$('body').addClass 'touch-based-device' if onTouchBasedDevice()
|
||||
|
||||
Calculator.bind()
|
||||
Courseware.bind()
|
||||
FeedbackForm.bind()
|
||||
$("a[rel*=leanModal]").leanModal()
|
||||
$('#csrfmiddlewaretoken').attr 'value', $.cookie('csrftoken')
|
||||
|
||||
if $('body').hasClass('courseware')
|
||||
Courseware.start()
|
||||
|
||||
# Preserved for backward compatibility
|
||||
window.submit_circuit = (circuit_id) ->
|
||||
$("input.schematic").each (index, element) ->
|
||||
element.schematic.update_value()
|
||||
|
||||
schematic_value $("#schematic_#{circuit_id}").attr("value")
|
||||
$.postWithPrefix "/save_circuit/#{circuit_id}", schematic: schematic_value, (data) ->
|
||||
alert('Saved') if data.results == 'success'
|
||||
|
||||
window.postJSON = (url, data, callback) ->
|
||||
$.postWithPrefix url, data, callback
|
||||
|
||||
70
static/coffee/src/modules/problem.coffee
Normal file
70
static/coffee/src/modules/problem.coffee
Normal file
@@ -0,0 +1,70 @@
|
||||
class @Problem
|
||||
constructor: (@id, url) ->
|
||||
@element = $("#problem_#{id}")
|
||||
@content_url = "#{url}problem_get?id=#{@id}"
|
||||
@render()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @element)
|
||||
|
||||
bind: =>
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
|
||||
window.update_schematics()
|
||||
@$('section.action input:button').click @refreshAnswers
|
||||
@$('section.action input.check').click @check
|
||||
@$('section.action input.reset').click @reset
|
||||
@$('section.action input.show').click @show
|
||||
@$('section.action input.save').click @save
|
||||
|
||||
render: (content) ->
|
||||
if content
|
||||
@element.html(content)
|
||||
@bind()
|
||||
else
|
||||
@element.load @content_url, @bind
|
||||
|
||||
check: =>
|
||||
Logger.log 'problem_check', @answers
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_check", @answers, (response) =>
|
||||
switch response.success
|
||||
when 'incorrect', 'correct'
|
||||
@render(response.contents)
|
||||
else
|
||||
alert(response.success)
|
||||
|
||||
reset: =>
|
||||
Logger.log 'problem_reset', @answers
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (content) =>
|
||||
@render(content)
|
||||
|
||||
show: =>
|
||||
if !@element.hasClass 'showed'
|
||||
Logger.log 'problem_show', problem: @id
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) =>
|
||||
$.each response, (key, value) =>
|
||||
if $.isArray(value)
|
||||
for choice in value
|
||||
@$("label[for='input_#{key}_#{choice}']").attr
|
||||
correct_answer: 'true'
|
||||
else
|
||||
@$("#answer_#{key}").text(value)
|
||||
@$('.show').val 'Hide Answer'
|
||||
@element.addClass 'showed'
|
||||
else
|
||||
@$('[id^=answer_]').text ''
|
||||
@$('[correct_answer]').attr correct_answer: null
|
||||
@element.removeClass 'showed'
|
||||
@$('.show').val 'Show Answer'
|
||||
|
||||
save: =>
|
||||
Logger.log 'problem_save', @answers
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) =>
|
||||
if response.success
|
||||
alert 'Saved'
|
||||
|
||||
refreshAnswers: =>
|
||||
@$('input.schematic').each (index, element) ->
|
||||
element.schematic.update_value()
|
||||
@$(".CodeMirror").each (index, element) ->
|
||||
element.CodeMirror.save() if element.CodeMirror.save
|
||||
@answers = @$("[id^=input_#{@id}_]").serialize()
|
||||
73
static/coffee/src/modules/sequence.coffee
Normal file
73
static/coffee/src/modules/sequence.coffee
Normal file
@@ -0,0 +1,73 @@
|
||||
class @Sequence
|
||||
constructor: (@id, @elements, @tag, position) ->
|
||||
@element = $("#sequence_#{@id}")
|
||||
@buildNavigation()
|
||||
@bind()
|
||||
@render position
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @element)
|
||||
|
||||
bind: ->
|
||||
@element.bind 'contentChanged', @toggleArrows
|
||||
@$('#sequence-list a').click @goto
|
||||
|
||||
buildNavigation: ->
|
||||
$.each @elements, (index, item) =>
|
||||
link = $('<a>').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1
|
||||
title = $('<p>').html(item.title)
|
||||
list_item = $('<li>').append(link.append(title))
|
||||
@$('#sequence-list').append list_item
|
||||
|
||||
toggleArrows: =>
|
||||
@$('.sequence-nav-buttons a').unbind('click')
|
||||
|
||||
if @position == 1
|
||||
@$('.sequence-nav-buttons .prev a').addClass('disabled')
|
||||
else
|
||||
@$('.sequence-nav-buttons .prev a').removeClass('disabled').click(@previous)
|
||||
|
||||
if @position == @elements.length
|
||||
@$('.sequence-nav-buttons .next a').addClass('disabled')
|
||||
else
|
||||
@$('.sequence-nav-buttons .next a').removeClass('disabled').click(@next)
|
||||
|
||||
render: (new_position) ->
|
||||
if @position != new_position
|
||||
if @position != undefined
|
||||
@mark_visited @position
|
||||
$.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position
|
||||
|
||||
@mark_active new_position
|
||||
@$('#seq_content').html @elements[new_position - 1].content
|
||||
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub])
|
||||
@position = new_position
|
||||
@element.trigger 'contentChanged'
|
||||
|
||||
goto: (event) =>
|
||||
event.preventDefault()
|
||||
new_position = $(event.target).data('element')
|
||||
Logger.log "seq_goto", old: @position, new: new_position, id: @id
|
||||
@render new_position
|
||||
|
||||
next: (event) =>
|
||||
event.preventDefault()
|
||||
new_position = @position + 1
|
||||
Logger.log "seq_next", old: @position, new: new_position, id: @id
|
||||
@render new_position
|
||||
|
||||
previous: (event) =>
|
||||
event.preventDefault()
|
||||
new_position = @position - 1
|
||||
Logger.log "seq_prev", old: @position, new: new_position, id: @id
|
||||
@render new_position
|
||||
|
||||
link_for: (position) ->
|
||||
@$("#sequence-list a[data-element=#{position}]")
|
||||
|
||||
mark_visited: (position) ->
|
||||
@link_for(position).attr class: "seq_#{@elements[position - 1].type}_visited"
|
||||
|
||||
mark_active: (position) ->
|
||||
@link_for(position).attr class: "seq_#{@elements[position - 1].type}_active"
|
||||
23
static/coffee/src/modules/tab.coffee
Normal file
23
static/coffee/src/modules/tab.coffee
Normal file
@@ -0,0 +1,23 @@
|
||||
class @Tab
|
||||
constructor: (@id, @items) ->
|
||||
@element = $("#tab_#{id}")
|
||||
@render()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @element)
|
||||
|
||||
render: ->
|
||||
$.each @items, (index, item) =>
|
||||
tab = $('<a>').attr(href: "##{@tabId(index)}").html(item.title)
|
||||
@$('.navigation').append($('<li>').append(tab))
|
||||
@element.append($('<section>').attr(id: @tabId(index)))
|
||||
@element.tabs
|
||||
show: @onShow
|
||||
|
||||
onShow: (element, ui) =>
|
||||
@$('section.ui-tabs-hide').html('')
|
||||
@$("##{@tabId(ui.index)}").html(eval(@items[ui.index]['content']))
|
||||
@element.trigger 'contentChanged'
|
||||
|
||||
tabId: (index) ->
|
||||
"tab-#{@id}-#{index}"
|
||||
47
static/coffee/src/modules/video.coffee
Normal file
47
static/coffee/src/modules/video.coffee
Normal file
@@ -0,0 +1,47 @@
|
||||
class @Video
|
||||
constructor: (@id, videos) ->
|
||||
window.player = null
|
||||
@element = $("#video_#{@id}")
|
||||
@parseVideos videos
|
||||
@fetchMetadata()
|
||||
@parseSpeed()
|
||||
$("#video_#{@id}").data('video', this)
|
||||
|
||||
if YT.Player
|
||||
@embed()
|
||||
else
|
||||
window.onYouTubePlayerAPIReady = =>
|
||||
$('.course-content .video').each ->
|
||||
$(this).data('video').embed()
|
||||
|
||||
youtubeId: (speed)->
|
||||
@videos[speed || @speed]
|
||||
|
||||
parseVideos: (videos) ->
|
||||
@videos = {}
|
||||
$.each videos.split(/,/), (index, video) =>
|
||||
video = video.split(/:/)
|
||||
speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0'
|
||||
@videos[speed] = video[1]
|
||||
|
||||
parseSpeed: ->
|
||||
@setSpeed($.cookie('video_speed'))
|
||||
@speeds = ($.map @videos, (url, speed) -> speed).sort()
|
||||
|
||||
setSpeed: (newSpeed) ->
|
||||
if @videos[newSpeed] != undefined
|
||||
@speed = newSpeed
|
||||
$.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/')
|
||||
else
|
||||
@speed = '1.0'
|
||||
|
||||
embed: ->
|
||||
@player = new VideoPlayer(this)
|
||||
|
||||
fetchMetadata: (url) ->
|
||||
@metadata = {}
|
||||
$.each @videos, (speed, url) =>
|
||||
$.get "http://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
|
||||
|
||||
getDuration: ->
|
||||
@metadata[@youtubeId()].duration
|
||||
129
static/coffee/src/modules/video/video_caption.coffee
Normal file
129
static/coffee/src/modules/video/video_caption.coffee
Normal file
@@ -0,0 +1,129 @@
|
||||
class @VideoCaption
|
||||
constructor: (@player, @youtubeId) ->
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
@player.$(selector)
|
||||
|
||||
bind: ->
|
||||
$(window).bind('resize', @onWindowResize)
|
||||
$(@player).bind('resize', @onWindowResize)
|
||||
$(@player).bind('updatePlayTime', @onUpdatePlayTime)
|
||||
@$('.hide-subtitles').click @toggle
|
||||
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
|
||||
.mousemove(@onMovement).bind('mousewheel', @onMovement)
|
||||
.bind('DOMMouseScroll', @onMovement)
|
||||
|
||||
captionURL: ->
|
||||
"/static/subs/#{@youtubeId}.srt.sjson"
|
||||
|
||||
render: ->
|
||||
@$('.video-wrapper').after """
|
||||
<ol class="subtitles"><li>Attempting to load captions...</li></ol>
|
||||
"""
|
||||
@$('.video-controls .secondary-controls').append """
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
|
||||
"""
|
||||
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
|
||||
@fetchCaption()
|
||||
|
||||
fetchCaption: ->
|
||||
$.getWithPrefix @captionURL(), (captions) =>
|
||||
@captions = captions.text
|
||||
@start = captions.start
|
||||
@renderCaption()
|
||||
|
||||
renderCaption: ->
|
||||
container = $('<ol>')
|
||||
|
||||
$.each @captions, (index, text) =>
|
||||
container.append $('<li>').html(text).attr
|
||||
'data-index': index
|
||||
'data-start': @start[index]
|
||||
|
||||
@$('.subtitles').html(container.html())
|
||||
@$('.subtitles li[data-index]').click @seekPlayer
|
||||
|
||||
# prepend and append an empty <li> for cosmatic reason
|
||||
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
|
||||
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
|
||||
|
||||
search: (time) ->
|
||||
min = 0
|
||||
max = @start.length - 1
|
||||
|
||||
while min < max
|
||||
index = Math.ceil((max + min) / 2)
|
||||
if time < @start[index]
|
||||
max = index - 1
|
||||
if time >= @start[index]
|
||||
min = index
|
||||
|
||||
return min
|
||||
|
||||
onUpdatePlayTime: (event, time) =>
|
||||
# This 250ms offset is required to match the video speed
|
||||
time = Math.round(Time.convert(time, @player.currentSpeed(), '1.0') * 1000 + 250)
|
||||
newIndex = @search time
|
||||
|
||||
if newIndex != undefined && @currentIndex != newIndex
|
||||
if @currentIndex
|
||||
@$(".subtitles li.current").removeClass('current')
|
||||
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
|
||||
|
||||
@currentIndex = newIndex
|
||||
@scrollCaption()
|
||||
|
||||
onWindowResize: =>
|
||||
@$('.subtitles').css maxHeight: @captionHeight()
|
||||
@$('.subtitles .spacing:first').height(@topSpacingHeight())
|
||||
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
|
||||
@scrollCaption()
|
||||
|
||||
onMouseEnter: =>
|
||||
clearTimeout @frozen if @frozen
|
||||
@frozen = setTimeout @onMouseLeave, 10000
|
||||
|
||||
onMovement: =>
|
||||
@onMouseEnter()
|
||||
|
||||
onMouseLeave: =>
|
||||
clearTimeout @frozen if @frozen
|
||||
@frozen = null
|
||||
@scrollCaption() if @player.isPlaying()
|
||||
|
||||
scrollCaption: ->
|
||||
if !@frozen && @$('.subtitles .current:first').length
|
||||
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
|
||||
offset: - @calculateOffset(@$('.subtitles .current:first'))
|
||||
|
||||
seekPlayer: (event) =>
|
||||
event.preventDefault()
|
||||
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @player.currentSpeed()) / 1000)
|
||||
$(@player).trigger('seek', time)
|
||||
|
||||
calculateOffset: (element) ->
|
||||
@captionHeight() / 2 - element.height() / 2
|
||||
|
||||
topSpacingHeight: ->
|
||||
@calculateOffset(@$('.subtitles li:not(.spacing):first'))
|
||||
|
||||
bottomSpacingHeight: ->
|
||||
@calculateOffset(@$('.subtitles li:not(.spacing):last'))
|
||||
|
||||
toggle: (event) =>
|
||||
event.preventDefault()
|
||||
if @player.element.hasClass('closed')
|
||||
@$('.hide-subtitles').attr('title', 'Turn off captions')
|
||||
@player.element.removeClass('closed')
|
||||
@scrollCaption()
|
||||
else
|
||||
@$('.hide-subtitles').attr('title', 'Turn on captions')
|
||||
@player.element.addClass('closed')
|
||||
|
||||
captionHeight: ->
|
||||
if @player.element.hasClass('fullscreen')
|
||||
$(window).height() - @$('.video-controls').height()
|
||||
else
|
||||
@$('.video-wrapper').height()
|
||||
42
static/coffee/src/modules/video/video_control.coffee
Normal file
42
static/coffee/src/modules/video/video_control.coffee
Normal file
@@ -0,0 +1,42 @@
|
||||
class @VideoControl
|
||||
constructor: (@player) ->
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
@player.$(selector)
|
||||
|
||||
bind: ->
|
||||
$(@player).bind('play', @onPlay)
|
||||
.bind('pause', @onPause)
|
||||
.bind('ended', @onPause)
|
||||
@$('.video_control').click @togglePlayback
|
||||
|
||||
render: ->
|
||||
@$('.video-controls').append """
|
||||
<div class="slider"></div>
|
||||
<div>
|
||||
<ul class="vcr">
|
||||
<li><a class="video_control play">Play</a></li>
|
||||
<li>
|
||||
<div class="vidtime">0:00 / 0:00</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="secondary-controls">
|
||||
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
onPlay: =>
|
||||
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
|
||||
|
||||
onPause: =>
|
||||
@$('.video_control').removeClass('pause').addClass('play').html('Play')
|
||||
|
||||
togglePlayback: (event) =>
|
||||
event.preventDefault()
|
||||
if @player.isPlaying()
|
||||
$(@player).trigger('pause')
|
||||
else
|
||||
$(@player).trigger('play')
|
||||
134
static/coffee/src/modules/video/video_player.coffee
Normal file
134
static/coffee/src/modules/video/video_player.coffee
Normal file
@@ -0,0 +1,134 @@
|
||||
class @VideoPlayer
|
||||
constructor: (@video) ->
|
||||
# Define a missing constant of Youtube API
|
||||
YT.PlayerState.UNSTARTED = -1
|
||||
|
||||
@currentTime = 0
|
||||
@element = $("#video_#{@video.id}")
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @element)
|
||||
|
||||
bind: ->
|
||||
$(@).bind('seek', @onSeek)
|
||||
.bind('updatePlayTime', @onUpdatePlayTime)
|
||||
.bind('speedChange', @onSpeedChange)
|
||||
.bind('play', @onPlay)
|
||||
.bind('pause', @onPause)
|
||||
.bind('ended', @onPause)
|
||||
$(document).keyup @bindExitFullScreen
|
||||
|
||||
@$('.add-fullscreen').click @toggleFullScreen
|
||||
@addToolTip() unless onTouchBasedDevice()
|
||||
|
||||
bindExitFullScreen: (event) =>
|
||||
if @element.hasClass('fullscreen') && event.keyCode == 27
|
||||
@toggleFullScreen(event)
|
||||
|
||||
render: ->
|
||||
new VideoControl @
|
||||
new VideoCaption @, @video.youtubeId('1.0')
|
||||
new VideoSpeedControl @, @video.speeds
|
||||
new VideoProgressSlider @
|
||||
@player = new YT.Player @video.id,
|
||||
playerVars:
|
||||
controls: 0
|
||||
wmode: 'transparent'
|
||||
rel: 0
|
||||
showinfo: 0
|
||||
enablejsapi: 1
|
||||
videoId: @video.youtubeId()
|
||||
events:
|
||||
onReady: @onReady
|
||||
onStateChange: @onStateChange
|
||||
|
||||
addToolTip: ->
|
||||
@$('.add-fullscreen, .hide-subtitles').qtip
|
||||
position:
|
||||
my: 'top right'
|
||||
at: 'top center'
|
||||
|
||||
onReady: =>
|
||||
$(@).trigger('ready')
|
||||
$(@).trigger('updatePlayTime', 0)
|
||||
unless onTouchBasedDevice()
|
||||
$('.course-content .video:first').data('video').player.play()
|
||||
|
||||
onStateChange: (event) =>
|
||||
switch event.data
|
||||
when YT.PlayerState.PLAYING
|
||||
$(@).trigger('play')
|
||||
when YT.PlayerState.PAUSED, YT.PlayerState.UNSTARTED
|
||||
$(@).trigger('pause')
|
||||
when YT.PlayerState.ENDED
|
||||
$(@).trigger('ended')
|
||||
|
||||
onPlay: =>
|
||||
Logger.log 'play_video', id: @currentTime, code: @player.getVideoEmbedCode()
|
||||
window.player.pauseVideo() if window.player && window.player != @player
|
||||
window.player = @player
|
||||
unless @player.interval
|
||||
@player.interval = setInterval(@update, 200)
|
||||
|
||||
onPause: =>
|
||||
Logger.log 'pause_video', id: @currentTime, code: @player.getVideoEmbedCode()
|
||||
window.player = null if window.player == @player
|
||||
clearInterval(@player.interval)
|
||||
@player.interval = null
|
||||
|
||||
onSeek: (event, time) ->
|
||||
@player.seekTo(time, true)
|
||||
if @isPlaying()
|
||||
clearInterval(@player.interval)
|
||||
@player.interval = setInterval(@update, 200)
|
||||
else
|
||||
@currentTime = time
|
||||
$(@).trigger('updatePlayTime', time)
|
||||
|
||||
onSpeedChange: (event, newSpeed) =>
|
||||
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
|
||||
@video.setSpeed(parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0')
|
||||
|
||||
if @isPlaying()
|
||||
@player.loadVideoById(@video.youtubeId(), @currentTime)
|
||||
else
|
||||
@player.cueVideoById(@video.youtubeId(), @currentTime)
|
||||
$(@).trigger('updatePlayTime', @currentTime)
|
||||
|
||||
update: =>
|
||||
if @currentTime = @player.getCurrentTime()
|
||||
$(@).trigger('updatePlayTime', @currentTime)
|
||||
|
||||
onUpdatePlayTime: (event, time) =>
|
||||
progress = Time.format(time) + ' / ' + Time.format(@duration())
|
||||
@$(".vidtime").html(progress)
|
||||
|
||||
toggleFullScreen: (event) =>
|
||||
event.preventDefault()
|
||||
if @element.hasClass('fullscreen')
|
||||
@$('.exit').remove()
|
||||
@$('.add-fullscreen').attr('title', 'Fill browser')
|
||||
@element.removeClass('fullscreen')
|
||||
else
|
||||
@element.append('<a href="#" class="exit">Exit</a>').addClass('fullscreen')
|
||||
@$('.add-fullscreen').attr('title', 'Exit fill browser')
|
||||
@$('.exit').click @toggleFullScreen
|
||||
$(@).trigger('resize')
|
||||
|
||||
# Delegates
|
||||
play: ->
|
||||
@player.playVideo() if @player.playVideo
|
||||
|
||||
isPlaying: ->
|
||||
@player.getPlayerState() == YT.PlayerState.PLAYING
|
||||
|
||||
pause: ->
|
||||
@player.pauseVideo()
|
||||
|
||||
duration: ->
|
||||
@video.getDuration()
|
||||
|
||||
currentSpeed: ->
|
||||
@video.speed
|
||||
54
static/coffee/src/modules/video/video_progress_slider.coffee
Normal file
54
static/coffee/src/modules/video/video_progress_slider.coffee
Normal file
@@ -0,0 +1,54 @@
|
||||
class @VideoProgressSlider
|
||||
constructor: (@player) ->
|
||||
@buildSlider()
|
||||
@buildHandle()
|
||||
$(@player).bind('updatePlayTime', @onUpdatePlayTime)
|
||||
$(@player).bind('ready', @onReady)
|
||||
|
||||
$: (selector) ->
|
||||
@player.$(selector)
|
||||
|
||||
buildSlider: ->
|
||||
@slider = @$('.slider').slider
|
||||
range: 'min'
|
||||
change: @onChange
|
||||
slide: @onSlide
|
||||
stop: @onStop
|
||||
|
||||
buildHandle: ->
|
||||
@handle = @$('.ui-slider-handle')
|
||||
@handle.qtip
|
||||
content: "#{Time.format(@slider.slider('value'))}"
|
||||
position:
|
||||
my: 'bottom center'
|
||||
at: 'top center'
|
||||
container: @handle
|
||||
hide:
|
||||
delay: 700
|
||||
style:
|
||||
classes: 'ui-tooltip-slider'
|
||||
widget: true
|
||||
|
||||
onReady: =>
|
||||
@slider.slider('option', 'max', @player.duration())
|
||||
|
||||
onUpdatePlayTime: (event, currentTime) =>
|
||||
if !@frozen
|
||||
@slider.slider('option', 'max', @player.duration())
|
||||
@slider.slider('value', currentTime)
|
||||
|
||||
onSlide: (event, ui) =>
|
||||
@frozen = true
|
||||
@updateTooltip(ui.value)
|
||||
$(@player).trigger('seek', ui.value)
|
||||
|
||||
onChange: (event, ui) =>
|
||||
@updateTooltip(ui.value)
|
||||
|
||||
onStop: (event, ui) =>
|
||||
@frozen = true
|
||||
$(@player).trigger('seek', ui.value)
|
||||
setTimeout (=> @frozen = false), 200
|
||||
|
||||
updateTooltip: (value)->
|
||||
@handle.qtip('option', 'content.text', "#{Time.format(value)}")
|
||||
52
static/coffee/src/modules/video/video_speed_control.coffee
Normal file
52
static/coffee/src/modules/video/video_speed_control.coffee
Normal file
@@ -0,0 +1,52 @@
|
||||
class @VideoSpeedControl
|
||||
constructor: (@player, @speeds) ->
|
||||
@render()
|
||||
@bind()
|
||||
|
||||
$: (selector) ->
|
||||
@player.$(selector)
|
||||
|
||||
bind: ->
|
||||
$(@player).bind('speedChange', @onSpeedChange)
|
||||
@$('.video_speeds a').click @changeVideoSpeed
|
||||
if onTouchBasedDevice()
|
||||
@$('.speeds').click (event) ->
|
||||
event.preventDefault()
|
||||
$(this).toggleClass('open')
|
||||
else
|
||||
@$('.speeds').mouseenter ->
|
||||
$(this).addClass('open')
|
||||
@$('.speeds').mouseleave ->
|
||||
$(this).removeClass('open')
|
||||
@$('.speeds').click (event) ->
|
||||
event.preventDefault()
|
||||
$(this).removeClass('open')
|
||||
|
||||
render: ->
|
||||
@$('.secondary-controls').prepend """
|
||||
<div class="speeds">
|
||||
<a href="#">
|
||||
<h3>Speed</h3>
|
||||
<p class="active"></p>
|
||||
</a>
|
||||
<ol class="video_speeds"></ol>
|
||||
</div>
|
||||
"""
|
||||
|
||||
$.each @speeds, (index, speed) =>
|
||||
link = $('<a>').attr(href: "#").html("#{speed}x")
|
||||
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
|
||||
@setSpeed(@player.currentSpeed())
|
||||
|
||||
changeVideoSpeed: (event) =>
|
||||
event.preventDefault()
|
||||
unless $(event.target).parent().hasClass('active')
|
||||
$(@player).trigger 'speedChange', $(event.target).parent().data('speed')
|
||||
|
||||
onSpeedChange: (event, speed) =>
|
||||
@setSpeed(parseFloat(speed).toFixed(2).replace /\.00$/, '.0')
|
||||
|
||||
setSpeed: (speed) ->
|
||||
@$('.video_speeds li').removeClass('active')
|
||||
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
|
||||
@$('.speeds p.active').html("#{speed}x")
|
||||
19
static/coffee/src/navigation.coffee
Normal file
19
static/coffee/src/navigation.coffee
Normal file
@@ -0,0 +1,19 @@
|
||||
class @Navigation
|
||||
constructor: ->
|
||||
if $('#accordion').length
|
||||
active = $('#accordion ul:has(li.active)').index('#accordion ul')
|
||||
$('#accordion').bind('accordionchange', @log).accordion
|
||||
active: if active >= 0 then active else 1
|
||||
header: 'h3'
|
||||
autoHeight: false
|
||||
$('#open_close_accordion a').click @toggle
|
||||
|
||||
$('#accordion').show()
|
||||
|
||||
log: (event, ui) ->
|
||||
log_event 'accordion',
|
||||
newheader: ui.newHeader.text()
|
||||
oldheader: ui.oldHeader.text()
|
||||
|
||||
toggle: ->
|
||||
$('.course-wrapper').toggleClass('closed')
|
||||
17
static/coffee/src/time.coffee
Normal file
17
static/coffee/src/time.coffee
Normal file
@@ -0,0 +1,17 @@
|
||||
class @Time
|
||||
@format: (time) ->
|
||||
pad = (number) -> if number < 10 then "0#{number}" else number
|
||||
|
||||
seconds = Math.floor time
|
||||
minutes = Math.floor seconds / 60
|
||||
hours = Math.floor minutes / 60
|
||||
seconds = seconds % 60
|
||||
minutes = minutes % 60
|
||||
|
||||
if hours
|
||||
"#{hours}:#{pad(minutes)}:#{pad(seconds % 60)}"
|
||||
else
|
||||
"#{minutes}:#{pad(seconds % 60)}"
|
||||
|
||||
@convert: (time, oldSpeed, newSpeed) ->
|
||||
(time * oldSpeed / newSpeed).toFixed(3)
|
||||
341
static/js/CodeMirror/python.js
Normal file
341
static/js/CodeMirror/python.js
Normal file
@@ -0,0 +1,341 @@
|
||||
CodeMirror.defineMode("python", function(conf, parserConf) {
|
||||
var ERRORCLASS = 'error';
|
||||
|
||||
function wordRegexp(words) {
|
||||
return new RegExp("^((" + words.join(")|(") + "))\\b");
|
||||
}
|
||||
|
||||
var singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!]");
|
||||
var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]');
|
||||
var doubleOperators = new RegExp("^((==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))");
|
||||
var doubleDelimiters = new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))");
|
||||
var tripleDelimiters = new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))");
|
||||
var identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
|
||||
|
||||
var wordOperators = wordRegexp(['and', 'or', 'not', 'is', 'in']);
|
||||
var commonkeywords = ['as', 'assert', 'break', 'class', 'continue',
|
||||
'def', 'del', 'elif', 'else', 'except', 'finally',
|
||||
'for', 'from', 'global', 'if', 'import',
|
||||
'lambda', 'pass', 'raise', 'return',
|
||||
'try', 'while', 'with', 'yield'];
|
||||
var commonBuiltins = ['abs', 'all', 'any', 'bin', 'bool', 'bytearray', 'callable', 'chr',
|
||||
'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
|
||||
'enumerate', 'eval', 'filter', 'float', 'format', 'frozenset',
|
||||
'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id',
|
||||
'input', 'int', 'isinstance', 'issubclass', 'iter', 'len',
|
||||
'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next',
|
||||
'object', 'oct', 'open', 'ord', 'pow', 'property', 'range',
|
||||
'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
|
||||
'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple',
|
||||
'type', 'vars', 'zip', '__import__', 'NotImplemented',
|
||||
'Ellipsis', '__debug__'];
|
||||
var py2 = {'builtins': ['apply', 'basestring', 'buffer', 'cmp', 'coerce', 'execfile',
|
||||
'file', 'intern', 'long', 'raw_input', 'reduce', 'reload',
|
||||
'unichr', 'unicode', 'xrange', 'False', 'True', 'None'],
|
||||
'keywords': ['exec', 'print']};
|
||||
var py3 = {'builtins': ['ascii', 'bytes', 'exec', 'print'],
|
||||
'keywords': ['nonlocal', 'False', 'True', 'None']};
|
||||
|
||||
if (!!parserConf.version && parseInt(parserConf.version, 10) === 3) {
|
||||
commonkeywords = commonkeywords.concat(py3.keywords);
|
||||
commonBuiltins = commonBuiltins.concat(py3.builtins);
|
||||
var stringPrefixes = new RegExp("^(([rb]|(br))?('{3}|\"{3}|['\"]))", "i");
|
||||
} else {
|
||||
commonkeywords = commonkeywords.concat(py2.keywords);
|
||||
commonBuiltins = commonBuiltins.concat(py2.builtins);
|
||||
var stringPrefixes = new RegExp("^(([rub]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i");
|
||||
}
|
||||
var keywords = wordRegexp(commonkeywords);
|
||||
var builtins = wordRegexp(commonBuiltins);
|
||||
|
||||
var indentInfo = null;
|
||||
|
||||
// tokenizers
|
||||
function tokenBase(stream, state) {
|
||||
// Handle scope changes
|
||||
if (stream.sol()) {
|
||||
var scopeOffset = state.scopes[0].offset;
|
||||
if (stream.eatSpace()) {
|
||||
var lineOffset = stream.indentation();
|
||||
if (lineOffset > scopeOffset) {
|
||||
indentInfo = 'indent';
|
||||
} else if (lineOffset < scopeOffset) {
|
||||
indentInfo = 'dedent';
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
if (scopeOffset > 0) {
|
||||
dedent(stream, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stream.eatSpace()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var ch = stream.peek();
|
||||
|
||||
// Handle Comments
|
||||
if (ch === '#') {
|
||||
stream.skipToEnd();
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
// Handle Number Literals
|
||||
if (stream.match(/^[0-9\.]/, false)) {
|
||||
var floatLiteral = false;
|
||||
// Floats
|
||||
if (stream.match(/^\d*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; }
|
||||
if (stream.match(/^\d+\.\d*/)) { floatLiteral = true; }
|
||||
if (stream.match(/^\.\d+/)) { floatLiteral = true; }
|
||||
if (floatLiteral) {
|
||||
// Float literals may be "imaginary"
|
||||
stream.eat(/J/i);
|
||||
return 'number';
|
||||
}
|
||||
// Integers
|
||||
var intLiteral = false;
|
||||
// Hex
|
||||
if (stream.match(/^0x[0-9a-f]+/i)) { intLiteral = true; }
|
||||
// Binary
|
||||
if (stream.match(/^0b[01]+/i)) { intLiteral = true; }
|
||||
// Octal
|
||||
if (stream.match(/^0o[0-7]+/i)) { intLiteral = true; }
|
||||
// Decimal
|
||||
if (stream.match(/^[1-9]\d*(e[\+\-]?\d+)?/)) {
|
||||
// Decimal literals may be "imaginary"
|
||||
stream.eat(/J/i);
|
||||
// TODO - Can you have imaginary longs?
|
||||
intLiteral = true;
|
||||
}
|
||||
// Zero by itself with no other piece of number.
|
||||
if (stream.match(/^0(?![\dx])/i)) { intLiteral = true; }
|
||||
if (intLiteral) {
|
||||
// Integer literals may be "long"
|
||||
stream.eat(/L/i);
|
||||
return 'number';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Strings
|
||||
if (stream.match(stringPrefixes)) {
|
||||
state.tokenize = tokenStringFactory(stream.current());
|
||||
return state.tokenize(stream, state);
|
||||
}
|
||||
|
||||
// Handle operators and Delimiters
|
||||
if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) {
|
||||
return null;
|
||||
}
|
||||
if (stream.match(doubleOperators)
|
||||
|| stream.match(singleOperators)
|
||||
|| stream.match(wordOperators)) {
|
||||
return 'operator';
|
||||
}
|
||||
if (stream.match(singleDelimiters)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stream.match(keywords)) {
|
||||
return 'keyword';
|
||||
}
|
||||
|
||||
if (stream.match(builtins)) {
|
||||
return 'builtin';
|
||||
}
|
||||
|
||||
if (stream.match(identifiers)) {
|
||||
return 'variable';
|
||||
}
|
||||
|
||||
// Handle non-detected items
|
||||
stream.next();
|
||||
return ERRORCLASS;
|
||||
}
|
||||
|
||||
function tokenStringFactory(delimiter) {
|
||||
while ('rub'.indexOf(delimiter.charAt(0).toLowerCase()) >= 0) {
|
||||
delimiter = delimiter.substr(1);
|
||||
}
|
||||
var singleline = delimiter.length == 1;
|
||||
var OUTCLASS = 'string';
|
||||
|
||||
return function tokenString(stream, state) {
|
||||
while (!stream.eol()) {
|
||||
stream.eatWhile(/[^'"\\]/);
|
||||
if (stream.eat('\\')) {
|
||||
stream.next();
|
||||
if (singleline && stream.eol()) {
|
||||
return OUTCLASS;
|
||||
}
|
||||
} else if (stream.match(delimiter)) {
|
||||
state.tokenize = tokenBase;
|
||||
return OUTCLASS;
|
||||
} else {
|
||||
stream.eat(/['"]/);
|
||||
}
|
||||
}
|
||||
if (singleline) {
|
||||
if (parserConf.singleLineStringErrors) {
|
||||
return ERRORCLASS;
|
||||
} else {
|
||||
state.tokenize = tokenBase;
|
||||
}
|
||||
}
|
||||
return OUTCLASS;
|
||||
};
|
||||
}
|
||||
|
||||
function indent(stream, state, type) {
|
||||
type = type || 'py';
|
||||
var indentUnit = 0;
|
||||
if (type === 'py') {
|
||||
if (state.scopes[0].type !== 'py') {
|
||||
state.scopes[0].offset = stream.indentation();
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < state.scopes.length; ++i) {
|
||||
if (state.scopes[i].type === 'py') {
|
||||
indentUnit = state.scopes[i].offset + conf.indentUnit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
indentUnit = stream.column() + stream.current().length;
|
||||
}
|
||||
state.scopes.unshift({
|
||||
offset: indentUnit,
|
||||
type: type
|
||||
});
|
||||
}
|
||||
|
||||
function dedent(stream, state, type) {
|
||||
type = type || 'py';
|
||||
if (state.scopes.length == 1) return;
|
||||
if (state.scopes[0].type === 'py') {
|
||||
var _indent = stream.indentation();
|
||||
var _indent_index = -1;
|
||||
for (var i = 0; i < state.scopes.length; ++i) {
|
||||
if (_indent === state.scopes[i].offset) {
|
||||
_indent_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_indent_index === -1) {
|
||||
return true;
|
||||
}
|
||||
while (state.scopes[0].offset !== _indent) {
|
||||
state.scopes.shift();
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
if (type === 'py') {
|
||||
state.scopes[0].offset = stream.indentation();
|
||||
return false;
|
||||
} else {
|
||||
if (state.scopes[0].type != type) {
|
||||
return true;
|
||||
}
|
||||
state.scopes.shift();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tokenLexer(stream, state) {
|
||||
indentInfo = null;
|
||||
var style = state.tokenize(stream, state);
|
||||
var current = stream.current();
|
||||
|
||||
// Handle '.' connected identifiers
|
||||
if (current === '.') {
|
||||
style = state.tokenize(stream, state);
|
||||
current = stream.current();
|
||||
if (style === 'variable' || style === 'builtin') {
|
||||
return 'variable';
|
||||
} else {
|
||||
return ERRORCLASS;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle decorators
|
||||
if (current === '@') {
|
||||
style = state.tokenize(stream, state);
|
||||
current = stream.current();
|
||||
if (style === 'variable'
|
||||
|| current === '@staticmethod'
|
||||
|| current === '@classmethod') {
|
||||
return 'meta';
|
||||
} else {
|
||||
return ERRORCLASS;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle scope changes.
|
||||
if (current === 'pass' || current === 'return') {
|
||||
state.dedent += 1;
|
||||
}
|
||||
if (current === 'lambda') state.lambda = true;
|
||||
if ((current === ':' && !state.lambda && state.scopes[0].type == 'py')
|
||||
|| indentInfo === 'indent') {
|
||||
indent(stream, state);
|
||||
}
|
||||
var delimiter_index = '[({'.indexOf(current);
|
||||
if (delimiter_index !== -1) {
|
||||
indent(stream, state, '])}'.slice(delimiter_index, delimiter_index+1));
|
||||
}
|
||||
if (indentInfo === 'dedent') {
|
||||
if (dedent(stream, state)) {
|
||||
return ERRORCLASS;
|
||||
}
|
||||
}
|
||||
delimiter_index = '])}'.indexOf(current);
|
||||
if (delimiter_index !== -1) {
|
||||
if (dedent(stream, state, current)) {
|
||||
return ERRORCLASS;
|
||||
}
|
||||
}
|
||||
if (state.dedent > 0 && stream.eol() && state.scopes[0].type == 'py') {
|
||||
if (state.scopes.length > 1) state.scopes.shift();
|
||||
state.dedent -= 1;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
var external = {
|
||||
startState: function(basecolumn) {
|
||||
return {
|
||||
tokenize: tokenBase,
|
||||
scopes: [{offset:basecolumn || 0, type:'py'}],
|
||||
lastToken: null,
|
||||
lambda: false,
|
||||
dedent: 0
|
||||
};
|
||||
},
|
||||
|
||||
token: function(stream, state) {
|
||||
var style = tokenLexer(stream, state);
|
||||
|
||||
state.lastToken = {style:style, content: stream.current()};
|
||||
|
||||
if (stream.eol() && stream.lambda) {
|
||||
state.lambda = false;
|
||||
}
|
||||
|
||||
return style;
|
||||
},
|
||||
|
||||
indent: function(state, textAfter) {
|
||||
if (state.tokenize != tokenBase) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return state.scopes[0].offset;
|
||||
}
|
||||
|
||||
};
|
||||
return external;
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/x-python", "python");
|
||||
315
static/js/jasmine-jquery.js
Normal file
315
static/js/jasmine-jquery.js
Normal file
@@ -0,0 +1,315 @@
|
||||
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 setFixtures = function(html) {
|
||||
jasmine.getFixtures().set(html);
|
||||
};
|
||||
|
||||
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.preload = function() {
|
||||
this.read.apply(this, arguments);
|
||||
};
|
||||
|
||||
jasmine.Fixtures.prototype.load = function() {
|
||||
this.cleanUp();
|
||||
this.createContainer_(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.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 sample = $(element).get()[0]
|
||||
if (sample == undefined || sample.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);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
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');
|
||||
},
|
||||
|
||||
// tests the existence of a specific event binding
|
||||
toHandle: function(eventName) {
|
||||
var events = this.actual.data("events");
|
||||
return events && events[eventName].length > 0;
|
||||
},
|
||||
|
||||
// tests the existence of a specific event binding + handler
|
||||
toHandleWith: function(eventName, eventHandler) {
|
||||
var stack = this.actual.data("events")[eventName];
|
||||
var i;
|
||||
for (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)
|
||||
if (this.actual.get && !$.isWindow(this.actual.get()[0]))
|
||||
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();
|
||||
});
|
||||
11
static/js/jquery.scrollTo-1.4.2-min.js
vendored
Normal file
11
static/js/jquery.scrollTo-1.4.2-min.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* jQuery.ScrollTo - Easy element scrolling using jQuery.
|
||||
* Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
|
||||
* Dual licensed under MIT and GPL.
|
||||
* Date: 5/25/2009
|
||||
* @author Ariel Flesler
|
||||
* @version 1.4.2
|
||||
*
|
||||
* http://flesler.blogspot.com/2007/10/jqueryscrollto.html
|
||||
*/
|
||||
;(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery);
|
||||
@@ -1,499 +0,0 @@
|
||||
// Things to abstract out to another file
|
||||
|
||||
// We do sync AJAX for just the page close event.
|
||||
// TODO: This should _really_ not be a global.
|
||||
var log_close_event = false;
|
||||
|
||||
function log_close() {
|
||||
var d=new Date();
|
||||
var t=d.getTime();
|
||||
//close_event_logged = "waiting";
|
||||
log_close_event = true;
|
||||
log_event('page_close', {});
|
||||
log_close_event = false;
|
||||
// Google Chrome will close without letting the event go through.
|
||||
// This causes the page close to be delayed until we've hit the
|
||||
// server. The code below fixes it, but breaks Firefox.
|
||||
// TODO: Check what happens with no network.
|
||||
/*while((close_event_logged != "done") && (d.getTime() < t+500)) {
|
||||
console.log(close_event_logged);
|
||||
}*/
|
||||
}
|
||||
|
||||
window.onbeforeunload = log_close;
|
||||
|
||||
function getCookie(name) {
|
||||
var cookieValue = null;
|
||||
if (document.cookie && document.cookie != '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
function postJSON(url, data, callback) {
|
||||
$.ajax({type:'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: callback,
|
||||
headers : {'X-CSRFToken':getCookie('csrftoken')}
|
||||
});
|
||||
}
|
||||
|
||||
function postJSONAsync(url, data, callback) {
|
||||
$.ajax({type:'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: callback,
|
||||
headers : {'X-CSRFToken':getCookie('csrftoken')},
|
||||
async:true
|
||||
});
|
||||
}
|
||||
|
||||
// For easy embedding of CSRF in forms
|
||||
$(function() {
|
||||
$('#csrfmiddlewaretoken').attr("value", getCookie('csrftoken'))
|
||||
});
|
||||
|
||||
// For working with circuits in wiki:
|
||||
|
||||
function submit_circuit(circuit_id) {
|
||||
$("input.schematic").each(function(index,element){ element.schematic.update_value(); });
|
||||
postJSON('/save_circuit/'+circuit_id,
|
||||
{'schematic': $('#schematic_'+circuit_id).attr("value")},
|
||||
function(data){ if (data.results=='success') alert("Saved");});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Video player
|
||||
|
||||
var load_id = 0;
|
||||
var caption_id;
|
||||
var video_speed = "1.0";
|
||||
|
||||
var updateytPlayerInterval;
|
||||
var ajax_videoInterval;
|
||||
|
||||
function change_video_speed(speed, youtube_id) {
|
||||
new_position = ytplayer.getCurrentTime() * video_speed / speed;
|
||||
video_speed = speed;
|
||||
ytplayer.loadVideoById(youtube_id, new_position);
|
||||
syncPlayButton();
|
||||
log_event("speed", {"new_speed":speed, "clip":youtube_id});
|
||||
|
||||
$.cookie("video_speed", speed, {'expires':3650, 'path':'/'});
|
||||
}
|
||||
|
||||
function caption_at(index) {
|
||||
if (captions==0)
|
||||
return "";
|
||||
|
||||
text_array=captions.text
|
||||
|
||||
if ((index>=text_array.length) || (index < 0))
|
||||
return "";
|
||||
return text_array[index];
|
||||
}
|
||||
|
||||
function caption_time_at(index) {
|
||||
if (captions==0)
|
||||
return 0;
|
||||
|
||||
time_array=captions.start;
|
||||
|
||||
if (index < 0)
|
||||
return 0;
|
||||
if (index>=time_array.length)
|
||||
return ytplayer.getDuration();
|
||||
|
||||
return time_array[index] / 1000.0 / video_speed;
|
||||
}
|
||||
|
||||
function caption_index(now) {
|
||||
// Returns the index of the current caption, given a time
|
||||
now = now * video_speed;
|
||||
|
||||
if (captions==0)
|
||||
return 0;
|
||||
|
||||
time_array=captions.start
|
||||
|
||||
// TODO: Bisection would be better, or something incremental
|
||||
var i;
|
||||
for(i=0;i<captions.start.length; i++) {
|
||||
if(time_array[i]>(now*1000)) {
|
||||
return i-1;
|
||||
}
|
||||
}
|
||||
return i-1;
|
||||
}
|
||||
|
||||
function format_time(t)
|
||||
{
|
||||
seconds = Math.floor(t);
|
||||
minutes = Math.floor(seconds / 60);
|
||||
hours = Math.floor(minutes / 60);
|
||||
seconds = seconds % 60;
|
||||
minutes = minutes % 60;
|
||||
|
||||
if (hours) {
|
||||
return hours+":"+((minutes < 10)?"0":"")+minutes+":"+((seconds < 10)?"0":"")+(seconds%60);
|
||||
} else {
|
||||
return minutes+":"+((seconds < 10)?"0":"")+(seconds%60);
|
||||
}
|
||||
}
|
||||
|
||||
function update_captions(t) {
|
||||
var i=caption_index(t);
|
||||
$("#vidtime").html(format_time(ytplayer.getCurrentTime())+' / '+format_time(ytplayer.getDuration()));
|
||||
var j;
|
||||
for(j=1; j<9; j++) {
|
||||
$("#std_n"+j).html(caption_at(i-j));
|
||||
$("#std_p"+j).html(caption_at(i+j));
|
||||
}
|
||||
$("#std_0").html(caption_at(i));
|
||||
}
|
||||
|
||||
function title_seek(i) {
|
||||
// Seek video forwards or backwards by i subtitles
|
||||
current=caption_index(getCurrentTime());
|
||||
new_time=caption_time_at(current+i);
|
||||
|
||||
ytplayer.seekTo(new_time, true);
|
||||
}
|
||||
|
||||
function updateHTML(elmId, value) {
|
||||
document.getElementById(elmId).innerHTML = value;
|
||||
}
|
||||
|
||||
function setytplayerState(newState) {
|
||||
// updateHTML("playerstate", newState);
|
||||
}
|
||||
|
||||
// Updates server with location in video so we can resume from the same place
|
||||
// IMPORTANT TODO: Load test
|
||||
// POSSIBLE FIX: Move to unload() event and similar
|
||||
var ajax_video=function(){};
|
||||
var ytplayer;
|
||||
|
||||
function onYouTubePlayerReady(playerId) {
|
||||
ytplayer = document.getElementById("myytplayer");
|
||||
updateytplayerInfoInterval = setInterval(updateytplayerInfo, 500);
|
||||
ajax_videoInterval = setInterval(ajax_video,5000);
|
||||
ytplayer.addEventListener("onStateChange", "onytplayerStateChange");
|
||||
ytplayer.addEventListener("onError", "onPlayerError");
|
||||
if((typeof load_id != "undefined") && (load_id != 0)) {
|
||||
var id=load_id;
|
||||
loadNewVideo(caption_id, id, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* HTML5 YouTube iFrame API Specific */
|
||||
function onYouTubePlayerAPIReady() {
|
||||
ytplayer = new YT.Player('html5_player', {
|
||||
events: {
|
||||
'onReady': onPlayerReady,
|
||||
'onStateChange': onPlayerStateChange
|
||||
}
|
||||
});
|
||||
updateytplayerInfoInterval = setInterval(updateHTML5ytplayerInfo, 200);
|
||||
//ajax_videoInterval = setInterval(ajax_video, 5000);
|
||||
}
|
||||
|
||||
// Need this function to call the API ready callback when we switch to a tab with AJAX that has a video
|
||||
// That callback is not being fired when we switch tabs.
|
||||
function loadHTML5Video() {
|
||||
if (!ytplayer && switched_tab){
|
||||
onYouTubePlayerAPIReady();
|
||||
}
|
||||
}
|
||||
|
||||
function isiOSDevice(){
|
||||
var iphone = "iphone";
|
||||
var ipod = "ipod";
|
||||
var ipad = "ipad";
|
||||
var uagent = navigator.userAgent.toLowerCase();
|
||||
|
||||
//alert(uagent);
|
||||
if (uagent.search(ipad) > -1 || uagent.search(iphone) > -1
|
||||
|| uagent.search(ipod) > -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function onPlayerReady(event) {
|
||||
//do not want to autoplay on iOS devices since its not enabled
|
||||
//and leads to confusing behavior for the user
|
||||
if (!isiOSDevice()) {
|
||||
event.target.playVideo();
|
||||
}
|
||||
}
|
||||
|
||||
function onPlayerStateChange(event) {
|
||||
if (event.data == YT.PlayerState.PLAYING) {
|
||||
}
|
||||
}
|
||||
|
||||
/* End HTML5 Specific */
|
||||
|
||||
|
||||
var switched_tab = false; // switch to true when we destroy so we know to call onYouTubePlayerAPIReady()
|
||||
// clear pings to video status when we switch to a different sequence tab with ajax
|
||||
function videoDestroy(id) {
|
||||
// postJSON('/modx/video/'+id+'/goto_position',
|
||||
// {'position' : ytplayer.getCurrentTime()});
|
||||
|
||||
load_id = 0;
|
||||
clearInterval(updateytplayerInfoInterval);
|
||||
clearInterval(ajax_videoInterval);
|
||||
ytplayer = false;
|
||||
switched_tab = true;
|
||||
}
|
||||
|
||||
function log_event(e, d) {
|
||||
data = {
|
||||
"event_type" : e,
|
||||
"event" : JSON.stringify(d),
|
||||
"page" : document.URL
|
||||
}
|
||||
$.ajax({type:'GET',
|
||||
url: '/event',
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
async: !log_close_event, // HACK: See comment on log_close_event
|
||||
success: function(){},
|
||||
headers : {'X-CSRFToken':getCookie('csrftoken')}
|
||||
});
|
||||
|
||||
/*, // Commenting out Chrome bug fix, since it breaks FF
|
||||
function(data) {
|
||||
console.log("closing");
|
||||
if (close_event_logged == "waiting") {
|
||||
close_event_logged = "done";
|
||||
console.log("closed");
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
function seek_slide(type,oe,value) {
|
||||
//log_event('video', [type, value]);
|
||||
if(type=='slide') {
|
||||
// HACK/TODO: Youtube recommends this be false for slide and true for stop.
|
||||
// Works better on my system with true/true.
|
||||
// We should test both configurations on low/high bandwidth
|
||||
// connections, and different browsers
|
||||
// One issue is that we query the Youtube window every 250ms for position/state
|
||||
// With false, it returns the old one (ignoring the new seek), and moves the
|
||||
// scroll bar to the wrong spot.
|
||||
ytplayer.seekTo(value, true);
|
||||
} else if (type=='stop') {
|
||||
ytplayer.seekTo(value, true);
|
||||
log_event('video', [type, value]);
|
||||
}
|
||||
|
||||
update_captions(value);
|
||||
}
|
||||
|
||||
function get_state() {
|
||||
if (ytplayer)
|
||||
return [ytplayer.getPlayerState(),
|
||||
ytplayer.getVideoUrl(),
|
||||
ytplayer.getDuration(), ytplayer.getCurrentTime(),
|
||||
ytplayer.getVideoBytesLoaded(), ytplayer.getVideoBytesTotal(),
|
||||
ytplayer.getVideoStartBytes(),
|
||||
ytplayer.getVolume(),ytplayer.isMuted(),
|
||||
ytplayer.getPlaybackQuality(),
|
||||
ytplayer.getAvailableQualityLevels()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function onytplayerStateChange(newState) {
|
||||
setytplayerState(newState);
|
||||
log_event('video', ['State Change',newState, get_state()]);
|
||||
}
|
||||
|
||||
function onPlayerError(errorCode) {
|
||||
// alert("An error occured: " + errorCode);
|
||||
log_event("player_error", {"error":errorCode});
|
||||
}
|
||||
|
||||
// Currently duplicated to check for if video control changed by clicking the video for HTML5
|
||||
// Hacky b/c of lack of control over YT player
|
||||
function updateHTML5ytplayerInfo() {
|
||||
var player_state = getPlayerState();
|
||||
if(player_state != 3) {
|
||||
$("#slider").slider("option","max",ytplayer.getDuration());
|
||||
$("#slider").slider("option","value",ytplayer.getCurrentTime());
|
||||
}
|
||||
if (player_state == 1){
|
||||
update_captions(getCurrentTime());
|
||||
}
|
||||
if (player_state == 1 && $("#video_control").hasClass("play"))
|
||||
$("#video_control").removeClass().addClass("pause");
|
||||
else if (player_state == 2 && $("#video_control").hasClass("pause"))
|
||||
$("#video_control").removeClass().addClass("play");
|
||||
}
|
||||
|
||||
function updateytplayerInfo() {
|
||||
var player_state = getPlayerState();
|
||||
if(player_state != 3) {
|
||||
$("#slider").slider("option","max",ytplayer.getDuration());
|
||||
$("#slider").slider("option","value",ytplayer.getCurrentTime());
|
||||
}
|
||||
if (player_state == 1){
|
||||
update_captions(getCurrentTime());
|
||||
handle = $('.ui-slider-handle', $('#slider'));
|
||||
handle.qtip('option', 'content.text', '' + format_time(getCurrentTime()));
|
||||
}
|
||||
// updateHTML("videoduration", getDuration());
|
||||
// updateHTML("videotime", getCurrentTime());
|
||||
// updateHTML("startbytes", getStartBytes());
|
||||
// updateHTML("volume", getVolume());
|
||||
}
|
||||
|
||||
// functions for the api calls
|
||||
function loadNewVideo(cap_id, id, startSeconds) {
|
||||
captions={"start":[0],"end":[0],"text":["Attempting to load captions..."]};
|
||||
$.getJSON("/static/subs/"+cap_id+".srt.sjson", function(data) {
|
||||
captions=data;
|
||||
});
|
||||
caption_id = cap_id;
|
||||
load_id = id;
|
||||
//if ((typeof ytplayer != "undefined") && (ytplayer.type=="application/x-shockwave-flash")) {
|
||||
// Try it every time. If we fail, we want the error message for now.
|
||||
// TODO: Add try/catch
|
||||
try {
|
||||
ytplayer.loadVideoById(id, parseInt(startSeconds));
|
||||
load_id=0;
|
||||
}
|
||||
catch(e) {
|
||||
window['console'].log(JSON.stringify(e));
|
||||
}
|
||||
log_event("load_video", {"id":id,"start":startSeconds});
|
||||
//$("#slider").slider("option","value",startSeconds);
|
||||
//seekTo(startSeconds);
|
||||
}
|
||||
|
||||
function syncPlayButton(){
|
||||
var state = getPlayerState();
|
||||
if (state == 1 || state == 3) {
|
||||
$("#video_control").removeClass("play").addClass("pause");
|
||||
} else if (state == 2 || state == -1 || state == 0){
|
||||
$("#video_control").removeClass("pause").addClass("play");
|
||||
}
|
||||
}
|
||||
|
||||
function cueNewVideo(id, startSeconds) {
|
||||
if (ytplayer) {
|
||||
ytplayer.cueVideoById(id, startSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (ytplayer) {
|
||||
ytplayer.playVideo();
|
||||
}
|
||||
log_event("play_video", {"id":getCurrentTime(), "code":getEmbedCode()});
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (ytplayer) {
|
||||
ytplayer.pauseVideo();
|
||||
}
|
||||
log_event("pause_video", {"id":getCurrentTime(), "code":getEmbedCode()});
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (ytplayer) {
|
||||
ytplayer.stopVideo();
|
||||
}
|
||||
log_event("stop_video", {"id":getCurrentTime(), "code":getEmbedCode()});
|
||||
}
|
||||
|
||||
function getPlayerState() {
|
||||
if (ytplayer) {
|
||||
return ytplayer.getPlayerState();
|
||||
}
|
||||
}
|
||||
|
||||
function seekTo(seconds) {
|
||||
if (ytplayer) {
|
||||
ytplayer.seekTo(seconds, true);
|
||||
}
|
||||
}
|
||||
|
||||
function getBytesTotal() {
|
||||
if (ytplayer) {
|
||||
return ytplayer.getVideoBytesTotal();
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentTime() {
|
||||
if (ytplayer) {
|
||||
return ytplayer.getCurrentTime();
|
||||
}
|
||||
}
|
||||
|
||||
function getDuration() {
|
||||
if (ytplayer) {
|
||||
return ytplayer.getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
function getStartBytes() {
|
||||
if (ytplayer) {
|
||||
return ytplayer.getVideoStartBytes();
|
||||
}
|
||||
}
|
||||
|
||||
function mute() {
|
||||
if (ytplayer) {
|
||||
ytplayer.mute();
|
||||
}
|
||||
}
|
||||
|
||||
function unMute() {
|
||||
if (ytplayer) {
|
||||
ytplayer.unMute();
|
||||
}
|
||||
}
|
||||
|
||||
function getEmbedCode() {
|
||||
if(ytplayer) {
|
||||
ytplayer.getVideoEmbedCode();
|
||||
}
|
||||
}
|
||||
|
||||
function getVideoUrl() {
|
||||
if(ytplayer) {
|
||||
ytplayer.getVideoUrl();
|
||||
}
|
||||
}
|
||||
|
||||
function setVolume(newVolume) {
|
||||
if (ytplayer) {
|
||||
ytplayer.setVolume(newVolume);
|
||||
}
|
||||
}
|
||||
|
||||
function getVolume() {
|
||||
if (ytplayer) {
|
||||
return ytplayer.getVolume();
|
||||
}
|
||||
}
|
||||
|
||||
function clearVideo() {
|
||||
if (ytplayer) {
|
||||
ytplayer.clearVideo();
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,13 @@ div.course-wrapper {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
|
||||
&.problem-header {
|
||||
section.staff {
|
||||
margin-top: 30px;
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:1120px) {
|
||||
display: block;
|
||||
width: auto;
|
||||
@@ -195,6 +202,10 @@ div.course-wrapper {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.histogram {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc outside none;
|
||||
|
||||
@@ -123,7 +123,7 @@ nav.sequence-nav {
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
p {
|
||||
p {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
display: none;
|
||||
@@ -231,6 +231,10 @@ nav.sequence-nav {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.touch-based-device & ol li a:hover p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -304,3 +308,4 @@ section.course-content {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,38 +14,29 @@ section.course-content {
|
||||
}
|
||||
}
|
||||
|
||||
div.video-subtitles {
|
||||
div.video {
|
||||
@include clearfix();
|
||||
background: #f3f3f3;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
@include clearfix();
|
||||
display: block;
|
||||
margin: 0 (-(lh()));
|
||||
padding: 6px lh();
|
||||
|
||||
div.video-wrapper {
|
||||
article.video-wrapper {
|
||||
float: left;
|
||||
margin-right: flex-gutter(9);
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
div.video-player {
|
||||
section.video-player {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
padding-bottom: 56.25%;
|
||||
padding-top: 30px;
|
||||
position: relative;
|
||||
|
||||
object {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
iframe#html5_player {
|
||||
object, iframe {
|
||||
border: none;
|
||||
display: none;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
@@ -68,7 +59,7 @@ section.course-content {
|
||||
}
|
||||
}
|
||||
|
||||
div#slider {
|
||||
div.slider {
|
||||
@extend .clearfix;
|
||||
background: #c2c2c2;
|
||||
border: none;
|
||||
@@ -175,7 +166,8 @@ section.course-content {
|
||||
}
|
||||
}
|
||||
|
||||
div#vidtime {
|
||||
div.vidtime {
|
||||
padding-left: lh(.75);
|
||||
font-weight: bold;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
padding-left: lh(.75);
|
||||
@@ -190,8 +182,20 @@ section.course-content {
|
||||
|
||||
div.speeds {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
&.open {
|
||||
&>a {
|
||||
background: url('../images/open-arrow.png') 10px center no-repeat;
|
||||
}
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&>a {
|
||||
background: url('../images/closed-arrow.png') 10px center no-repeat;
|
||||
border-left: 1px solid #000;
|
||||
border-right: 1px solid #000;
|
||||
@@ -208,15 +212,6 @@ section.course-content {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 110px;
|
||||
|
||||
&.open {
|
||||
background: url('../images/open-arrow.png') 10px center no-repeat;
|
||||
|
||||
ol#video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #999;
|
||||
float: left;
|
||||
@@ -234,47 +229,52 @@ section.course-content {
|
||||
padding: 0 lh(.5) 0 0;
|
||||
}
|
||||
|
||||
// fix for now
|
||||
ol#video_speeds {
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
|
||||
display: none;
|
||||
left: -1px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top:0;
|
||||
@include transition();
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid #000;
|
||||
@include box-shadow( 0 1px 0 #555);
|
||||
// fix for now
|
||||
ol.video_speeds {
|
||||
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
|
||||
@include transition();
|
||||
background-color: #444;
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 125px;
|
||||
z-index: 10;
|
||||
|
||||
li {
|
||||
@include box-shadow( 0 1px 0 #555);
|
||||
border-bottom: 1px solid #000;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
border: 0;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
padding: 0 lh(.5);
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
@include box-shadow(none);
|
||||
margin-top: 0;
|
||||
}
|
||||
display: block;
|
||||
padding: lh(.5);
|
||||
|
||||
&:hover {
|
||||
background-color: #666;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #444;
|
||||
opacity: 1;
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@include box-shadow(none);
|
||||
border-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,7 +334,7 @@ section.course-content {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div#slider {
|
||||
div.slider {
|
||||
height: 14px;
|
||||
margin-top: -7px;
|
||||
|
||||
@@ -352,15 +352,14 @@ section.course-content {
|
||||
ol.subtitles {
|
||||
float: left;
|
||||
max-height: 460px;
|
||||
overflow: hidden;
|
||||
padding-top: 10px;
|
||||
overflow: auto;
|
||||
width: flex-grid(3, 9);
|
||||
|
||||
li {
|
||||
border: 0;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
@include transition(all, .5s, ease-in);
|
||||
|
||||
@@ -373,11 +372,7 @@ section.course-content {
|
||||
color: $mit-red;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
div:empty {
|
||||
&:empty {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
@@ -386,7 +381,7 @@ section.course-content {
|
||||
&.closed {
|
||||
@extend .trans;
|
||||
|
||||
div.video-wrapper {
|
||||
article.video-wrapper {
|
||||
width: flex-grid(9,9);
|
||||
}
|
||||
|
||||
@@ -441,11 +436,11 @@ section.course-content {
|
||||
}
|
||||
|
||||
div.tc-wrapper {
|
||||
div.video-wrapper {
|
||||
article.video-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
object#myytplayer, iframe {
|
||||
object, iframe {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
@@ -487,7 +482,7 @@ section.course-content {
|
||||
}
|
||||
}
|
||||
|
||||
div.course-wrapper.closed section.course-content div.video-subtitles {
|
||||
div.course-wrapper.closed section.course-content div.video {
|
||||
ol.subtitles {
|
||||
max-height: 577px;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,10 @@
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
##Is there a reason this isn't in header_extra? Is it important that the javascript is at the bottom of the generated document?
|
||||
<!-- TODO: http://docs.jquery.com/Plugins/Validation -->
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
${init}
|
||||
});
|
||||
document.write('\x3Cscript type="text/javascript" src="' +
|
||||
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -26,7 +24,7 @@
|
||||
<a href="#">close</a>
|
||||
</header>
|
||||
|
||||
<div id="accordion">
|
||||
<div id="accordion" style="display: none">
|
||||
<nav>
|
||||
${accordion}
|
||||
</nav>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<h1>Circuits & Electronics</h1>
|
||||
<h2>6.002x</h2>
|
||||
<a class="enroll" rel="leanModal" href="/info">View 6.002x Circuits <span>&</span> Electronics as a guest</a>
|
||||
<a class="enroll" rel="leanModal" href="#enroll"><noscript>In order to</noscript> Enroll in 6.002x Circuits <span>&</span> Electronics <noscript>you need to have javascript enabled</noscript></a>
|
||||
</section>
|
||||
<p>6.002x (Circuits and Electronics) is an experimental on-line adaptation of MIT’s first undergraduate analog design course: 6.002. This course is running, free of charge, for students worldwide from March 5, 2012 through June 8, 2012.</p>
|
||||
</section>
|
||||
@@ -53,6 +54,7 @@
|
||||
|
||||
<section class="cta">
|
||||
<a class="enroll" rel="leanModal" href="/info">View 6.002x Circuits & Electronics as a guest</a>
|
||||
<a class="enroll" rel="leanModal" href="#enroll"><noscript>In order to</noscript> Enroll in 6.002x Circuits & Electronics <noscript>you need to have javascript enabled</noscript></a>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
66
templates/jasmine/base.html
Normal file
66
templates/jasmine/base.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!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/jasmine-jquery.js' %}"></script>
|
||||
|
||||
{# source files #}
|
||||
{% for url in suite.js_files %}
|
||||
<script src="{{ url }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
{% load compressed %}
|
||||
{# static files #}
|
||||
{% compressed_js 'application' %}
|
||||
|
||||
{# 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>
|
||||
@@ -1,12 +1,12 @@
|
||||
<section class="text-input">
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
|
||||
% if size:
|
||||
size="${size}"
|
||||
% endif
|
||||
% if dojs == 'math':
|
||||
onkeyup="DoUpdateMath('${id}')"
|
||||
% endif
|
||||
/>
|
||||
<section id="jstextline_${id}" class="jstextline">
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
|
||||
% if size:
|
||||
size="${size}"
|
||||
% endif
|
||||
% if dojs == 'math':
|
||||
onkeyup="DoUpdateMath('${id}')"
|
||||
% endif
|
||||
/>
|
||||
|
||||
% if dojs == 'math':
|
||||
<span id="display_${id}">`{::}`</span>
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
% elif state == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
|
||||
@@ -49,10 +49,8 @@
|
||||
|
||||
<%block name="headextra"/>
|
||||
|
||||
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
|
||||
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
|
||||
MathJax extension libraries -->
|
||||
<%include file="mathjax_include.html" />
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
</head>
|
||||
|
||||
<body class="<%block name='bodyclass'/>">
|
||||
@@ -138,7 +136,6 @@
|
||||
<script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.qtip.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.cookie.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/video_player.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/cktsim.js')}"></script>
|
||||
|
||||
@@ -146,6 +143,7 @@
|
||||
var codemirror_set= {}; // associative array of codemirror objects
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/jquery.scrollTo-1.4.2-min.js')}"></script>
|
||||
<%block name="js_extra"/>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -80,11 +80,7 @@ function DoUpdateMath(inputId) {
|
||||
|
||||
</script>
|
||||
|
||||
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates -->
|
||||
<!-- TODO: move to settings -->
|
||||
|
||||
## next two lines are alternate mathjax sources and configurations
|
||||
## <script type="text/javascript" src="http://cdn.mathjax.org/mathjax/2.0-latest/MathJax.js?config=TeX-MML-AM_HTMLorMML-full">
|
||||
## <script type="text/javascript" src="/static/js/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-AMS_HTML-full"></script>
|
||||
|
||||
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
|
||||
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
|
||||
MathJax extension libraries -->
|
||||
<script type="text/javascript" src="/static/js/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<h2 class="problem-header">${ problem['name'] }
|
||||
% if problem['weight']:
|
||||
: ${ problem['weight'] } points
|
||||
% endif
|
||||
% if settings.QUICKEDIT:
|
||||
<span class="staff">
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<font size=-2><a href=${MITX_ROOT_URL}/quickedit/${id}>Quick
|
||||
Edit Problem</a></font></span>
|
||||
% endif
|
||||
<h2 class="problem-header">
|
||||
${ problem['name'] }
|
||||
% if problem['weight']:
|
||||
: ${ problem['weight'] } points
|
||||
% endif
|
||||
|
||||
% if settings.QUICKEDIT:
|
||||
<section class="staff">
|
||||
<a href=${MITX_ROOT_URL}/quickedit/${id}>Quick Edit Problem</a>
|
||||
</section>
|
||||
% endif
|
||||
</h2>
|
||||
|
||||
<section class="problem">
|
||||
@@ -21,16 +19,16 @@ Edit Problem</a></font></span>
|
||||
<input type="hidden" name="problem_id" value="${ problem['name'] }">
|
||||
|
||||
% if check_button:
|
||||
<input id="check_${ id }" type="button" value="${ check_button }" >
|
||||
<input class="check" type="button" value="${ check_button }">
|
||||
% endif
|
||||
% if reset_button:
|
||||
<input id="reset_${ id }" type="button" value="Reset" >
|
||||
<input class="reset" type="button" value="Reset">
|
||||
% endif
|
||||
% if save_button:
|
||||
<input id="save_${ id }" type="button" value="Save" >
|
||||
<input class="save" type="button" value="Save">
|
||||
% endif
|
||||
% if answer_available:
|
||||
<input id="show_${ id }" type="button" value="Show Answer" >
|
||||
<input class="show" type="button" value="Show Answer">
|
||||
% endif
|
||||
% if explain :
|
||||
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
|
||||
@@ -42,3 +40,4 @@ Edit Problem</a></font></span>
|
||||
% endif
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<section id="main_${id}" class="problems-wrapper"></section>
|
||||
<section id="problem_${id}" class="problems-wrapper" data-url="${ajax_url}"></section>
|
||||
|
||||
@@ -75,7 +75,7 @@ $(function() {
|
||||
});
|
||||
|
||||
$('#pwd_reset_button').click(function() {
|
||||
$.post('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
|
||||
$.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
|
||||
"email" : $('#id_email').val()}, function(data){
|
||||
$("#password_reset_complete_link").click();
|
||||
log_event("profile", {"type":"password_send"});
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<nav aria-label="Section Navigation" class="sequence-nav">
|
||||
<ol>
|
||||
% for t in range(1,1+len(items)):
|
||||
<li><a href="#" class="seq_inactive" id="tt_${ t }"></a></li>
|
||||
% endfor
|
||||
</ol>
|
||||
<div id="sequence_${id}" class="sequence">
|
||||
<nav aria-label="Section Navigation" class="sequence-nav">
|
||||
<ol id="sequence-list">
|
||||
</ol>
|
||||
|
||||
<ul>
|
||||
<li class="${ id }prev prev"><a href="#">Previous</a></li>
|
||||
<li class="${ id }next next"><a href="#">Next</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<ul class="sequence-nav-buttons">
|
||||
<li class="prev"><a href="#">Previous</a></li>
|
||||
<li class="next"><a href="#">Next</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div id="seq_content"></div>
|
||||
<div id="seq_content"></div>
|
||||
|
||||
<nav class="sequence-bottom">
|
||||
<ul aria-label="Section Navigation">
|
||||
<li class="${ id }prev prev"><a href="#">Previous</a></li>
|
||||
<li class="${ id }next next"><a href="#">Next</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav class="sequence-bottom">
|
||||
<ul aria-label="Section Navigation" class="sequence-nav-buttons">
|
||||
<li class="prev"><a href="#">Previous</a></li>
|
||||
<li class="next"><a href="#">Next</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
new Sequence('${id}', ${items}, '${tag}', ${position});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// IMPORTANT TODO: Namespace
|
||||
|
||||
var ${ id }contents=["",
|
||||
%for t in items:
|
||||
${t['content']} ,
|
||||
%endfor
|
||||
""
|
||||
];
|
||||
|
||||
var ${ id }types=["",
|
||||
%for t in items:
|
||||
"${t['type']}" ,
|
||||
%endfor
|
||||
""
|
||||
];
|
||||
|
||||
var ${ id }init_functions=["",
|
||||
%for t in items:
|
||||
function(){ ${t['init_js']} },
|
||||
%endfor
|
||||
""];
|
||||
|
||||
var ${ id }titles=${titles};
|
||||
|
||||
var ${ id }destroy_functions=["",
|
||||
%for t in items:
|
||||
function(){ ${t['destroy_js']} },
|
||||
%endfor
|
||||
""];
|
||||
|
||||
var ${ id }loc = -1;
|
||||
function disablePrev() {
|
||||
var i=${ id }loc-1;
|
||||
log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'});
|
||||
if (i < 1 ) {
|
||||
$('.${ id }prev a').addClass('disabled');
|
||||
} else {
|
||||
$('.${ id }prev a').removeClass('disabled');
|
||||
};
|
||||
}
|
||||
|
||||
function disableNext() {
|
||||
var i=${ id }loc+1;
|
||||
log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'});
|
||||
|
||||
if(i > ${ len(items) } ) {
|
||||
$('.${ id }next a').addClass('disabled');
|
||||
} else {
|
||||
$('.${ id }next a').removeClass('disabled');
|
||||
};
|
||||
}
|
||||
|
||||
function ${ id }goto(i) {
|
||||
log_event("seq_goto", {'old':${id}loc, 'new':i,'id':'${id}'});
|
||||
|
||||
postJSON('${ MITX_ROOT_URL }/modx/sequential/${ id }/goto_position',
|
||||
{'position' : i });
|
||||
|
||||
if (${ id }loc!=-1)
|
||||
${ id }destroy_functions[ ${ id }loc ]();
|
||||
$('#seq_content').html(${ id }contents[i]);
|
||||
${ id }init_functions[i]()
|
||||
//$('#tt_'+${ id }loc).attr("style", "background-color:gray");
|
||||
$('#tt_'+${ id }loc).removeClass();
|
||||
$('#tt_'+${ id }loc).addClass("seq_"+${ id }types[${ id }loc]+"_visited");
|
||||
${ id }loc=i;
|
||||
//$('#tt_'+i).attr("style", "background-color:red");
|
||||
$('#tt_'+i).removeClass();
|
||||
$('#tt_'+i).addClass("seq_"+${ id }types[${ id }loc]+"_active");
|
||||
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
|
||||
|
||||
disableNext();
|
||||
disablePrev();
|
||||
}
|
||||
|
||||
function ${ id }setup_click(i) {
|
||||
$('#tt_'+i).click(function(eo) { ${ id }goto(i);});
|
||||
$('#tt_'+i).addClass("seq_"+${ id }types[i]+"_inactive");
|
||||
$('#tt_'+i).append("<p>" + ${ id }titles[i-1] + "</p>");
|
||||
|
||||
}
|
||||
|
||||
function ${ id }next() {
|
||||
var i=${ id }loc+1;
|
||||
log_event("seq_next", {'old':${id}loc, 'new':i,'id':'${id}'});
|
||||
if(i > ${ len(items) } ) {
|
||||
i = ${ len(items) };
|
||||
} else {
|
||||
${ id }goto(i);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function ${ id }prev() {
|
||||
var i=${ id }loc-1;
|
||||
log_event("seq_prev", {'old':${id}loc, 'new':i,'id':'${id}'});
|
||||
if (i < 1 ) {
|
||||
i = 1;
|
||||
} else {
|
||||
${ id }goto(i);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
$(function() {
|
||||
var i;
|
||||
for(i=1; i<${ len(items)+1 }; i++) {
|
||||
${ id }setup_click(i);
|
||||
}
|
||||
|
||||
|
||||
$('.${ id }next a').click(function(eo) { ${ id }next(); return false;});
|
||||
$('.${ id }prev a').click(function(eo) { ${ id }prev(); return false;});
|
||||
${ id }goto( ${ position } );
|
||||
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
<%!
|
||||
import json
|
||||
import math
|
||||
%>
|
||||
|
||||
|
||||
var rawData = ${json.dumps(histogram)};
|
||||
|
||||
var maxx = 1;
|
||||
var maxy = 1.5;
|
||||
var xticks = Array();
|
||||
var yticks = Array();
|
||||
var data = Array();
|
||||
for (var i = 0; i < rawData.length; i++) {
|
||||
var score = rawData[i][0];
|
||||
var count = rawData[i][1];
|
||||
var log_count = Math.log(count + 1);
|
||||
|
||||
data.push( [score, log_count] );
|
||||
|
||||
xticks.push( [score, score.toString()] );
|
||||
yticks.push( [log_count, count.toString()] );
|
||||
|
||||
maxx = Math.max( score + 1, maxx );
|
||||
maxy = Math.max(log_count*1.1, maxy );
|
||||
}
|
||||
|
||||
$.plot($("#histogram_${module_id}"), [{
|
||||
data: data,
|
||||
bars: { show: true,
|
||||
align: 'center',
|
||||
lineWidth: 0,
|
||||
fill: 1.0 },
|
||||
color: "#b72121",
|
||||
}],
|
||||
{
|
||||
xaxis: {min: -1, max: maxx, ticks: xticks, tickLength: 0},
|
||||
yaxis: {min: 0.0, max: maxy, ticks: yticks, labelWidth: 50},
|
||||
}
|
||||
);
|
||||
@@ -2,5 +2,5 @@
|
||||
${xml | h}
|
||||
</div>
|
||||
%if render_histogram:
|
||||
<div id="histogram_${module_id}" style="width:200px;height:150px"></div>
|
||||
<div id="histogram_${module_id}" class="histogram" data-histogram="${histogram}"></div>
|
||||
%endif
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<div id="tabs">
|
||||
<ul>
|
||||
% for t in items:
|
||||
<li> <a href="#tabs-${items.index(t)}">${t[0]}</a>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
% for t in items:
|
||||
<div id="tabs-${items.index(t)}">
|
||||
</div>
|
||||
% endfor
|
||||
|
||||
<div id="tab_${id}" class="tab">
|
||||
<ul class="navigation"></ul>
|
||||
</div>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
new Tab('${id}', ${items});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// IMPORTANT TODO: Namespace
|
||||
|
||||
var ${ id }contents=["",
|
||||
%for t in items:
|
||||
${t[1]['content']} ,
|
||||
%endfor
|
||||
""
|
||||
];
|
||||
|
||||
var ${ id }init_functions=["",
|
||||
%for t in items:
|
||||
function(){ ${t[1]['init_js']} },
|
||||
%endfor
|
||||
""];
|
||||
|
||||
var ${ id }destroy_functions=["",
|
||||
%for t in items:
|
||||
function(){ ${t[1]['destroy_js']} },
|
||||
%endfor
|
||||
""];
|
||||
|
||||
var ${ id }loc = -1;
|
||||
|
||||
function ${ id }goto(i) {
|
||||
if (${ id }loc!=-1)
|
||||
${ id }destroy_functions[ ${ id }loc ]();
|
||||
$('#tabs-'+(i-1)).html(${ id }contents[i]);
|
||||
${ id }init_functions[i]()
|
||||
$('#tt_'+${ id }loc).attr("style", "background-color:grey");
|
||||
${ id }loc=i;
|
||||
$('#tt_'+i).attr("style", "background-color:red");
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
|
||||
}
|
||||
|
||||
$("#tabs").tabs({select:function(event, ui){
|
||||
//global=ui;
|
||||
return true;
|
||||
},
|
||||
show:function(event,ui){
|
||||
//global=ui;
|
||||
${ id }goto(ui.index+1);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
<section class="text-input">
|
||||
<section id="textbox_${id}" class="textbox">
|
||||
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}">${value|h}</textarea>
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
% elif state == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
@@ -18,23 +18,24 @@
|
||||
<span class="message">${msg|n}</span>
|
||||
<br/>
|
||||
|
||||
<br/>
|
||||
<script>
|
||||
// Note: We need to make the area follow the CodeMirror for this to
|
||||
// work.
|
||||
$(function(){
|
||||
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"),
|
||||
{'mode':"${mode}"
|
||||
% if linenumbers=='true':
|
||||
, lineNumbers: true
|
||||
% endif
|
||||
});
|
||||
cm.refresh();
|
||||
codemirror_set["${id}"] = cm; // track it for refreshes
|
||||
});
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.CodeMirror {border-style: solid;
|
||||
border-width: 1px;}
|
||||
</style>
|
||||
<br/>
|
||||
|
||||
<script type="text/javascript" src="${ settings.LIB_URL }CodeMirror/codemirror.js"></script>
|
||||
<script type="text/javascript" src="${ settings.LIB_URL }CodeMirror/python.js"></script>
|
||||
<link rel="stylesheet" href="${ settings.LIB_URL }CodeMirror/codemirror.css" />
|
||||
<script>
|
||||
// Note: We need to make the area follow the CodeMirror for this to work.
|
||||
$(function(){
|
||||
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"), {
|
||||
% if linenumbers == 'true':
|
||||
lineNumbers: true,
|
||||
% endif
|
||||
mode: "${mode}"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style type="text/css">
|
||||
.CodeMirror {border-style: solid;
|
||||
border-width: 1px;}
|
||||
</style>
|
||||
</section>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<section class="text-input">
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
|
||||
% if size:
|
||||
size="${size}"
|
||||
% endif
|
||||
/>
|
||||
<section id="textinput_${id}" class="textinput">
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
|
||||
% if size:
|
||||
size="${size}"
|
||||
% endif
|
||||
/>
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
% elif state == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
|
||||
@@ -1,128 +1,19 @@
|
||||
% if name is not UNDEFINED and name != None:
|
||||
% if name is not UNDEFINED and name is not None:
|
||||
<h1> ${name} </h1>
|
||||
% endif
|
||||
|
||||
<div class="video-subtitles">
|
||||
<div class="tc-wrapper">
|
||||
|
||||
<div class="video-wrapper">
|
||||
<div class="video-player">
|
||||
<div id="ytapiplayer">
|
||||
</div>
|
||||
|
||||
<iframe id="html5_player" type="text/html" frameborder="0">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<section class="video-controls">
|
||||
<div id="slider"></div>
|
||||
|
||||
<section>
|
||||
<ul class="vcr">
|
||||
<li><a id="video_control" class="pause">Pause</a></li>
|
||||
|
||||
<li>
|
||||
<div id="vidtime">0:00 / 0:00</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<div class="secondary-controls">
|
||||
<div class="speeds">
|
||||
<a href="#">
|
||||
<h3>Speed</h3>
|
||||
<p class="active"></p>
|
||||
<ol id="video_speeds"></ol>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
|
||||
|
||||
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
|
||||
</div>
|
||||
<div id="video_${id}" class="video" data-streams="${streams}">
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
<div id="${id}"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="video-controls"></section>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<ol class="subtitles">
|
||||
<!-- <li id="stt_n5"><div id="std_n7" onclick="title_seek(-7);"></div></li> -->
|
||||
<li id="stt_n4"><div id="std_n6" onclick="title_seek(-6);"></div></li>
|
||||
<li id="stt_n4"><div id="std_n5" onclick="title_seek(-5);"></div></li>
|
||||
<li id="stt_n4"><div id="std_n4" onclick="title_seek(-4);"></div></li>
|
||||
<li id="stt_n3"><div id="std_n3" onclick="title_seek(-3);"></div></li>
|
||||
<li id="stt_n2"><div id="std_n2" onclick="title_seek(-2);"></div></li>
|
||||
<li id="stt_n1"><div id="std_n1" onclick="title_seek(-1);"></div></li>
|
||||
<li id="stt_0 "class="current"><div id="std_0" onclick="title_seek(0);"></div></li>
|
||||
<li id="stt_p1"><div id="std_p1" onclick="title_seek( 1);"></div></li>
|
||||
<li id="stt_p2"><div id="std_p2" onclick="title_seek( 2);"></div></li>
|
||||
<li id="stt_p3"><div id="std_p3" onclick="title_seek( 3);"></div></li>
|
||||
<li id="stt_p4"><div id="std_p4" onclick="title_seek( 4);"></div></li>
|
||||
<li id="stt_p5"><div id="std_p5" onclick="title_seek( 5);"></div></li>
|
||||
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 6);"></div></li>
|
||||
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 7);"></div></li>
|
||||
<li id="stt_p6"><div id="std_p7" onclick="title_seek( 8);"></div></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script src="/static/js/jquery.ui.touch-punch.min.js"></script>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(function() {
|
||||
// tooltips for full browser and closed caption
|
||||
$('.add-fullscreen, .hide-subtitles ').qtip({
|
||||
position: {
|
||||
my: 'top right',
|
||||
at: 'top center'
|
||||
}
|
||||
});
|
||||
|
||||
//full browser
|
||||
$('.add-fullscreen').click(function() {
|
||||
|
||||
$('div.video-subtitles').toggleClass('fullscreen');
|
||||
|
||||
if ($('div.video-subtitles').hasClass('fullscreen')) {
|
||||
$('div.video-subtitles').append('<a href="#" class="exit">Exit</a>');
|
||||
} else {
|
||||
$('a.exit').remove();
|
||||
}
|
||||
|
||||
$('.exit').click(function() {
|
||||
$('div.video-subtitles').removeClass('fullscreen');
|
||||
$(this).remove();
|
||||
return false;
|
||||
});
|
||||
|
||||
var link_title = $(this).attr('title');
|
||||
$(this).attr('title', (link_title == 'Exit fill browser') ? 'Fill browser' : 'Exit fill browser');
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
//hide subtitles
|
||||
$('.hide-subtitles').click(function() {
|
||||
$('div.video-subtitles').toggleClass('closed');
|
||||
|
||||
var link_title = $(this).attr('title');
|
||||
$(this).attr('title', (link_title == 'Turn on captions') ? 'Turn off captions' : 'Turn on captions');
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$("div.speeds a").hover(function() {
|
||||
$(this).toggleClass("open");
|
||||
});
|
||||
|
||||
$("div.speeds a").click(function() {
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<ol class="video-mod">
|
||||
% for t in annotations:
|
||||
<li id="video-${annotations.index(t)}">
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
var streams=${ streams }
|
||||
var params = { allowScriptAccess: "always", bgcolor: "#cccccc", wmode: "transparent", allowFullScreen: "true" };
|
||||
var atts = { id: "myytplayer" };
|
||||
|
||||
// If the user doesn't have flash, use the HTML5 Video instead. YouTube's
|
||||
// iFrame API which supports HTML5 is still developmental so it is not default
|
||||
if (swfobject.hasFlashPlayerVersion("10.1")){
|
||||
swfobject.embedSWF(document.location.protocol + "//www.youtube.com/apiplayer?enablejsapi=1&playerapiid=ytplayer?wmode=transparent",
|
||||
"ytapiplayer", "640", "385", "8", null, null, params, atts);
|
||||
} else {
|
||||
|
||||
//end of this URL may need &origin=http://..... once pushed to production to prevent XSS
|
||||
$("#html5_player").attr("src", document.location.protocol + "//www.youtube.com/embed/" + streams["1.0"] + "?enablejsapi=1&controls=0");
|
||||
$("#html5_player").show();
|
||||
|
||||
var tag = document.createElement('script');
|
||||
tag.src = document.location.protocol + "//www.youtube.com/player_api";
|
||||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
// Make sure the callback is called once API ready, YT seems to be buggy
|
||||
loadHTML5Video();
|
||||
}
|
||||
|
||||
var captions=0;
|
||||
|
||||
/* Cache a reference to our slider element */
|
||||
var slider = $('#slider')
|
||||
|
||||
.slider({
|
||||
range: "min",
|
||||
slide: function(event,ui) {
|
||||
var slider_time = format_time(ui.value)
|
||||
|
||||
seek_slide('slide',event.originalEvent,ui.value);
|
||||
handle.qtip('option', 'content.text', '' + slider_time);
|
||||
},
|
||||
stop:function(event,ui){seek_slide('stop',event.originalEvent,ui.value);}
|
||||
}),
|
||||
|
||||
/* Grab and cache the newly created slider handle */
|
||||
handle = $('.ui-slider-handle', slider);
|
||||
|
||||
/*
|
||||
* Selector needs changing here to match your elements.
|
||||
*
|
||||
* Notice the second argument to the $() constructor, which tells
|
||||
* jQuery to use that as the top-level element to seareh down from.
|
||||
*/
|
||||
handle.qtip({
|
||||
content: '' + slider.slider('option', 'value'), // Use the current value of the slider
|
||||
position: {
|
||||
my: 'bottom center',
|
||||
at: 'top center',
|
||||
container: handle // Stick it inside the handle element so it keeps the position synched up
|
||||
},
|
||||
hide: {
|
||||
delay: 700 // Give it a longer delay so it doesn't hide frequently as we move the handle
|
||||
},
|
||||
style: {
|
||||
classes: 'ui-tooltip-slider',
|
||||
widget: true // Make it Themeroller compatible
|
||||
}
|
||||
});
|
||||
|
||||
function good() {
|
||||
window['console'].log(ytplayer.getCurrentTime());
|
||||
}
|
||||
|
||||
ajax_video=good;
|
||||
|
||||
// load the same video speed your last video was at in a sequence
|
||||
// if the last speed played on video doesn't exist on another video just use 1.0 as default
|
||||
|
||||
function add_speed(key, stream) {
|
||||
var id = 'speed_' + stream;
|
||||
|
||||
if (key == video_speed) {
|
||||
$("#video_speeds").append(' <li class="active" id="'+id+'">'+key+'x</li>');
|
||||
$("p.active").text(key + 'x');
|
||||
} else {
|
||||
$("#video_speeds").append(' <li id="'+id+'">'+key+'x</li>');
|
||||
}
|
||||
|
||||
$("#"+id).click(function(){
|
||||
change_video_speed(key, stream);
|
||||
$(this).siblings().removeClass("active");
|
||||
$(this).addClass("active");
|
||||
var active = $(this).text();
|
||||
$("p.active").text(active);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var l=[]
|
||||
for (var key in streams) {
|
||||
l.push(key);
|
||||
}
|
||||
|
||||
function sort_by_value(a,b) {
|
||||
var x=parseFloat(a);
|
||||
var y=parseFloat(b);
|
||||
var r=((x < y) ? -1 : ((x > y) ? 1 : 0));
|
||||
return r;
|
||||
}
|
||||
|
||||
l.sort(sort_by_value);
|
||||
|
||||
$(document).ready(function() {
|
||||
video_speed = $.cookie("video_speed");
|
||||
|
||||
//ugly hack to account for different formats in vid speed in the XML (.75 vs 0.75, 1.5 vs 1.50);
|
||||
if (( !video_speed ) || ( !streams[video_speed] && !streams[video_speed + "0"]) && !streams[video_speed.slice(0,-1)] && !streams[video_speed.slice(1)] && !streams["0" + video_speed]) {
|
||||
video_speed = "1.0";
|
||||
}
|
||||
|
||||
if (streams[video_speed + "0"]){
|
||||
video_speed = video_speed + "0";
|
||||
} else if (streams[video_speed.slice(0, -1)]){
|
||||
video_speed = video_speed.slice(0, -1);
|
||||
} else if (streams[video_speed.slice(1)]) {
|
||||
video_speed = video_speed.slice(1);
|
||||
} else if (streams["0" + video_speed]) {
|
||||
video_speed = "0" + video_speed;
|
||||
}
|
||||
|
||||
loadNewVideo(streams["1.0"], streams[video_speed], ${ position });
|
||||
|
||||
for(var i=0; i<l.length; i++) {
|
||||
add_speed(l[i], streams[l[i]])
|
||||
}
|
||||
|
||||
var dropUpHeight = $('ol#video_speeds').height();
|
||||
console.log(dropUpHeight);
|
||||
$('ol#video_speeds').css('top', -(dropUpHeight + 2));
|
||||
});
|
||||
|
||||
function toggleVideo(){
|
||||
if ($("#video_control").hasClass("play")){
|
||||
play();
|
||||
$("#video_control").removeClass().addClass("pause");
|
||||
} else {
|
||||
pause();
|
||||
$("#video_control").removeClass().addClass("play");
|
||||
}
|
||||
}
|
||||
|
||||
$("#video_control").click(toggleVideo);
|
||||
|
||||
// space bar to pause video
|
||||
$(".video-wrapper").keyup(function(e){
|
||||
active = document.activeElement;
|
||||
if (e.which == 32) {
|
||||
e.preventDefault();
|
||||
$("#video_control").click();
|
||||
}
|
||||
});
|
||||
2
urls.py
2
urls.py
@@ -57,7 +57,7 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^courseware/(?P<course>[^/]*)/$', 'courseware.views.index', name="courseware_course"),
|
||||
url(r'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'),
|
||||
url(r'^section/(?P<section>[^/]*)/$', 'courseware.views.render_section'),
|
||||
url(r'^modx/(?P<module>[^/]*)/(?P<id>[^/]*)/(?P<dispatch>[^/]*)$', 'courseware.views.modx_dispatch'), #reset_problem'),
|
||||
url(r'^modx/(?P<module>[^/]*)/(?P<id>[^/]*)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
|
||||
url(r'^profile$', 'courseware.views.profile'),
|
||||
url(r'^profile/(?P<student_id>[^/]*)/$', 'courseware.views.profile'),
|
||||
url(r'^change_setting$', 'student.views.change_setting'),
|
||||
|
||||
Reference in New Issue
Block a user