diff --git a/.gitignore b/.gitignore index 570fbd87bd..ebf06998b1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ coverage.xml cover/ log/ reports/ -\#*\# \ No newline at end of file +/src/ +\#*\# diff --git a/Gemfile b/Gemfile index b0065474bc..c6a19caca2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ source :rubygems -gem 'rake', '0.8.3' +gem 'rake' gem 'sass', '3.1.15' gem 'bourbon', '~> 1.3.6' diff --git a/Gemfile.lock b/Gemfile.lock index d6ec0096e2..2a9d48c35c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/brew-formulas.txt b/brew-formulas.txt new file mode 100644 index 0000000000..61e3d33e3a --- /dev/null +++ b/brew-formulas.txt @@ -0,0 +1,8 @@ +readline +sqlite +gdbm +pkg-config +gfortran +python +yuicompressor +node diff --git a/create-dev-env.sh b/create-dev-env.sh new file mode 100755 index 0000000000..a45fc0dba0 --- /dev/null +++ b/create-dev-env.sh @@ -0,0 +1,273 @@ +#!/bin/bash +set -e +trap "ouch" ERR + +ouch() { + printf '\E[31m' + + cat<>$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< $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< + # TODO: handle direction and randomize + snippets = [{'snippet': ''' `a+b`
a+b^2
@@ -81,10 +79,7 @@ class MultipleChoiceResponse(GenericResponse): a+b+d
- - 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: - - + TODO: handle direction and randomize + ''' + snippets = [{'snippet': ''' The location of the sky The location of the earth - + '''}] - 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 ... or in a - - Example: - - + ''' + snippets = [{'snippet': '''
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' -
- - Alternatively, the check function can be defined in Example: - - 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>') - - 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 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>'), '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']) diff --git a/djangoapps/courseware/modules/template_module.py b/djangoapps/courseware/modules/template_module.py index a8899f985c..938b5c4497 100644 --- a/djangoapps/courseware/modules/template_module.py +++ b/djangoapps/courseware/modules/template_module.py @@ -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({ }) diff --git a/djangoapps/courseware/modules/vertical_module.py b/djangoapps/courseware/modules/vertical_module.py index f64e45fe7f..0819fb9f1b 100644 --- a/djangoapps/courseware/modules/vertical_module.py +++ b/djangoapps/courseware/modules/vertical_module.py @@ -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]]) diff --git a/djangoapps/courseware/modules/video_module.py b/djangoapps/courseware/modules/video_module.py index c678838f2b..ad26ede81e 100644 --- a/djangoapps/courseware/modules/video_module.py +++ b/djangoapps/courseware/modules/video_module.py @@ -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]]) diff --git a/djangoapps/courseware/modules/x_module.py b/djangoapps/courseware/modules/x_module.py index ffaeb8fb4d..035b3819d1 100644 --- a/djangoapps/courseware/modules/x_module.py +++ b/djangoapps/courseware/modules/x_module.py @@ -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 diff --git a/djangoapps/courseware/views.py b/djangoapps/courseware/views.py index adcff2f06d..3185edbe98 100644 --- a/djangoapps/courseware/views.py +++ b/djangoapps/courseware/views.py @@ -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) diff --git a/djangoapps/simplewiki/views.py b/djangoapps/simplewiki/views.py index 34a81e6b57..167ce27d95 100644 --- a/djangoapps/simplewiki/views.py +++ b/djangoapps/simplewiki/views.py @@ -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): diff --git a/envs/common.py b/envs/common.py index 4fd5046792..31b8e9936f 100644 --- a/envs/common.py +++ b/envs/common.py @@ -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... diff --git a/install.txt b/install.txt new file mode 100644 index 0000000000..fa82b11a5c --- /dev/null +++ b/install.txt @@ -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=. + + diff --git a/rakefile b/rakefile index bdeeb6b09d..a78e835b45 100644 --- a/rakefile +++ b/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} diff --git a/requirements.txt b/requirements.txt index 457144a132..669ffc7ee6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/run.sh b/run.sh index d63ed291f0..3e273657dc 100755 --- a/run.sh +++ b/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=. \ No newline at end of file diff --git a/static/coffee/.gitignore b/static/coffee/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/static/coffee/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/static/coffee/files.json b/static/coffee/files.json index bfae4dfe87..1e5e010d73 100644 --- a/static/coffee/files.json +++ b/static/coffee/files.json @@ -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" diff --git a/static/coffee/fixtures/items.json b/static/coffee/fixtures/items.json new file mode 100644 index 0000000000..df37531f3f --- /dev/null +++ b/static/coffee/fixtures/items.json @@ -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" + } +] diff --git a/static/coffee/fixtures/problem.html b/static/coffee/fixtures/problem.html new file mode 100644 index 0000000000..f77ece7845 --- /dev/null +++ b/static/coffee/fixtures/problem.html @@ -0,0 +1 @@ +
diff --git a/static/coffee/fixtures/problem_content.html b/static/coffee/fixtures/problem_content.html new file mode 100644 index 0000000000..d2e89fed2b --- /dev/null +++ b/static/coffee/fixtures/problem_content.html @@ -0,0 +1,16 @@ +

Problem Header

+ +
+

Problem Content

+ +
+ + + + + + + Explanation +
+
+
diff --git a/static/coffee/fixtures/sequence.html b/static/coffee/fixtures/sequence.html new file mode 100644 index 0000000000..53e9531dd2 --- /dev/null +++ b/static/coffee/fixtures/sequence.html @@ -0,0 +1,20 @@ +
+ + +
+ + +
diff --git a/static/coffee/fixtures/tab.html b/static/coffee/fixtures/tab.html new file mode 100644 index 0000000000..7d28fa2ad7 --- /dev/null +++ b/static/coffee/fixtures/tab.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/static/coffee/fixtures/video.html b/static/coffee/fixtures/video.html new file mode 100644 index 0000000000..15404a89d1 --- /dev/null +++ b/static/coffee/fixtures/video.html @@ -0,0 +1,12 @@ +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/coffee/spec/calculator_spec.coffee b/static/coffee/spec/calculator_spec.coffee index 5c3fde5e2d..58d7c70790 100644 --- a/static/coffee/spec/calculator_spec.coffee +++ b/static/coffee/spec/calculator_spec.coffee @@ -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) diff --git a/static/coffee/spec/calculator_spec.js b/static/coffee/spec/calculator_spec.js deleted file mode 100644 index 9ea57f3aef..0000000000 --- a/static/coffee/spec/calculator_spec.js +++ /dev/null @@ -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); diff --git a/static/coffee/spec/courseware_spec.coffee b/static/coffee/spec/courseware_spec.coffee index 5933e3e686..9d938c14e1 100644 --- a/static/coffee/spec/courseware_spec.coffee +++ b/static/coffee/spec/courseware_spec.coffee @@ -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 """ +
+
+
+ """ - 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('
') - 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('
') - 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 """ +
+
+
+
+
+
+
+ """ + @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]]) diff --git a/static/coffee/spec/courseware_spec.js b/static/coffee/spec/courseware_spec.js deleted file mode 100644 index d2d6e5583b..0000000000 --- a/static/coffee/spec/courseware_spec.js +++ /dev/null @@ -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('
'); - 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('
'); - 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); diff --git a/static/coffee/spec/feedback_form_spec.coffee b/static/coffee/spec/feedback_form_spec.coffee index 191645b3d3..ce4195faab 100644 --- a/static/coffee/spec/feedback_form_spec.coffee +++ b/static/coffee/spec/feedback_form_spec.coffee @@ -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 diff --git a/static/coffee/spec/feedback_form_spec.js b/static/coffee/spec/feedback_form_spec.js deleted file mode 100644 index bccb53604f..0000000000 --- a/static/coffee/spec/feedback_form_spec.js +++ /dev/null @@ -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); diff --git a/static/coffee/spec/helper.coffee b/static/coffee/spec/helper.coffee index 1f27e257c2..34e03e9d32 100644 --- a/static/coffee/spec/helper.coffee +++ b/static/coffee/spec/helper.coffee @@ -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' diff --git a/static/coffee/spec/helper.js b/static/coffee/spec/helper.js deleted file mode 100644 index 3add5f2bf8..0000000000 --- a/static/coffee/spec/helper.js +++ /dev/null @@ -1,5 +0,0 @@ -(function() { - - jasmine.getFixtures().fixturesPath = "/_jasmine/fixtures/"; - -}).call(this); diff --git a/static/coffee/spec/histogram_spec.coffee b/static/coffee/spec/histogram_spec.coffee new file mode 100644 index 0000000000..4fd7ef98c3 --- /dev/null +++ b/static/coffee/spec/histogram_spec.coffee @@ -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 diff --git a/static/coffee/spec/logger_spec.coffee b/static/coffee/spec/logger_spec.coffee new file mode 100644 index 0000000000..bfad742de3 --- /dev/null +++ b/static/coffee/spec/logger_spec.coffee @@ -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 diff --git a/static/coffee/spec/modules/problem_spec.coffee b/static/coffee/spec/modules/problem_spec.coffee new file mode 100644 index 0000000000..6bca63cfe1 --- /dev/null +++ b/static/coffee/spec/modules/problem_spec.coffee @@ -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 '
' + + 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 ''' + + + + + ''' + + 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 ''' + + ''' + $('#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 ''' + % if state == 'unsubmitted': - % elif state == 'correct': + % elif state == 'correct': % elif state == 'incorrect': @@ -18,23 +18,24 @@ ${msg|n}
-
- - +
+ + + + + + diff --git a/templates/textinput.html b/templates/textinput.html index 3e01ae3306..9736199f02 100644 --- a/templates/textinput.html +++ b/templates/textinput.html @@ -1,15 +1,15 @@ -
- +
+ % if state == 'unsubmitted': - % elif state == 'correct': + % elif state == 'correct': % elif state == 'incorrect': diff --git a/templates/video.html b/templates/video.html index 0d1f6c5641..f49b5b56c2 100644 --- a/templates/video.html +++ b/templates/video.html @@ -1,128 +1,19 @@ -% if name is not UNDEFINED and name != None: +% if name is not UNDEFINED and name is not None:

${name}

% endif -
-
- -
-
-
-
- - -
- -
-
- -
-
    -
  • Pause
  • - -
  • -
    0:00 / 0:00
    -
  • -
- - - +
+
+
+
+
-
+ +
+
- -
    - -
  1. -
  2. -
  3. -
  4. -
  5. -
  6. -
  7. -
  8. -
  9. -
  10. -
  11. -
  12. -
  13. -
  14. -
  15. -
-
- -<%block name="js_extra"> - - - -
    % for t in annotations:
  1. diff --git a/templates/video_init.js b/templates/video_init.js deleted file mode 100644 index bcbaecd249..0000000000 --- a/templates/video_init.js +++ /dev/null @@ -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('
  2. '+key+'x
  3. '); - $("p.active").text(key + 'x'); - } else { - $("#video_speeds").append('
  4. '+key+'x
  5. '); - } - - $("#"+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[^/]*)/$', 'courseware.views.index', name="courseware_course"), url(r'^jumpto/(?P[^/]+)/$', 'courseware.views.jump_to'), url(r'^section/(?P
    [^/]*)/$', 'courseware.views.render_section'), - url(r'^modx/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)$', 'courseware.views.modx_dispatch'), #reset_problem'), + url(r'^modx/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'), url(r'^profile$', 'courseware.views.profile'), url(r'^profile/(?P[^/]*)/$', 'courseware.views.profile'), url(r'^change_setting$', 'student.views.change_setting'),