Commit 0a91a98d by Don Mitchell

Merge remote-tracking branch 'origin/feature/cale/cms-master' into

feature/dhm/cms-settings

Conflicts:
	cms/djangoapps/contentstore/course_info_model.py
	cms/djangoapps/contentstore/views.py
	cms/static/js/models/course_info.js
	cms/static/js/template_loader.js
	cms/static/js/views/course_info_edit.js
	cms/templates/base.html
	cms/templates/course_info.html
	cms/urls.py
parents 70e2e048 c3d7c466
......@@ -4,7 +4,6 @@ from xmodule.modulestore.django import modulestore
from lxml import etree
import re
from django.http import HttpResponseBadRequest
from contentstore.utils import get_modulestore
## TODO store as array of { date, content } and override course_info_module.definition_from_xml
## This should be in a class which inherits from XmlDescriptor
......@@ -14,10 +13,10 @@ def get_course_updates(location):
[{id : location.url() + idx to make unique, date : string, content : html string}]
"""
try:
course_updates = get_modulestore(location).get_item(location)
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
template = Location(['i4x', 'edx', "templates", 'course_info', "Empty"])
course_updates = get_modulestore(location).clone_item(template, Location(location))
course_updates = modulestore('direct').clone_item(template, Location(location))
# current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
location_base = course_updates.location.url()
......@@ -54,7 +53,7 @@ def update_course_updates(location, update, passed_id=None):
into the html structure.
"""
try:
course_updates = get_modulestore(location).get_item(location)
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
......@@ -94,13 +93,13 @@ def update_course_updates(location, update, passed_id=None):
date_element = etree.SubElement(element, "h2")
date_element.text = update['date']
if new_html_parsed is not None:
element[1] = new_html_parsed
element.append(new_html_parsed)
else:
date_element.tail = update['content']
# update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed)
get_modulestore(location).update_item(location, course_updates.definition['data'])
modulestore('direct').update_item(location, course_updates.definition['data'])
return {"id" : passed_id,
"date" : update['date'],
......@@ -115,7 +114,7 @@ def delete_course_update(location, update, passed_id):
return HttpResponseBadRequest
try:
course_updates = get_modulestore(location).get_item(location)
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
......@@ -134,7 +133,7 @@ def delete_course_update(location, update, passed_id):
# update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed)
store = get_modulestore(location)
store = modulestore('direct')
store.update_item(location, course_updates.definition['data'])
return get_course_updates(location)
......
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_importer import perform_xlint
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
unnamed_modules = 0
class Command(BaseCommand):
help = \
'''
Verify the structure of courseware as to it's suitability for import
To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)]
'''
def handle(self, *args, **options):
if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
data_dir = args[0]
if len(args) > 1:
course_dirs = args[1:]
else:
course_dirs = None
print "Importing. Data_dir={data}, course_dirs={courses}".format(
data=data_dir,
courses=course_dirs)
perform_xlint(data_dir, course_dirs, load_error_modules=False)
import logging
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from lxml import etree
import re
from django.http import HttpResponseBadRequest, Http404
def get_module_info(store, location, parent_location = None):
try:
if location.revision is None:
module = store.get_item(location)
else:
module = store.get_item(location)
except ItemNotFoundError:
raise Http404
return {
'id': module.location.url(),
'data': module.definition['data'],
'metadata': module.metadata
}
def set_module_info(store, location, post_data):
module = None
isNew = False
try:
if location.revision is None:
module = store.get_item(location)
else:
module = store.get_item(location)
except:
pass
if module is None:
# new module at this location
# presume that we have an 'Empty' template
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
isNew = True
logging.debug('post = {0}'.format(post_data))
if post_data.get('data') is not None:
data = post_data['data']
logging.debug('data = {0}'.format(data))
store.update_item(location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection
if 'children' in post_data and post_data['children'] is not None:
children = post_data['children']
store.update_children(location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata']
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module.metadata:
del module.metadata[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
module.metadata.update(posted_metadata)
# commit to datastore
store.update_metadata(location, module.metadata)
from factory import Factory
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from time import gmtime
from uuid import uuid4
from xmodule.timeparse import stringify_time
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
return XModuleCourseFactory._create(class_to_create, **kwargs)
def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
return XModuleItemFactory._create(class_to_create, **kwargs)
class XModuleCourseFactory(Factory):
"""
Factory for XModule courses.
"""
ABSTRACT_FACTORY = True
_creation_function = (XMODULE_COURSE_CREATION,)
@classmethod
def _create(cls, target_class, *args, **kwargs):
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
org = kwargs.get('org')
number = kwargs.get('number')
display_name = kwargs.get('display_name')
location = Location('i4x', org, number,
'course', Location.clean(display_name))
store = modulestore('direct')
# Write the data to the mongo datastore
new_course = store.clone_item(template, location)
# This metadata code was copied from cms/djangoapps/contentstore/views.py
if display_name is not None:
new_course.metadata['display_name'] = display_name
new_course.metadata['data_dir'] = uuid4().hex
new_course.metadata['start'] = stringify_time(gmtime())
new_course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
# Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), new_course.own_metadata)
return new_course
class Course:
pass
class CourseFactory(XModuleCourseFactory):
FACTORY_FOR = Course
template = 'i4x://edx/templates/course/Empty'
org = 'MITx'
number = '999'
display_name = 'Robot Super Course'
class XModuleItemFactory(Factory):
"""
Factory for XModule items.
"""
ABSTRACT_FACTORY = True
_creation_function = (XMODULE_ITEM_CREATION,)
@classmethod
def _create(cls, target_class, *args, **kwargs):
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
parent_location = Location(kwargs.get('parent_location'))
template = Location(kwargs.get('template'))
display_name = kwargs.get('display_name')
store = modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location)
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
new_item = store.clone_item(template, dest_location)
# TODO: This needs to be deleted when we have proper storage for static content
new_item.metadata['data_dir'] = parent.metadata['data_dir']
# replace the display name with an optional parameter passed in from the caller
if display_name is not None:
new_item.metadata['display_name'] = display_name
store.update_metadata(new_item.location.url(), new_item.own_metadata)
if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
return new_item
class Item:
pass
class ItemFactory(XModuleItemFactory):
FACTORY_FOR = Item
parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
template = 'i4x://edx/templates/chapter/Empty'
display_name = 'Section One'
\ No newline at end of file
import json
from django.test import TestCase
from django.test.client import Client
from mock import patch, Mock
from override_settings import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
......@@ -9,11 +8,10 @@ from path import path
from student.models import Registration
from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml
import copy
from factories import *
def parse_json(response):
......@@ -22,33 +20,33 @@ def parse_json(response):
def user(email):
'''look up a user by email'''
"""look up a user by email"""
return User.objects.get(email=email)
def registration(email):
'''look up registration object by email'''
"""look up registration object by email"""
return Registration.objects.get(user__email=email)
class ContentStoreTestCase(TestCase):
def _login(self, email, pw):
'''Login. View should always return 200. The success/fail is in the
returned json'''
"""Login. View should always return 200. The success/fail is in the
returned json"""
resp = self.client.post(reverse('login_post'),
{'email': email, 'password': pw})
self.assertEqual(resp.status_code, 200)
return resp
def login(self, email, pw):
'''Login, check that it worked.'''
"""Login, check that it worked."""
resp = self._login(email, pw)
data = parse_json(resp)
self.assertTrue(data['success'])
return resp
def _create_account(self, username, email, pw):
'''Try to create an account. No error checking'''
"""Try to create an account. No error checking"""
resp = self.client.post('/create_account', {
'username': username,
'email': email,
......@@ -62,7 +60,7 @@ class ContentStoreTestCase(TestCase):
return resp
def create_account(self, username, email, pw):
'''Create the account and check that it worked'''
"""Create the account and check that it worked"""
resp = self._create_account(username, email, pw)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
......@@ -74,8 +72,8 @@ class ContentStoreTestCase(TestCase):
return resp
def _activate_user(self, email):
'''Look up the activation key for the user, then hit the activate view.
No error checking'''
"""Look up the activation key for the user, then hit the activate view.
No error checking"""
activation_key = registration(email).activation_key
# and now we try to activate
......@@ -220,12 +218,6 @@ class ContentStoreTest(TestCase):
'display_name': 'Robot Super Course',
}
self.section_data = {
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
'template' : 'i4x://edx/templates/chapter/Empty',
'display_name': 'Section One',
}
def tearDown(self):
# Make sure you flush out the test modulestore after the end
# of the last test because otherwise on the next run
......@@ -262,6 +254,16 @@ class ContentStoreTest(TestCase):
self.assertEqual(data['ErrMsg'],
'There is already a course defined with the same organization and course number.')
def test_create_course_with_bad_organization(self):
"""Test new course creation - error path for bad organization name"""
self.course_data['org'] = 'University of California, Berkeley'
resp = self.client.post(reverse('create_new_course'), self.course_data)
data = parse_json(resp)
self.assertEqual(resp.status_code, 200)
self.assertEqual(data['ErrMsg'],
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
def test_course_index_view_with_no_courses(self):
"""Test viewing the index page with no courses"""
# Create a course so there is something to view
......@@ -271,20 +273,27 @@ class ContentStoreTest(TestCase):
status_code=200,
html=True)
def test_course_factory(self):
course = CourseFactory.create()
self.assertIsInstance(course, xmodule.course_module.CourseDescriptor)
def test_item_factory(self):
course = CourseFactory.create()
item = ItemFactory.create(parent_location=course.location)
self.assertIsInstance(item, xmodule.seq_module.SequenceDescriptor)
def test_course_index_view_with_course(self):
"""Test viewing the index page with an existing course"""
# Create a course so there is something to view
resp = self.client.post(reverse('create_new_course'), self.course_data)
CourseFactory.create(display_name='Robot Super Educational Course')
resp = self.client.get(reverse('index'))
self.assertContains(resp,
'<span class="class-name">Robot Super Course</span>',
'<span class="class-name">Robot Super Educational Course</span>',
status_code=200,
html=True)
def test_course_overview_view_with_course(self):
"""Test viewing the course overview page with an existing course"""
# Create a course so there is something to view
resp = self.client.post(reverse('create_new_course'), self.course_data)
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
data = {
'org': 'MITx',
......@@ -300,8 +309,15 @@ class ContentStoreTest(TestCase):
def test_clone_item(self):
"""Test cloning an item. E.g. creating a new section"""
resp = self.client.post(reverse('create_new_course'), self.course_data)
resp = self.client.post(reverse('clone_item'), self.section_data)
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
section_data = {
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
'template' : 'i4x://edx/templates/chapter/Empty',
'display_name': 'Section One',
}
resp = self.client.post(reverse('clone_item'), section_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
......@@ -323,3 +339,4 @@ class ContentStoreTest(TestCase):
def test_edit_unit_full(self):
self.check_edit_unit('full')
from .utils import get_course_location_for_item, get_lms_link_for_item, \
compute_unit_state, get_date_display, UnitState
from util.json_request import expect_json
import json
import logging
import os
import sys
import time
import tarfile
import shutil
from datetime import datetime
from collections import defaultdict
from uuid import uuid4
from path import path
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, \
create_all_course_groups, get_user_by_email, add_user_to_course_group, \
remove_user_from_course_group, is_user_in_course_group_role, \
get_users_in_course_group_by_role
from cache_toolbox.core import del_cached_content
from collections import defaultdict
from datetime import datetime
from django.conf import settings
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.http import HttpResponse, Http404, HttpResponseBadRequest, \
HttpResponseForbidden
from django.core.context_processors import csrf
from django_future.csrf import ensure_csrf_cookie
from external_auth.views import ssl_login_shortcut
from functools import partial
from mitxmako.shortcuts import render_to_response, render_to_string
from path import path
from static_replace import replace_urls
from util.json_request import expect_json
from uuid import uuid4
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from django.core.urlresolvers import reverse
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError
from xmodule.modulestore import Location
from static_replace import replace_urls
from external_auth.views import ssl_login_shortcut
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.exceptions import NotFoundError
from functools import partial
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item
from xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates,\
update_course_updates, delete_course_update
from cache_toolbox.core import del_cached_content
from xmodule.timeparse import stringify_time
from xmodule.x_module import ModuleSystem
from xmodule_modifiers import replace_static_urls, wrap_xmodule
import json
import logging
import os
import shutil
import sys
import tarfile
import time
from contentstore import course_info_model
from contentstore.utils import get_modulestore
from contentstore.module_info_model import get_module_info, set_module_info
from cms.djangoapps.models.settings.course_details import CourseDetails,\
CourseSettingsEncoder
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
......@@ -474,11 +480,20 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None)
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_display.html",
)
# cdodge: Special case
if module.location.category == 'static_tab':
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_tab_display.html",
)
else:
module.get_html = wrap_xmodule(
module.get_html,
module,
"xmodule_display.html",
)
module.get_html = replace_static_urls(
module.get_html,
module.metadata.get('data_dir', module.location.course),
......@@ -905,7 +920,8 @@ def course_info(request, org, course, name, provided_id=None):
'active_tab': 'courseinfo-tab',
'context_course': course_module,
'url_base' : "/" + org + "/" + course + "/",
'course_updates' : json.dumps(course_info_model.get_course_updates(location))
'course_updates' : json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
@expect_json
......@@ -928,13 +944,38 @@ def course_info_updates(request, org, course, provided_id=None):
real_method = request.method
if request.method == 'GET':
return HttpResponse(json.dumps(course_info_model.get_course_updates(location)), mimetype="application/json")
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
elif real_method == 'POST':
return HttpResponse(json.dumps(course_info_model.update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
# new instance (unless django makes PUT a POST): updates are coming as POST. Not sure why.
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
elif real_method == 'PUT':
return HttpResponse(json.dumps(course_info_model.update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
elif real_method == 'DELETE':
return HttpResponse(json.dumps(course_info_model.delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
@expect_json
@login_required
@ensure_csrf_cookie
def module_info(request, module_location):
location = Location(module_location)
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
# check that logged in user has permissions to this item
if not has_access(request.user, location):
raise PermissionDenied()
if real_method == 'GET':
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location)), mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT':
return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
else:
return HttpResponseBadRequest
@login_required
@ensure_csrf_cookie
......@@ -1079,7 +1120,10 @@ def create_new_course(request):
number = request.POST.get('number')
display_name = request.POST.get('display_name')
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
try:
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
except InvalidLocationError as e:
return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
# see if the course already exists
existing_course = None
......
......@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
dev_env=True,
debug=True)
debug=True,
local_loglevel='ERROR',
console_loglevel='ERROR')
PIPELINE_JS['js-test-source'] = {
'source_filenames': sum([
......
<a href="#" class="edit-button"><span class="edit-icon"></span>Edit</a>
<h2>Course Handouts</h2>
<%if (model.get('data') != null) { %>
<div class="handouts-content">
<%= model.get('data') %>
</div>
<% } else {%>
<p>You have no handouts defined</p>
<% } %>
<form class="edit-handouts-form" style="display: block;">
<div class="row">
<textarea class="handouts-content-editor text-editor"></textarea>
</div>
<div class="row">
<a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a>
</div>
</form>
<li name="<%- updateModel.cid %>">
<!-- FIXME what style should we use for initially hidden? --> <!-- TODO decide whether this should use codemirror -->
<form class="new-update-form">
<div class="row">
<label class="inline-label">Date:</label>
<!-- TODO replace w/ date widget and actual date (problem is that persisted version is "Month day" not an actual date obj -->
<input type="text" class="date" value="<%= updateModel.get('date') %>">
</div>
<div class="row">
<textarea class="new-update-content text-editor"><%= updateModel.get('content') %></textarea>
</div>
<div class="row">
<!-- cid rather than id b/c new ones have cid's not id's -->
<a href="#" class="save-button" name="<%= updateModel.cid %>">Save</a>
<a href="#" class="cancel-button" name="<%= updateModel.cid %>">Cancel</a>
</div>
</form>
<div class="post-preview">
<div class="post-actions">
<a href="#" class="edit-button" name="<%- updateModel.cid %>"><span class="edit-icon"></span>Edit</a>
<a href="#" class="delete-button" name="<%- updateModel.cid %>"><span class="delete-icon"></span>Delete</a>
</div>
<h2>
<span class="calendar-icon"></span><span class="date-display"><%=
updateModel.get('date') %></span>
</h2>
<div class="update-contents"><%= updateModel.get('content') %></div>
</div>
</li>
\ No newline at end of file
<!-- In order to enable better debugging of templates, put them in
the script tag section.
TODO add lazy load fn to load templates as needed (called
from backbone view initialize to set this.template of the view)
-->
<%block name="jsextra">
<script type="text/javascript" charset="utf-8">
// How do I load an html file server side so I can
// Precompiling your templates can be a big help when debugging errors you can't reproduce. This is because precompiled templates can provide line numbers and a stack trace, something that is not possible when compiling templates on the client. The source property is available on the compiled template function for easy precompilation.
// <script>CMS.course_info_update = <%= _.template(jstText).source %>;</script>
</script>
</%block>
\ No newline at end of file
cms/static/img/delete-icon.png

970 Bytes | W: | H:

cms/static/img/delete-icon.png

2.77 KB | W: | H:

cms/static/img/delete-icon.png
cms/static/img/delete-icon.png
cms/static/img/delete-icon.png
cms/static/img/delete-icon.png
  • 2-up
  • Swipe
  • Onion skin
cms/static/img/edit-icon.png

1.04 KB | W: | H:

cms/static/img/edit-icon.png

2.86 KB | W: | H:

cms/static/img/edit-icon.png
cms/static/img/edit-icon.png
cms/static/img/edit-icon.png
cms/static/img/edit-icon.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -11,6 +11,11 @@ $(document).ready(function() {
$body = $('body');
$modal = $('.history-modal');
$modalCover = $('<div class="modal-cover">');
// cdodge: this looks funny, but on AWS instances, this base.js get's wrapped in a separate scope as part of Django static
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window, when we can access it from other
// scopes (namely the course-info tab)
window.$modalCover = $modalCover;
$body.append($modalCover);
$newComponentItem = $('.new-component-item');
$newComponentTypePicker = $('.new-component');
......@@ -93,7 +98,7 @@ $(document).ready(function() {
// section name editing
$('.section-name').bind('click', editSectionName);
$('.edit-section-name-cancel').bind('click', cancelEditSectionName);
$('.edit-section-name-save').bind('click', saveEditSectionName);
// $('.edit-section-name-save').bind('click', saveEditSectionName);
// section date setting
$('.set-publish-date').bind('click', setSectionScheduleDate);
......@@ -585,33 +590,44 @@ function hideToastMessage(e) {
$(this).closest('.toast-notification').remove();
}
function addNewSection(e) {
function addNewSection(e, isTemplate) {
e.preventDefault();
var $newSection = $($('#new-section-template').html());
var $cancelButton = $newSection.find('.new-section-name-cancel');
$('.new-courseware-section-button').after($newSection);
$newSection.find('.new-section-name').focus().select();
$newSection.find('.new-section-name-save').bind('click', saveNewSection);
$newSection.find('.new-section-name-cancel').bind('click', cancelNewSection);
$newSection.find('.section-name-form').bind('submit', saveNewSection);
$cancelButton.bind('click', cancelNewSection);
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
}
function checkForCancel(e) {
if(e.which == 27) {
$body.unbind('keyup', checkForCancel);
e.data.$cancelButton.click();
}
}
function saveNewSection(e) {
e.preventDefault();
parent = $(this).data('parent');
template = $(this).data('template');
display_name = $(this).prev('.new-section-name').val();
var $saveButton = $(this).find('.new-section-name-save');
var parent = $saveButton.data('parent');
var template = $saveButton.data('template');
var display_name = $(this).find('.new-section-name').val();
$.post('/clone_item',
{'parent_location' : parent,
'template' : template,
'display_name': display_name,
},
function(data) {
$.post('/clone_item', {
'parent_location' : parent,
'template' : template,
'display_name': display_name,
},
function(data) {
if (data.id != undefined)
location.reload();
});
location.reload();
}
);
}
function cancelNewSection(e) {
......@@ -619,44 +635,44 @@ function cancelNewSection(e) {
$(this).parents('section.new-section').remove();
}
function addNewCourse(e) {
e.preventDefault();
var $newCourse = $($('#new-course-template').html());
var $cancelButton = $newCourse.find('.new-course-cancel');
$('.new-course-button').after($newCourse);
$newCourse.find('.new-course-name').focus().select();
$newCourse.find('.new-course-save').bind('click', saveNewCourse);
$newCourse.find('.new-course-cancel').bind('click', cancelNewCourse);
$newCourse.find('form').bind('submit', saveNewCourse);
$cancelButton.bind('click', cancelNewCourse);
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
}
function saveNewCourse(e) {
e.preventDefault();
var $newCourse = $(this).closest('.new-course');
template = $(this).data('template');
org = $newCourse.find('.new-course-org').val();
number = $newCourse.find('.new-course-number').val();
display_name = $newCourse.find('.new-course-name').val();
var template = $(this).find('.new-course-save').data('template');
var org = $newCourse.find('.new-course-org').val();
var number = $newCourse.find('.new-course-number').val();
var display_name = $newCourse.find('.new-course-name').val();
if (org == '' || number == '' || display_name == ''){
alert('You must specify all fields in order to create a new course.');
return;
}
$.post('/create_new_course',
{ 'template' : template,
'org' : org,
'number' : number,
'display_name': display_name,
},
function(data) {
if (data.id != undefined)
location.reload();
else if (data.ErrMsg != undefined)
$.post('/create_new_course', {
'template' : template,
'org' : org,
'number' : number,
'display_name': display_name,
},
function(data) {
if (data.id != undefined) {
window.location = '/' + data.id.replace(/.*:\/\//, '');
} else if (data.ErrMsg != undefined) {
alert(data.ErrMsg);
});
}
});
}
function cancelNewCourse(e) {
......@@ -672,35 +688,37 @@ function addNewSubsection(e) {
$section.find('.new-subsection-name-input').focus().select();
var $saveButton = $newSubsection.find('.new-subsection-name-save');
$saveButton.bind('click', saveNewSubsection);
var $cancelButton = $newSubsection.find('.new-subsection-name-cancel');
parent = $(this).parents("section.branch").data("id");
var parent = $(this).parents("section.branch").data("id");
$saveButton.data('parent', parent)
$saveButton.data('template', $(this).data('template'));
$newSubsection.find('.new-subsection-name-cancel').bind('click', cancelNewSubsection);
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
$cancelButton.bind('click', cancelNewSubsection);
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
}
function saveNewSubsection(e) {
e.preventDefault();
parent = $(this).data('parent');
template = $(this).data('template');
var parent = $(this).find('.new-subsection-name-save').data('parent');
var template = $(this).find('.new-subsection-name-save').data('template');
display_name = $(this).prev('.subsection-name').find('.new-subsection-name-input').val()
var display_name = $(this).find('.new-subsection-name-input').val();
$.post('/clone_item',
{'parent_location' : parent,
'template' : template,
'display_name': display_name,
},
function(data) {
$.post('/clone_item', {
'parent_location' : parent,
'template' : template,
'display_name': display_name
},
function(data) {
if (data.id != undefined) {
location.reload();
}
});
}
);
}
function cancelNewSubsection(e) {
......@@ -710,22 +728,30 @@ function cancelNewSubsection(e) {
function editSectionName(e) {
e.preventDefault();
$(this).children('div.section-name-edit').show();
$(this).children('span.section-name-span').hide();
$(this).unbind('click', editSectionName);
$(this).children('.section-name-edit').show();
$(this).find('.edit-section-name').focus();
$(this).children('.section-name-span').hide();
$(this).find('.section-name-edit').bind('submit', saveEditSectionName);
$(this).find('.edit-section-name-cancel').bind('click', cancelNewSection);
$body.bind('keyup', { $cancelButton: $(this).find('.edit-section-name-cancel') }, checkForCancel);
}
function cancelEditSectionName(e) {
e.preventDefault();
$(this).parent().hide();
$(this).parent().siblings('span.section-name-span').show();
$(this).parent().siblings('.section-name-span').show();
$(this).closest('.section-name').bind('click', editSectionName);
e.stopPropagation();
}
function saveEditSectionName(e) {
e.preventDefault();
id = $(this).closest("section.courseware-section").data("id");
display_name = $.trim($(this).prev('.edit-section-name').val());
$(this).closest('.section-name').unbind('click', editSectionName);
var id = $(this).closest('.courseware-section').data('id');
var display_name = $.trim($(this).find('.edit-section-name').val());
$(this).closest('.courseware-section .section-name').append($spinner);
$spinner.show();
......@@ -746,10 +772,10 @@ function saveEditSectionName(e) {
}).success(function()
{
$spinner.delay(250).fadeOut(250);
$_this.parent().siblings('span.section-name-span').html(display_name);
$_this.parent().siblings('span.section-name-span').show();
$_this.parent().hide();
e.stopPropagation();
$_this.closest('h3').find('.section-name-span').html(display_name).show();
$_this.hide();
$_this.closest('.section-name').bind('click', editSectionName);
e.stopPropagation();
});
}
......
......@@ -15,7 +15,7 @@ CMS.Models.CourseInfo = Backbone.Model.extend({
// course update -- biggest kludge here is the lack of a real id to map updates to originals
CMS.Models.CourseUpdate = Backbone.Model.extend({
defaults: {
"date" : $.datepicker.formatDate('MM d', new Date()),
"date" : $.datepicker.formatDate('MM d, yy', new Date()),
"content" : ""
}
});
......@@ -29,6 +29,8 @@ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
model : CMS.Models.CourseUpdate
});
\ No newline at end of file
CMS.Models.ModuleInfo = Backbone.Model.extend({
url: function() {return "/module_info/" + this.id;},
defaults: {
"id": null,
"data": null,
"metadata" : null,
"children" : null
},
});
\ No newline at end of file
......@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.3",
templateVersion: "0.0.8",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
......
......@@ -13,7 +13,11 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
el: this.$('#course-update-view'),
collection: this.model.get('updates')
});
// TODO instantiate the handouts view
new CMS.Views.ClassInfoHandoutsView({
el: this.$('#course-handouts-view'),
model: this.model.get('handouts')
});
return this;
}
});
......@@ -34,11 +38,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// instantiates an editor template for each update in the collection
window.templateLoader.loadRemoteTemplate("course_info_update",
// TODO Where should the template reside? how to use the static.url to create the path?
"/static/coffee/src/client_templates/course_info_update.html",
function (raw_template) {
"/static/client_templates/course_info_update.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
self.render();
}
);
},
......@@ -53,28 +57,47 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$(updateEle).append(newEle);
});
this.$el.find(".new-update-form").hide();
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
return this;
},
onNew: function(event) {
var self = this;
// create new obj, insert into collection, and render this one ele overriding the hidden attr
var newModel = new CMS.Models.CourseUpdate();
this.collection.add(newModel, {at : 0});
var newForm = this.template({ updateModel : newModel });
var $newForm = $(this.template({ updateModel : newModel }));
var $textArea = $newForm.find(".new-update-content").first();
if (this.$codeMirror == null ) {
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
});
}
var updateEle = this.$el.find("#course-update-list");
$(updateEle).append(newForm);
$(newForm).find(".new-update-form").show();
$(updateEle).prepend($newForm);
$newForm.addClass('editing');
this.$currentPost = $newForm.closest('li');
window.$modalCover.show();
window.$modalCover.bind('click', function() {
self.closeEditor(self, true);
});
$('.date').datepicker('destroy');
$('.date').datepicker({ 'dateFormat': 'MM d, yy' });
},
onSave: function(event) {
var targetModel = this.eventModel(event);
targetModel.set({ date : this.dateEntry(event).val(), content : this.contentEntry(event).val() });
// push change to display, hide the editor, submit the change
$(this.dateDisplay(event)).val(targetModel.get('date'));
$(this.contentDisplay(event)).val(targetModel.get('content'));
$(this.editor(event)).hide();
console.log(this.contentEntry(event).val());
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
// push change to display, hide the editor, submit the change
this.closeEditor(this);
targetModel.save();
},
......@@ -82,14 +105,31 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// change editor contents back to model values and hide the editor
$(this.editor(event)).hide();
var targetModel = this.eventModel(event);
$(this.dateEntry(event)).val(targetModel.get('date'));
$(this.contentEntry(event)).val(targetModel.get('content'));
this.closeEditor(this, !targetModel.id);
},
onEdit: function(event) {
var self = this;
this.$currentPost = $(event.target).closest('li');
this.$currentPost.addClass('editing');
$(this.editor(event)).show();
var $textArea = this.$currentPost.find(".new-update-content").first();
if (this.$codeMirror == null ) {
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
});
}
window.$modalCover.show();
var targetModel = this.eventModel(event);
window.$modalCover.bind('click', function() {
self.closeEditor(self);
});
},
onDelete: function(event) {
// TODO ask for confirmation
// remove the dom element and delete the model
......@@ -101,6 +141,24 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
}
});
},
closeEditor: function(self, removePost) {
var targetModel = self.collection.getByCid(self.$currentPost.attr('name'));
if(removePost) {
self.$currentPost.remove();
}
// close the modal and insert the appropriate data
self.$currentPost.removeClass('editing');
self.$currentPost.find('.date-display').html(targetModel.get('date'));
self.$currentPost.find('.date').val(targetModel.get('date'));
self.$currentPost.find('.update-contents').html(targetModel.get('content'));
self.$currentPost.find('.new-update-content').val(targetModel.get('content'));
self.$currentPost.find('form').hide();
window.$modalCover.unbind('click');
window.$modalCover.hide();
},
// Dereferencing from events to screen elements
eventModel: function(event) {
......@@ -119,7 +177,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
dateEntry: function(event) {
var li = $(event.currentTarget).closest("li");
if (li) return $(li).find("#date-entry").first();
if (li) return $(li).find(".date").first();
},
contentEntry: function(event) {
......@@ -135,4 +193,83 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
}
});
\ No newline at end of file
// the handouts view is dumb right now; it needs tied to a model and all that jazz
CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
// collection is CourseUpdateCollection
events: {
"click .save-button" : "onSave",
"click .cancel-button" : "onCancel",
"click .edit-button" : "onEdit"
},
initialize: function() {
var self = this;
this.model.fetch(
{
complete: function() {
window.templateLoader.loadRemoteTemplate("course_info_handouts",
"/static/client_templates/course_info_handouts.html",
function (raw_template) {
self.template = _.template(raw_template);
self.render();
}
);
}
}
);
},
render: function () {
var updateEle = this.$el;
var self = this;
this.$el.html(
$(this.template( {
model: this.model
})
)
);
this.$preview = this.$el.find('.handouts-content');
this.$form = this.$el.find(".edit-handouts-form");
this.$editor = this.$form.find('.handouts-content-editor');
this.$form.hide();
return this;
},
onEdit: function(event) {
var self = this;
this.$editor.val(this.$preview.html());
this.$form.show();
if (this.$codeMirror == null) {
this.$codeMirror = CodeMirror.fromTextArea(this.$editor.get(0), {
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
});
}
window.$modalCover.show();
window.$modalCover.bind('click', function() {
self.closeEditor(self);
});
},
onSave: function(event) {
this.model.set('data', this.$codeMirror.getValue());
this.render();
this.model.save();
this.$form.hide();
this.closeEditor(this);
},
onCancel: function(event) {
this.$form.hide();
this.closeEditor(this);
},
closeEditor: function(self) {
this.$form.hide();
window.$modalCover.unbind('click');
window.$modalCover.hide();
}
});
\ No newline at end of file
......@@ -181,6 +181,11 @@ code {
padding: 20px;
}
.details {
margin-bottom: 30px;
font-size: 14px;
}
h4 {
padding: 6px 14px;
border-bottom: 1px solid #cbd1db;
......@@ -338,4 +343,29 @@ body.show-wip {
content: '';
@extend .spinner-icon;
}
}
.new-button {
@include grey-button;
padding: 20px 0;
text-align: center;
&.big {
display: block;
}
}
.edit-button.standard,
.delete-button.standard {
float: left;
@include white-button;
padding: 3px 10px 4px;
margin-left: 7px;
font-size: 12px;
font-weight: 400;
.edit-icon,
.delete-icon {
margin-right: 4px;
}
}
\ No newline at end of file
......@@ -51,14 +51,14 @@
@include button;
border: 1px solid $darkGrey;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0) 60%);
@include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
background-color: #dfe5eb;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #5d6779;
color: #778192;
&:hover {
background-color: #f2f6f9;
color: #5d6779;
color: #778192;
}
}
......
body.updates {
.course-info {
h2 {
margin-bottom: 24px;
font-size: 22px;
font-weight: 300;
}
.course-info-wrapper {
display: table;
width: 100%;
clear: both;
}
.main-column,
.course-handouts {
float: none;
display: table-cell;
}
.main-column {
border-radius: 3px 0 0 3px;
border-right-color: $mediumGrey;
}
.CodeMirror {
border: 1px solid #3c3c3c;
background: #fff;
color: #3c3c3c;
}
}
.course-updates {
padding: 30px 40px;
margin: 0;
li {
padding: 24px 0 32px;
.update-list > li {
padding: 34px 0 42px;
border-top: 1px solid #cbd1db;
}
h3 {
margin-bottom: 18px;
font-size: 14px;
font-weight: 700;
color: #646464;
letter-spacing: 1px;
text-transform: uppercase;
&.editing {
position: relative;
z-index: 1001;
padding: 0;
border-top: none;
border-radius: 3px;
background: #fff;
.post-preview {
display: none;
}
}
h1 {
float: none;
font-size: 24px;
font-weight: 300;
}
h2 {
margin-bottom: 18px;
font-size: 14px;
font-weight: 700;
line-height: 30px;
color: #646464;
letter-spacing: 1px;
text-transform: uppercase;
}
h3 {
margin: 34px 0 11px;
font-size: 16px;
font-weight: 700;
}
}
.update-contents {
padding-left: 30px;
p {
font-size: 14px;
line-height: 18px;
font-size: 16px;
line-height: 25px;
}
p + p {
margin-top: 18px;
margin-top: 25px;
}
.primary {
border: 1px solid #ddd;
background: #f6f6f6;
padding: 20px;
}
}
.new-update-button {
@include grey-button;
@include blue-button;
display: block;
text-align: center;
padding: 12px 0;
padding: 18px 0;
margin-bottom: 28px;
}
.new-update-form {
@include edit-box;
margin-bottom: 24px;
padding: 30px;
border: none;
textarea {
height: 180px;
}
}
.post-actions {
float: right;
.edit-button,
.delete-button{
float: left;
@include white-button;
padding: 3px 10px 4px;
margin-left: 7px;
font-size: 12px;
font-weight: 400;
.edit-icon,
.delete-icon {
margin-right: 4px;
}
}
}
}
.course-handouts {
padding: 15px 20px;
width: 30%;
padding: 20px 30px;
margin: 0;
border-radius: 0 3px 3px 0;
border-left: none;
background: $lightGrey;
.new-handout-button {
@include grey-button;
display: block;
text-align: center;
padding: 12px 0;
margin-bottom: 28px;
h2 {
font-size: 18px;
font-weight: 700;
}
li {
margin-bottom: 10px;
.edit-button {
float: right;
@include white-button;
padding: 3px 10px 4px;
margin-left: 7px;
font-size: 12px;
font-weight: 400;
.edit-icon,
.delete-icon {
margin-right: 4px;
}
}
.handouts-content {
font-size: 14px;
}
.new-handout-form {
@include edit-box;
margin-bottom: 24px;
.treeview-handoutsnav li {
margin-bottom: 12px;
}
}
.edit-handouts-form {
@include edit-box;
position: absolute;
right: 0;
z-index: 10001;
width: 800px;
padding: 30px;
textarea {
height: 300px;
}
}
\ No newline at end of file
......@@ -5,12 +5,7 @@ input.courseware-unit-search-input {
}
.courseware-overview {
.new-courseware-section-button {
@include grey-button;
display: block;
text-align: center;
padding: 12px 0;
}
}
.courseware-section {
......@@ -146,18 +141,18 @@ input.courseware-unit-search-input {
.section-name-edit {
input {
font-size: 16px;
font-size: 16px;
}
.save-button {
@include blue-button;
padding: 7px 20px 7px;
padding: 10px 20px;
margin-right: 5px;
}
.cancel-button {
@include white-button;
padding: 7px 20px 7px;
padding: 10px 20px;
}
}
......@@ -205,7 +200,7 @@ input.courseware-unit-search-input {
.new-section-name-save,
.new-subsection-name-save {
@include blue-button;
padding: 2px 20px 5px;
padding: 6px 20px 8px;
margin: 0 5px;
color: #fff !important;
}
......@@ -213,7 +208,7 @@ input.courseware-unit-search-input {
.new-section-name-cancel,
.new-subsection-name-cancel {
@include white-button;
padding: 2px 20px 5px;
padding: 6px 20px 8px;
color: #8891a1 !important;
}
......
......@@ -89,6 +89,7 @@
.new-course-save {
@include blue-button;
// padding: ;
}
.new-course-cancel {
......
......@@ -137,6 +137,10 @@
height: 11px;
margin-right: 8px;
background: url(../img/plus-icon.png) no-repeat;
&.white {
background: url(../img/plus-icon-white.png) no-repeat;
}
}
.plus-icon-small {
......
.component {
font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #3c3c3c;
a {
color: #1d9dd9;
text-decoration: none;
}
p {
font-size: 16px;
line-height: 1.6;
}
h1 {
float: none;
}
h2 {
color: #646464;
font-size: 19px;
font-weight: 300;
letter-spacing: 1px;
margin-bottom: 15px;
margin-left: 0;
text-transform: uppercase;
}
h3 {
font-size: 19px;
font-weight: 400;
}
h4 {
background: none;
padding: 0;
border: none;
@include box-shadow(none);
font-size: 16px;
font-weight: 400;
}
code {
margin: 0 2px;
padding: 0px 5px;
border-radius: 3px;
border: 1px solid #eaeaea;
white-space: nowrap;
font-family: Monaco, monospace;
font-size: 14px;
background-color: #f8f8f8;
}
p + h2, ul + h2, ol + h2, p + h3 {
margin-top: 40px;
}
p + p, ul + p, ol + p {
margin-top: 20px;
}
p {
color: #3c3c3c;
font: normal 1em/1.6em;
margin: 0px;
}
}
\ No newline at end of file
......@@ -6,6 +6,72 @@
padding: 12px 0;
}
.unit-body {
padding: 30px 40px;
}
.components > li {
margin: 0;
border-radius: 0;
&.new-component-item {
margin-top: 20px;
}
}
.component {
border: 1px solid $mediumGrey;
border-top: none;
&:first-child {
border-top: 1px solid $mediumGrey;
}
&:hover {
border: 1px solid $mediumGrey;
border-top: none;
&:first-child {
border-top: 1px solid $mediumGrey;
}
.drag-handle {
background: url(../img/drag-handles.png) center no-repeat $lightGrey;
}
}
.drag-handle {
top: 0;
right: 0;
z-index: 11;
width: 35px;
border: none;
background: url(../img/drag-handles.png) center no-repeat $lightGrey;
&:hover {
background: url(../img/drag-handles.png) center no-repeat $lightGrey;
}
}
.component-actions {
top: 26px;
right: 44px;
}
}
.component.editing {
.xmodule_display {
display: none;
}
}
.xmodule_display {
padding: 20px 20px 22px;
font-size: 24px;
font-weight: 300;
background: $lightGrey;
}
.static-page-item {
position: relative;
margin: 10px 0;
......
......@@ -4,6 +4,7 @@
}
.main-column {
clear: both;
float: left;
width: 70%;
}
......@@ -54,94 +55,11 @@
position: relative;
z-index: 10;
margin: 20px 40px;
border: 1px solid #d1ddec;
border-radius: 3px;
background: #fff;
@include transition(none);
&:hover {
border-color: #6696d7;
.drag-handle,
.component-actions a {
background-color: $blue;
}
.drag-handle {
border-color: $blue;
}
}
&.editing {
border-color: #6696d7;
.drag-handle,
.component-actions {
display: none;
}
}
&.component-placeholder {
border-color: #6696d7;
}
.xmodule_display {
padding: 40px 20px 20px;
}
.component-actions {
position: absolute;
top: 4px;
right: 4px;
@include transition(opacity .15s);
}
.edit-button,
.delete-button {
float: left;
padding: 3px 10px 4px;
margin-left: 3px;
border: 1px solid #fff;
border-radius: 3px;
background: #d1ddec;
font-size: 12px;
color: #fff;
@include transition(all .15s);
&:hover {
background-color: $blue;
color: #fff;
}
.edit-icon,
.delete-icon {
margin-right: 4px;
}
}
.drag-handle {
position: absolute;
display: block;
top: -1px;
right: -16px;
z-index: -1;
width: 15px;
height: 100%;
border-radius: 0 3px 3px 0;
border: 1px solid #d1ddec;
background: url(../img/white-drag-handles.png) center no-repeat #d1ddec;
cursor: move;
@include transition(all .15s);
}
&.new-component-item {
padding: 0;
border: 1px solid #8891a1;
border-radius: 3px;
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
background-color: #d1dae3;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset);
@include transition(background-color .15s, border-color .15s);
border: none;
border-radius: 0;
&.adding {
background-color: $blue;
......@@ -223,8 +141,63 @@
}
}
.component {
border: 1px solid #d1ddec;
border-radius: 3px;
background: #fff;
@include transition(none);
&:hover {
border-color: #6696d7;
.drag-handle {
background-color: $blue;
border-color: $blue;
}
}
&.editing {
border-color: #6696d7;
.drag-handle,
.component-actions {
display: none;
}
}
&.component-placeholder {
border-color: #6696d7;
}
.component-actions {
position: absolute;
top: 7px;
right: 9px;
@include transition(opacity .15s);
a {
color: $darkGrey;
}
}
.drag-handle {
position: absolute;
display: block;
top: -1px;
right: -16px;
z-index: -1;
width: 15px;
height: 100%;
border-radius: 0 3px 3px 0;
border: 1px solid #d1ddec;
background: url(../img/white-drag-handles.png) center no-repeat #d1ddec;
cursor: move;
@include transition(all .15s);
}
}
.xmodule_display {
padding: 10px 20px;
padding: 40px 20px 20px;
}
.component-editor {
......
......@@ -2,11 +2,6 @@
.user-overview {
@extend .window;
padding: 30px 40px;
.details {
margin-bottom: 20px;
font-size: 14px;
}
}
.new-user-button {
......
......@@ -25,6 +25,7 @@
@import "modal";
@import "alerts";
@import "login";
@import "lms";
@import 'jquery-ui-calendar';
@import 'content-types';
......
......@@ -9,9 +9,6 @@
<%static:css group='base-style'/>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
<title><%block name="title"></%block></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
......@@ -36,9 +33,8 @@
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
<script src="${static.url('js/vendor/jquery.form.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/symbolset.ss-standard.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/symbolset.ss-symbolicons.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
<script type="text/javascript">
document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
......
......@@ -6,8 +6,8 @@
<a href="#" class="cancel-button">Cancel</a>
</div>
<div class="component-actions">
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a>
</div>
<a href="#" class="drag-handle"></a>
${preview}
\ No newline at end of file
......@@ -3,29 +3,39 @@
<!-- TODO decode course # from context_course into title -->
<%block name="title">Course Info</%block>
<%block name="bodyclass">course-info</%block>
<%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/module_info.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript" charset="utf-8">
$(document).ready(function(){
var course_updates = new CMS.Models.CourseUpdateCollection();
course_updates.reset(${course_updates|n});
course_updates.urlbase = '${url_base}';
var editor = new CMS.Views.CourseInfoEdit({
el: $('.main-wrapper'),
model : new CMS.Models.CourseInfo({
courseId : '${context_course.location}',
updates : course_updates,
// FIXME add handouts
handouts : null})
});
editor.render();
});
$(document).ready(function(){
var course_updates = new CMS.Models.CourseUpdateCollection();
course_updates.reset(${course_updates|n});
course_updates.urlbase = '${url_base}';
var course_handouts = new CMS.Models.ModuleInfo({
id: '${handouts_location}'
});
course_handouts.urlbase = '${url_base}';
var editor = new CMS.Views.CourseInfoEdit({
el: $('.main-wrapper'),
model : new CMS.Models.CourseInfo({
courseId : '${context_course.location}',
updates : course_updates,
handouts : course_handouts
})
});
editor.render();
});
</script>
</%block>
......@@ -33,16 +43,18 @@
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Course Info</h1>
<div class="main-column">
<div class="unit-body window" id="course-update-view">
<h2>Updates</h2>
<a href="#" class="new-update-button">New Update</a>
<ol class="update-list" id="course-update-list"></ol>
<!-- probably replace w/ a vertical where each element of the vertical is a separate update w/ a date and html field -->
<div class="course-info-wrapper">
<div class="main-column window">
<article class="course-updates" id="course-update-view">
<h2>Course Updates & News</h2>
<a href="#" class="new-update-button">New Update</a>
<ol class="update-list" id="course-update-list"></ol>
<!-- probably replace w/ a vertical where each element of the vertical is a separate update w/ a date and html field -->
</article>
</div>
</div>
<div class="sidebar window">
</div>
</div>
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
</div>
</div>
</div>
</div>
</%block>
\ No newline at end of file
......@@ -20,23 +20,24 @@
<div>
<h1>Static Tabs</h1>
</div>
<div class="main-column">
<article class="unit-body window">
<div class="tab-list">
<ol class='components'>
% for id in components:
<li class="component" data-id="${id}"/>
% endfor
<li class="new-component-item">
<a href="#" class="new-component-button new-tab">
<span class="plus-icon"></span>New Tab
</a>
</li>
</ol>
</div>
</article>
</div>
<article class="unit-body window">
<div class="details">
<p>Here you can add and manage additional pages for your course. These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.</p>
</div>
<div class="tab-list">
<ol class='components'>
% for id in components:
<li class="component" data-id="${id}"/>
% endfor
<li class="new-component-item">
<a href="#" class="new-button big new-tab">
<span class="plus-icon"></span>New Tab
</a>
</li>
</ol>
</div>
</article>
</div>
</div>
</%block>
\ No newline at end of file
......@@ -22,14 +22,14 @@
</div>
</div>
<div class="row">
<a href="#" class="new-course-save" data-template="${new_course_template}">Save</a>
<a href="#" class="new-course-cancel">Cancel</a>
<input type="submit" value="Save" class="new-course-save" data-template="${new_course_template}" />
<input type="button" value="Cancel" class="new-course-cancel" />
</div>
</form>
</div>
</section>
</script>
</%block>
</%block>
<%block name="content">
<div class="main-wrapper">
......
......@@ -12,10 +12,10 @@
<%namespace name="units" file="widgets/units.html" />
<%block name="jsextra">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script>
</%block>
<%block name="header_extras">
......@@ -24,7 +24,33 @@
<header>
<a href="#" class="expand-collapse-icon collapse"></a>
<div class="item-details">
<h3 class="section-name"><input type="text" value="New Section Name" class="new-section-name" /><a href="#" class="new-section-name-save" data-parent="${parent_location}" data-template="${new_section_template}">Save</a><a href="#" class="new-section-name-cancel">Cancel</a></h3>
<h3 class="section-name">
<form class="section-name-form">
<input type="text" value="New Section Name" class="new-section-name" />
<input type="submit" class="new-section-name-save" data-parent="${parent_location}" data-template="${new_section_template}" value="Save" />
<input type="button" class="new-section-name-cancel" value="Cancel" /></h3>
</form>
</div>
</header>
</section>
</script>
<script type="text/template" id="blank-slate-template">
<section class="courseware-section branch new-section">
<header>
<a href="#" class="expand-collapse-icon collapse"></a>
<div class="item-details">
<h3 class="section-name">
<span class="section-name-span">Click here to set the section name</span>
<form class="section-name-form">
<input type="text" value="New Section Name" class="new-section-name" />
<input type="submit" class="new-section-name-save" data-parent="${parent_location}" data-template="${new_section_template}" value="Save" />
<input type="button" class="new-section-name-cancel" value="Cancel" /></h3>
</form>
</div>
<div class="item-actions">
<a href="#" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle"></a>
</div>
</header>
</section>
......@@ -33,14 +59,14 @@
<script type="text/template" id="new-subsection-template">
<li class="branch collapsed">
<div class="section-item editing">
<div>
<form class="new-subsection-form">
<span class="folder-icon"></span>
<span class="subsection-name">
<input type="text" value="New Subsection" class="new-subsection-name-input" />
</span>
<a href="#" class="new-subsection-name-save">Save</a>
<a href="#" class="new-subsection-name-cancel">Cancel</a>
</div>
<input type="submit" value="Save" class="new-subsection-name-save" />
<input type="button" value="Cancel" class="new-subsection-name-cancel" />
</form>
</div>
<ol>
<li>
......@@ -75,7 +101,7 @@
<h1>Courseware</h1>
<div class="page-actions"></div>
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
<a href="#" class="new-courseware-section-button"><span class="plus-icon"></span> New Section</a>
<a href="#" class="new-button big new-courseware-section-button"><span class="plus-icon"></span> New Section</a>
% for section in sections:
<section class="courseware-section branch" data-id="${section.location}">
<header>
......@@ -83,10 +109,11 @@
<div class="item-details" data-id="${section.location}">
<h3 class="section-name">
<span class="section-name-span">${section.display_name}</span>
<div class="section-name-edit" style="display:none">
<form class="section-name-edit" style="display:none">
<input type="text" value="${section.display_name}" class="edit-section-name" autocomplete="off"/>
<a href="#" class="save-button edit-section-name-save">Save</a><a href="#" class="cancel-button edit-section-name-cancel">Cancel</a>
</div>
<input type="submit" class="save-button edit-section-name-save" value="Save" />
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form>
</h3>
<div class="section-published-date">
<%
......
......@@ -33,7 +33,7 @@
<li class="component" data-id="${id}"/>
% endfor
<li class="new-component-item">
<a href="#" class="new-component-button">
<a href="#" class="new-component-button new-button big">
<span class="plus-icon"></span>New Component
</a>
<div class="new-component">
......
<section class="xmodule_display xmodule_${class_}" data-type="${module_name}">
${display_name}
</section>
......@@ -45,6 +45,10 @@ urlpatterns = ('',
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
# this is a generic method to return the data/metadata associated with a xmodule
url(r'^module_info/(?P<module_location>.*)$', 'contentstore.views.module_info', name='module_info'),
# temporary landing page for a course
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'),
......
......@@ -60,6 +60,7 @@ def replace(static_url, prefix=None, course_namespace=None):
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
def replace_url(static_url):
return replace(static_url, staticfiles_prefix, course_namespace = course_namespace)
......
......@@ -8,7 +8,7 @@ def expect_json(view_function):
def expect_json_with_cloned_request(request, *args, **kwargs):
# cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information
# e.g. 'charset', so we can't do a direct string compare
if request.META['CONTENT_TYPE'].lower().startswith("application/json"):
if request.META.get('CONTENT_TYPE','').lower().startswith("application/json"):
cloned_request = copy.copy(request)
cloned_request.POST = cloned_request.POST.copy()
cloned_request.POST.update(json.loads(request.body))
......
......@@ -21,6 +21,7 @@ def wrap_xmodule(get_html, module, template, context=None):
module: An XModule
template: A template that takes the variables:
content: the results of get_html,
display_name: the display name of the xmodule, if available (None otherwise)
class_: the module class name
module_name: the js_module_name of the module
"""
......@@ -31,6 +32,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html():
context.update({
'content': get_html(),
'display_name' : module.metadata.get('display_name') if module.metadata is not None else None,
'class_': module.__class__.__name__,
'module_name': module.js_module_name
})
......
*/jasmine_test_runner.html
......@@ -3,6 +3,7 @@ import platform
import sys
from logging.handlers import SysLogHandler
LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
def get_logger_config(log_dir,
logging_env="no_env",
......@@ -11,7 +12,8 @@ def get_logger_config(log_dir,
dev_env=False,
syslog_addr=None,
debug=False,
local_loglevel='INFO'):
local_loglevel='INFO',
console_loglevel=None):
"""
......@@ -30,9 +32,12 @@ def get_logger_config(log_dir,
"""
# Revert to INFO if an invalid string is passed in
if local_loglevel not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
if local_loglevel not in LOG_LEVELS:
local_loglevel = 'INFO'
if console_loglevel is None or console_loglevel not in LOG_LEVELS:
console_loglevel = 'DEBUG' if debug else 'INFO'
hostname = platform.node().split(".")[0]
syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s "
"[{hostname} %(process)d] [%(filename)s:%(lineno)d] "
......@@ -55,7 +60,7 @@ def get_logger_config(log_dir,
},
'handlers': {
'console': {
'level': 'DEBUG' if debug else 'INFO',
'level': console_loglevel,
'class': 'logging.StreamHandler',
'formatter': 'standard',
'stream': sys.stdout,
......
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Jasmine Test Runner</title>
<link rel="stylesheet" type="text/css" href="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.css">
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.js"></script>
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine-html.js"></script>
<script type="text/javascript" src="<%= phantom_jasmine_path %>/lib/console-runner.js"></script>
<script type="text/javascript" src="<%= common_coffee_root %>/ajax_prefix.js"></script>
<script type="text/javascript" src="<%= common_coffee_root %>/logger.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.min.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jasmine-jquery.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script>
<script type="text/javascript">
AjaxPrefix.addAjaxPrefix(jQuery, function() {
return "";
});
</script>
<!-- SOURCE FILES -->
<% for src in js_source %>
<script type="text/javascript" src="<%= src %>"></script>
<% end %>
<!-- SPEC FILES -->
<% for src in js_specs %>
<script type="text/javascript" src="<%= src %>"></script>
<% end %>
</head>
<body>
<script type="text/javascript">
var console_reporter = new jasmine.ConsoleReporter()
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
jasmine.getEnv().addReporter(console_reporter);
jasmine.getEnv().execute();
</script>
</body>
</html>
......@@ -347,7 +347,7 @@ class CapaModule(XModule):
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
return self.system.replace_urls(html, self.metadata['data_dir'])
return self.system.replace_urls(html, self.metadata['data_dir'], course_namespace=self.location)
def handle_ajax(self, dispatch, get):
'''
......@@ -451,7 +451,7 @@ class CapaModule(XModule):
new_answers = dict()
for answer_id in answers:
try:
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'])}
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)}
except TypeError:
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
new_answer = {answer_id: answers[answer_id]}
......
......@@ -47,6 +47,11 @@ class StaticContent(object):
return None
@staticmethod
def get_base_url_path_for_course_assets(loc):
if loc is not None:
return "/c4x/{org}/{course}/asset".format(**loc.dict())
@staticmethod
def get_id_from_location(location):
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course,
'category' : location.category, 'name' : location.name,
......
import abc
import inspect
import json
import logging
import random
import sys
......@@ -66,17 +65,27 @@ def grader_from_conf(conf):
for subgraderconf in conf:
subgraderconf = subgraderconf.copy()
weight = subgraderconf.pop("weight", 0)
# NOTE: 'name' used to exist in SingleSectionGrader. We are deprecating SingleSectionGrader
# and converting everything into an AssignmentFormatGrader by adding 'min_count' and
# 'drop_count'. AssignmentFormatGrader does not expect 'name', so if it appears
# in bad_args, go ahead remove it (this causes no errors). Eventually, SingleSectionGrader
# should be completely removed.
name = 'name'
try:
if 'min_count' in subgraderconf:
#This is an AssignmentFormatGrader
subgrader_class = AssignmentFormatGrader
elif 'name' in subgraderconf:
elif name in subgraderconf:
#This is an SingleSectionGrader
subgrader_class = SingleSectionGrader
else:
raise ValueError("Configuration has no appropriate grader class.")
bad_args = invalid_args(subgrader_class.__init__, subgraderconf)
# See note above concerning 'name'.
if bad_args.issuperset({name}):
bad_args = bad_args - {name}
del subgraderconf[name]
if len(bad_args) > 0:
log.warning("Invalid arguments for a subgrader: %s", bad_args)
for key in bad_args:
......
<section id="problem_1" class="problems-wrapper" data-url="/problem/url/"></section>
<section class='xmodule_display xmodule_CapaModule' data-type='Problem'>
<section id='problem_1'
class='problems-wrapper'
data-problem-id='i4x://edX/101/problem/Problem1'
data-url='/problem/Problem1'>
</section>
</section>
\ No newline at end of file
......@@ -8,25 +8,43 @@ describe 'Problem', ->
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
window.update_schematics = ->
# Load this function from spec/helper.coffee
# Note that if your test fails with a message like:
# 'External request attempted for blah, which is not defined.'
# this msg is coming from the stubRequests function else clause.
jasmine.stubRequests()
# note that the fixturesPath is set in spec/helper.coffee
loadFixtures 'problem.html'
spyOn Logger, 'log'
spyOn($.fn, 'load').andCallFake (url, callback) ->
$(@).html readFixtures('problem_content.html')
callback()
jasmine.stubRequests()
describe 'constructor', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
it 'set the element', ->
expect(@problem.el).toBe '#problem_1'
it 'set the element from html', ->
@problem999 = new Problem ("
<section class='xmodule_display xmodule_CapaModule' data-type='Problem'>
<section id='problem_999'
class='problems-wrapper'
data-problem-id='i4x://edX/999/problem/Quiz'
data-url='/problem/quiz/'>
</section>
</section>
")
expect(@problem999.element_id).toBe 'problem_999'
it 'set the element from loadFixtures', ->
@problem1 = new Problem($('.xmodule_display'))
expect(@problem1.element_id).toBe 'problem_1'
describe 'bind', ->
beforeEach ->
spyOn window, 'update_schematics'
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
it 'set mathjax typeset', ->
expect(MathJax.Hub.Queue).toHaveBeenCalled()
......@@ -38,7 +56,7 @@ describe 'Problem', ->
expect($('section.action input:button')).toHandleWith 'click', @problem.refreshAnswers
it 'bind the check button', ->
expect($('section.action input.check')).toHandleWith 'click', @problem.check
expect($('section.action input.check')).toHandleWith 'click', @problem.check_fd
it 'bind the reset button', ->
expect($('section.action input.reset')).toHandleWith 'click', @problem.reset
......@@ -60,7 +78,7 @@ describe 'Problem', ->
describe 'render', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
@bind = @problem.bind
spyOn @problem, 'bind'
......@@ -86,9 +104,13 @@ describe 'Problem', ->
it 're-bind the content', ->
expect(@problem.bind).toHaveBeenCalled()
describe 'check_fd', ->
xit 'should have specs written for this functionality', ->
expect(false)
describe 'check', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
@problem.answers = 'foo=1&bar=2'
it 'log the problem_check event', ->
......@@ -98,30 +120,34 @@ describe 'Problem', ->
it 'submit the answer for check', ->
spyOn $, 'postWithPrefix'
@problem.check()
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_check', 'foo=1&bar=2', jasmine.any(Function)
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/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!')
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'correct', contents: 'Correct!')
@problem.check()
expect(@problem.el.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!')
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'incorrect', contents: 'Incorrect!')
@problem.check()
expect(@problem.el.html()).toEqual 'Correct!'
expect(@problem.el.html()).toEqual 'Incorrect!'
describe 'when the response is undetermined', ->
it 'alert the response', ->
spyOn window, 'alert'
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'Number Only!')
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'Number Only!')
@problem.check()
expect(window.alert).toHaveBeenCalledWith 'Number Only!'
describe 'reset', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
it 'log the problem_reset event', ->
@problem.answers = 'foo=1&bar=2'
......@@ -131,7 +157,8 @@ describe 'Problem', ->
it 'POST to the problem reset page', ->
spyOn $, 'postWithPrefix'
@problem.reset()
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_reset', { id: 1 }, jasmine.any(Function)
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_reset',
{ id: 'i4x://edX/101/problem/Problem1' }, jasmine.any(Function)
it 'render the returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
......@@ -141,7 +168,7 @@ describe 'Problem', ->
describe 'show', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
@problem.el.prepend '<div id="answer_1_1" /><div id="answer_1_2" />'
describe 'when the answer has not yet shown', ->
......@@ -150,12 +177,14 @@ describe 'Problem', ->
it 'log the problem_show event', ->
@problem.show()
expect(Logger.log).toHaveBeenCalledWith 'problem_show', problem: 1
expect(Logger.log).toHaveBeenCalledWith 'problem_show',
problem: 'i4x://edX/101/problem/Problem1'
it 'fetch the answers', ->
spyOn $, 'postWithPrefix'
@problem.show()
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_show', jasmine.any(Function)
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_show',
jasmine.any(Function)
it 'show the answers', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
......@@ -220,7 +249,7 @@ describe 'Problem', ->
describe 'save', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
@problem.answers = 'foo=1&bar=2'
it 'log the problem_save event', ->
......@@ -230,7 +259,8 @@ describe 'Problem', ->
it 'POST to save problem', ->
spyOn $, 'postWithPrefix'
@problem.save()
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_save', 'foo=1&bar=2', jasmine.any(Function)
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_save',
'foo=1&bar=2', jasmine.any(Function)
it 'alert to the user', ->
spyOn window, 'alert'
......@@ -240,7 +270,7 @@ describe 'Problem', ->
describe 'refreshMath', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
$('#input_example_1').val 'E=mc^2'
@problem.refreshMath target: $('#input_example_1').get(0)
......@@ -250,7 +280,7 @@ describe 'Problem', ->
describe 'updateMathML', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
@stubbedJax.root.toMathML.andReturn '<MathML>'
describe 'when there is no exception', ->
......@@ -270,7 +300,7 @@ describe 'Problem', ->
describe 'refreshAnswers', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
@problem = new Problem($('.xmodule_display'))
@problem.el.html '''
<textarea class="CodeMirror" />
<input id="input_1_1" name="input_1_1" class="schematic" value="one" />
......@@ -293,3 +323,6 @@ describe 'Problem', ->
it 'serialize all answers', ->
@problem.refreshAnswers()
expect(@problem.answers).toEqual "input_1_1=one&input_1_2=two"
jasmine.getFixtures().fixturesPath = 'xmodule/js/fixtures'
jasmine.stubbedMetadata =
slowerSpeedYoutubeId:
id: 'slowerSpeedYoutubeId'
duration: 300
normalSpeedYoutubeId:
id: 'normalSpeedYoutubeId'
duration: 200
bogus:
duration: 100
jasmine.stubbedCaption =
start: [0, 10000, 20000, 30000]
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000']
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.match /.+\/problem_get$/
settings.success html: readFixtures('problem_content.html')
else if settings.url == '/calculate' ||
settings.url.match(/.+\/goto_position$/) ||
settings.url.match(/event$/) ||
settings.url.match(/.+\/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', 'getVolume', 'setVolume', 'loadVideoById',
'playVideo', 'pauseVideo', 'seekTo']
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
enableParts = [enableParts] unless $.isArray(enableParts)
suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite
enableParts.push currentPartName
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider']
unless $.inArray(part, enableParts) >= 0
spyOn window, part
loadFixtures 'video.html'
jasmine.stubRequests()
YT.Player = undefined
context.video = new Video 'example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
jasmine.stubYoutubePlayer()
if createPlayer
return new VideoPlayer(video: 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'
......@@ -3,6 +3,7 @@ class @Video
@el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '')
@caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true"
window.player = null
@el = $("#video_#{@id}")
......
......@@ -10,7 +10,7 @@ class @VideoCaption extends Subview
.bind('DOMMouseScroll', @onMovement)
captionURL: ->
"/static/#{@captionDataDir}/subs/#{@youtubeId}.srt.sjson"
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
render: ->
# TODO: make it so you can have a video with no captions.
......
......@@ -31,7 +31,7 @@ class @VideoPlayer extends Subview
el: @el
youtubeId: @video.youtubeId('1.0')
currentSpeed: @currentSpeed()
captionDataDir: @video.caption_data_dir
captionAssetPath: @video.caption_asset_path
unless onTouchBasedDevice()
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
......
......@@ -153,7 +153,7 @@ class Location(_LocationBase):
def check(val, regexp):
if val is not None and regexp.search(val) is not None:
log.debug('invalid characters val="%s", list_="%s"' % (val, list_))
raise InvalidLocationError(location)
raise InvalidLocationError("Invalid characters in '%s'." % (val))
list_ = list(list_)
for val in list_[:4] + [list_[5]]:
......
......@@ -53,6 +53,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
self.unnamed = defaultdict(int) # category -> num of new url_names for that category
self.used_names = defaultdict(set) # category -> set of used url_names
self.org, self.course, self.url_name = course_id.split('/')
# cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name
self.course_id = course_id
self.load_error_modules = load_error_modules
def process_xml(xml):
......@@ -303,7 +305,7 @@ class XMLModuleStore(ModuleStoreBase):
try:
course_descriptor = self.load_course(course_dir, errorlog.tracker)
except Exception as e:
msg = "Failed to load course '{0}': {1}".format(course_dir, str(e))
msg = "ERROR: Failed to load course '{0}': {1}".format(course_dir, str(e))
log.exception(msg)
errorlog.tracker(msg)
......@@ -337,7 +339,7 @@ class XMLModuleStore(ModuleStoreBase):
with open(policy_path) as f:
return json.load(f)
except (IOError, ValueError) as err:
msg = "Error loading course policy from {0}".format(policy_path)
msg = "ERROR: loading course policy from {0}".format(policy_path)
tracker(msg)
log.warning(msg + " " + str(err))
return {}
......@@ -455,10 +457,18 @@ class XMLModuleStore(ModuleStoreBase):
slug = os.path.splitext(os.path.basename(filepath))[0]
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
# VS[compat]:
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
# from the course policy
if category == "static_tab":
for tab in course_descriptor.tabs or []:
if tab.get('url_slug') == slug:
module.metadata['display_name'] = tab['name']
module.metadata['data_dir'] = course_dir
self.modules[course_descriptor.id][module.location] = module
except Exception, e:
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
system.error_tracker("ERROR: " + str(e))
def get_instance(self, course_id, location, depth=0):
"""
......
......@@ -11,12 +11,12 @@ from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
log = logging.getLogger(__name__)
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace):
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace, subpath = 'static'):
remap_dict = {}
# now import all static assets
static_dir = course_data_path / 'static/'
static_dir = course_data_path / subpath
for dirname, dirnames, filenames in os.walk(static_dir):
for filename in filenames:
......@@ -24,6 +24,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
try:
content_path = os.path.join(dirname, filename)
fullname_with_subpath = content_path.replace(static_dir, '') # strip away leading path from the name
if fullname_with_subpath.startswith('/'):
fullname_with_subpath = fullname_with_subpath[1:]
content_loc = StaticContent.compute_location(target_location_namespace.org, target_location_namespace.course, fullname_with_subpath)
mime_type = mimetypes.guess_type(filename)[0]
......@@ -88,7 +90,7 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic
def import_from_xml(store, data_dir, course_dirs=None,
default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True, static_content_store=None, target_location_namespace = None):
load_error_modules=True, static_content_store=None, target_location_namespace=None):
"""
Import the specified xml data_dir into the "store" modulestore,
using org and course as the location org and course.
......@@ -125,8 +127,11 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_location = module.location
if static_content_store is not None:
_namespace_rename = target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location
# first pass to find everything in /static/
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location)
_namespace_rename, subpath='static')
for module in module_store.modules[course_id].itervalues():
......@@ -159,6 +164,16 @@ def import_from_xml(store, data_dir, course_dirs=None,
# HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
module.metadata['hide_progress_tab'] = True
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
# if there is *any* tabs - then there at least needs to be some predefined ones
if module.tabs is None or len(module.tabs) == 0:
module.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
# so let's make sure we import in case there are no other references to it in the modules
verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
......@@ -192,7 +207,6 @@ def import_from_xml(store, data_dir, course_dirs=None,
store.update_item(module.location, module_data)
if 'children' in module.definition:
store.update_children(module.location, module.definition['children'])
......@@ -200,6 +214,100 @@ def import_from_xml(store, data_dir, course_dirs=None,
# inherited metadata everywhere.
store.update_metadata(module.location, dict(module.own_metadata))
return module_store, course_items
return module_store, course_items
def validate_category_hierarcy(module_store, course_id, parent_category, expected_child_category):
err_cnt = 0
parents = []
# get all modules of parent_category
for module in module_store.modules[course_id].itervalues():
if module.location.category == parent_category:
parents.append(module)
for parent in parents:
for child_loc in [Location(child) for child in parent.definition.get('children', [])]:
if child_loc.category != expected_child_category:
err_cnt += 1
print 'ERROR: child {0} of parent {1} was expected to be category of {2} but was {3}'.format(
child_loc, parent.location, expected_child_category, child_loc.category)
return err_cnt
def validate_data_source_path_existence(path, is_err = True, extra_msg = None):
_cnt = 0
if not os.path.exists(path):
print ("{0}: Expected folder at {1}. {2}".format('ERROR' if is_err == True else 'WARNING', path, extra_msg if
extra_msg is not None else ''))
_cnt = 1
return _cnt
def validate_data_source_paths(data_dir, course_dir):
# check that there is a '/static/' directory
course_path = data_dir / course_dir
err_cnt = 0
warn_cnt = 0
err_cnt += validate_data_source_path_existence(course_path / 'static')
warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err = False,
extra_msg = 'Video captions (if they are used) will not work unless they are static/subs.')
return err_cnt, warn_cnt
def perform_xlint(data_dir, course_dirs,
default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True):
err_cnt = 0
warn_cnt = 0
module_store = XMLModuleStore(
data_dir,
default_class=default_class,
course_dirs=course_dirs,
load_error_modules=load_error_modules
)
# check all data source path information
for course_dir in course_dirs:
_err_cnt, _warn_cnt = validate_data_source_paths(path(data_dir), course_dir)
err_cnt += _err_cnt
warn_cnt += _warn_cnt
# first count all errors and warnings as part of the XMLModuleStore import
for err_log in module_store._location_errors.itervalues():
for err_log_entry in err_log.errors:
msg = err_log_entry[0]
if msg.startswith('ERROR:'):
err_cnt+=1
else:
warn_cnt+=1
# then count outright all courses that failed to load at all
for err_log in module_store.errored_courses.itervalues():
for err_log_entry in err_log.errors:
msg = err_log_entry[0]
print msg
if msg.startswith('ERROR:'):
err_cnt+=1
else:
warn_cnt+=1
for course_id in module_store.modules.keys():
# constrain that courses only have 'chapter' children
err_cnt += validate_category_hierarcy(module_store, course_id, "course", "chapter")
# constrain that chapters only have 'sequentials'
err_cnt += validate_category_hierarcy(module_store, course_id, "chapter", "sequential")
# constrain that sequentials only have 'verticals'
err_cnt += validate_category_hierarcy(module_store, course_id, "sequential", "vertical")
print "\n\n------------------------------------------\nVALIDATION SUMMARY: {0} Errors {1} Warnings\n".format(err_cnt, warn_cnt)
if err_cnt > 0:
print "This course is not suitable for importing. Please fix courseware according to specifications before importing."
elif warn_cnt > 0:
print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing"
else:
print "This course can be imported successfully."
......@@ -127,8 +127,10 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
for child in xml_object:
try:
children.append(system.process_xml(etree.tostring(child)).location.url())
except:
except Exception, e:
log.exception("Unable to load child when parsing Sequence. Continuing...")
if system.error_tracker is not None:
system.error_tracker("ERROR: " + str(e))
continue
return {'children': children}
......
......@@ -3,7 +3,7 @@ from xmodule.raw_module import RawDescriptor
from lxml import etree
from mako.template import Template
from xmodule.modulestore.django import modulestore
import logging
class CustomTagModule(XModule):
"""
......@@ -61,7 +61,7 @@ class CustomTagDescriptor(RawDescriptor):
# cdodge: look up the template as a module
template_loc = self.location._replace(category='custom_tag_template', name=template_name)
template_module = modulestore().get_item(template_loc)
template_module = modulestore().get_instance(system.course_id, template_loc)
template_module_data = template_module.definition['data']
template = Template(template_module_data)
return template.render(**params)
......
......@@ -6,6 +6,9 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
log = logging.getLogger(__name__)
......@@ -93,6 +96,13 @@ class VideoModule(XModule):
return self.youtube
def get_html(self):
if isinstance(modulestore(), MongoModuleStore) :
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
else:
# VS[compat]
# cdodge: filesystem static content support.
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
return self.system.render_template('video.html', {
'streams': self.video_list(),
'id': self.location.html_id(),
......@@ -102,6 +112,7 @@ class VideoModule(XModule):
'display_name': self.display_name,
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
'data_dir': self.metadata['data_dir'],
'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions
})
......
......@@ -108,7 +108,20 @@ class HTMLSnippet(object):
All of these will be loaded onto the page in the CMS
"""
return cls.js
# cdodge: We've moved the xmodule.coffee script from an outside directory into the xmodule area of common
# this means we need to make sure that all xmodules include this dependency which had been previously implicitly
# fulfilled in a different area of code
js = cls.js
if js is None:
js = {}
if 'coffee' not in js:
js['coffee'] = []
js['coffee'].append(resource_string(__name__, 'js/src/xmodule.coffee'))
return js
@classmethod
def get_css(cls):
......
CodeMirror.defineMode("htmlmixed", function(config) {
var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true});
var jsMode = CodeMirror.getMode(config, "javascript");
var cssMode = CodeMirror.getMode(config, "css");
function html(stream, state) {
var style = htmlMode.token(stream, state.htmlState);
if (style == "tag" && stream.current() == ">" && state.htmlState.context) {
if (/^script$/i.test(state.htmlState.context.tagName)) {
state.token = javascript;
state.localState = jsMode.startState(htmlMode.indent(state.htmlState, ""));
}
else if (/^style$/i.test(state.htmlState.context.tagName)) {
state.token = css;
state.localState = cssMode.startState(htmlMode.indent(state.htmlState, ""));
}
}
return style;
}
function maybeBackup(stream, pat, style) {
var cur = stream.current();
var close = cur.search(pat), m;
if (close > -1) stream.backUp(cur.length - close);
else if (m = cur.match(/<\/?$/)) {
stream.backUp(cur[0].length);
if (!stream.match(pat, false)) stream.match(cur[0]);
}
return style;
}
function javascript(stream, state) {
if (stream.match(/^<\/\s*script\s*>/i, false)) {
state.token = html;
state.localState = null;
return html(stream, state);
}
return maybeBackup(stream, /<\/\s*script\s*>/,
jsMode.token(stream, state.localState));
}
function css(stream, state) {
if (stream.match(/^<\/\s*style\s*>/i, false)) {
state.token = html;
state.localState = null;
return html(stream, state);
}
return maybeBackup(stream, /<\/\s*style\s*>/,
cssMode.token(stream, state.localState));
}
return {
startState: function() {
var state = htmlMode.startState();
return {token: html, localState: null, mode: "html", htmlState: state};
},
copyState: function(state) {
if (state.localState)
var local = CodeMirror.copyState(state.token == css ? cssMode : jsMode, state.localState);
return {token: state.token, localState: local, mode: state.mode,
htmlState: CodeMirror.copyState(htmlMode, state.htmlState)};
},
token: function(stream, state) {
return state.token(stream, state);
},
indent: function(state, textAfter) {
if (state.token == html || /^\s*<\//.test(textAfter))
return htmlMode.indent(state.htmlState, textAfter);
else if (state.token == javascript)
return jsMode.indent(state.localState, textAfter);
else
return cssMode.indent(state.localState, textAfter);
},
electricChars: "/{}:",
innerMode: function(state) {
var mode = state.token == html ? htmlMode : state.token == javascript ? jsMode : cssMode;
return {state: state.localState || state.htmlState, mode: mode};
}
};
}, "xml", "javascript", "css");
CodeMirror.defineMIME("text/html", "htmlmixed");
......@@ -148,7 +148,7 @@ def get_course_about_section(course, section_key):
request = get_request_for_thread()
loc = course.location._replace(category='about', name=section_key)
course_module = get_module(request.user, request, loc, None, course.id, not_found_ok = True)
course_module = get_module(request.user, request, loc, None, course.id, not_found_ok = True, wrap_xmodule_display = False)
html = ''
......@@ -186,8 +186,7 @@ def get_course_info_section(request, cache, course, section_key):
loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
course_module = get_module(request.user, request, loc, cache, course.id)
course_module = get_module(request.user, request, loc, cache, course.id, wrap_xmodule_display = False)
html = ''
if course_module is not None:
......@@ -196,7 +195,6 @@ def get_course_info_section(request, cache, course, section_key):
return html
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
# arjun will address this by the end of October if no one does so prior to
# then.
......@@ -222,7 +220,7 @@ def get_course_syllabus_section(course, section_key):
filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'),
course.metadata['data_dir'])
course.metadata['data_dir'], course_namespace=course.location)
except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format(
key=section_key, url=course.location.url()))
......
......@@ -115,7 +115,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
return chapters
def get_module(user, request, location, student_module_cache, course_id, position=None, not_found_ok = False):
def get_module(user, request, location, student_module_cache, course_id, position=None, not_found_ok = False, wrap_xmodule_display = True):
"""
Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none
......@@ -136,7 +136,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
if possible. If not possible, return None.
"""
try:
return _get_module(user, request, location, student_module_cache, course_id, position)
return _get_module(user, request, location, student_module_cache, course_id, position, wrap_xmodule_display)
except ItemNotFoundError:
if not not_found_ok:
log.exception("Error in get_module")
......@@ -146,7 +146,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
log.exception("Error in get_module")
return None
def _get_module(user, request, location, student_module_cache, course_id, position=None):
def _get_module(user, request, location, student_module_cache, course_id, position=None, wrap_xmodule_display = True):
"""
Actually implement get_module. See docstring there for details.
"""
......@@ -261,8 +261,13 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
# Make an error module
return err_descriptor.xmodule_constructor(system)(None, None)
_get_html = module.get_html
if wrap_xmodule_display == True:
_get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html')
module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, 'xmodule_display.html'),
_get_html,
module.metadata['data_dir'] if 'data_dir' in module.metadata else '',
course_namespace = module.location._replace(category=None, name=None))
......
......@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
dev_env=True,
debug=True)
debug=True,
local_loglevel='ERROR',
console_loglevel='ERROR')
PIPELINE_JS['js-test-source'] = {
'source_filenames': sum([
......
......@@ -2,7 +2,7 @@
<h2> ${display_name} </h2>
% endif
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}">
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-data-dir="${data_dir}" data-caption-asset-path="${caption_asset_path}" data-show-captions="${show_captions}">
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
......
......@@ -3,6 +3,8 @@ require 'tempfile'
require 'net/http'
require 'launchy'
require 'colorize'
require 'erb'
require 'tempfile'
# Build Constants
REPO_ROOT = File.dirname(__FILE__)
......@@ -47,7 +49,7 @@ def django_for_jasmine(system, django_reload)
end
django_pid = fork do
exec(*django_admin(system, 'jasmine', 'runserver', "12345", reload_arg).split(' '))
exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', "12345", reload_arg).split(' '))
end
jasmine_url = 'http://localhost:12345/_jasmine/'
up = false
......@@ -79,6 +81,31 @@ def django_for_jasmine(system, django_reload)
end
end
def template_jasmine_runner(lib)
coffee_files = Dir["#{lib}/**/js/**/*.coffee", "common/static/coffee/src/**/*.coffee"]
if !coffee_files.empty?
sh("coffee -c #{coffee_files.join(' ')}")
end
phantom_jasmine_path = File.expand_path("common/test/phantom-jasmine")
common_js_root = File.expand_path("common/static/js")
common_coffee_root = File.expand_path("common/static/coffee/src")
# Get arrays of spec and source files, ordered by how deep they are nested below the library
# (and then alphabetically) and expanded from a relative to an absolute path
spec_glob = File.join("#{lib}", "**", "spec", "**", "*.js")
src_glob = File.join("#{lib}", "**", "src", "**", "*.js")
js_specs = Dir[spec_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)}
js_source = Dir[src_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)}
template = ERB.new(File.read("#{lib}/jasmine_test_runner.html.erb"))
template_output = "#{lib}/jasmine_test_runner.html"
File.open(template_output, 'w') do |f|
f.write(template.result(binding))
end
yield File.expand_path(template_output)
end
def report_dir_path(dir)
return File.join(REPORT_DIR, dir.to_s)
end
......@@ -126,22 +153,6 @@ end
end
task :pylint => "pylint_#{system}"
desc "Open jasmine tests in your default browser"
task "browse_jasmine_#{system}" do
django_for_jasmine(system, true) do |jasmine_url|
Launchy.open(jasmine_url)
puts "Press ENTER to terminate".red
$stdin.gets
end
end
desc "Use phantomjs to run jasmine tests from the console"
task "phantomjs_jasmine_#{system}" do
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
django_for_jasmine(system, false) do |jasmine_url|
sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}")
end
end
end
$failed_tests = 0
......@@ -210,6 +221,23 @@ TEST_TASK_DIRS = []
end
end
end
desc "Open jasmine tests for #{system} in your default browser"
task "browse_jasmine_#{system}" do
django_for_jasmine(system, true) do |jasmine_url|
Launchy.open(jasmine_url)
puts "Press ENTER to terminate".red
$stdin.gets
end
end
desc "Use phantomjs to run jasmine tests for #{system} from the console"
task "phantomjs_jasmine_#{system}" do
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
django_for_jasmine(system, false) do |jasmine_url|
sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}")
end
end
end
desc "Reset the relational database used by django. WARNING: this will delete all of your existing users"
......@@ -245,6 +273,22 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
sh("nosetests #{lib}")
end
desc "Open jasmine tests for #{lib} in your default browser"
task "browse_jasmine_#{lib}" do
template_jasmine_runner(lib) do |f|
sh("python -m webbrowser -t 'file://#{f}'")
puts "Press ENTER to terminate".red
$stdin.gets
end
end
desc "Use phantomjs to run jasmine tests for #{lib} from the console"
task "phantomjs_jasmine_#{lib}" do
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
template_jasmine_runner(lib) do |f|
sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{f}")
end
end
end
task :report_dirs
......@@ -364,6 +408,20 @@ namespace :cms do
end
end
namespace :cms do
desc "Import course data within the given DATA_DIR variable"
task :xlint do
if ENV['DATA_DIR'] and ENV['COURSE_DIR']
sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'], ENV['COURSE_DIR']))
elsif ENV['DATA_DIR']
sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR']))
else
raise "Please specify a DATA_DIR variable that point to your data directory.\n" +
"Example: \`rake cms:import DATA_DIR=../data\`"
end
end
end
desc "Build a properties file used to trigger autodeploy builds"
task :autodeploy_properties do
File.open("autodeploy.properties", "w") do |file|
......
......@@ -55,3 +55,4 @@ dogstatsd-python
# Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs.
# MySQL-python
sphinx
factory_boy
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment