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 ...@@ -4,7 +4,6 @@ from xmodule.modulestore.django import modulestore
from lxml import etree from lxml import etree
import re import re
from django.http import HttpResponseBadRequest 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 ## 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 ## This should be in a class which inherits from XmlDescriptor
...@@ -14,10 +13,10 @@ def get_course_updates(location): ...@@ -14,10 +13,10 @@ def get_course_updates(location):
[{id : location.url() + idx to make unique, date : string, content : html string}] [{id : location.url() + idx to make unique, date : string, content : html string}]
""" """
try: try:
course_updates = get_modulestore(location).get_item(location) course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
template = Location(['i4x', 'edx', "templates", 'course_info', "Empty"]) 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} # current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
location_base = course_updates.location.url() location_base = course_updates.location.url()
...@@ -54,7 +53,7 @@ def update_course_updates(location, update, passed_id=None): ...@@ -54,7 +53,7 @@ def update_course_updates(location, update, passed_id=None):
into the html structure. into the html structure.
""" """
try: try:
course_updates = get_modulestore(location).get_item(location) course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
return HttpResponseBadRequest return HttpResponseBadRequest
...@@ -94,13 +93,13 @@ def update_course_updates(location, update, passed_id=None): ...@@ -94,13 +93,13 @@ def update_course_updates(location, update, passed_id=None):
date_element = etree.SubElement(element, "h2") date_element = etree.SubElement(element, "h2")
date_element.text = update['date'] date_element.text = update['date']
if new_html_parsed is not None: if new_html_parsed is not None:
element[1] = new_html_parsed element.append(new_html_parsed)
else: else:
date_element.tail = update['content'] date_element.tail = update['content']
# update db record # update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed) 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, return {"id" : passed_id,
"date" : update['date'], "date" : update['date'],
...@@ -115,7 +114,7 @@ def delete_course_update(location, update, passed_id): ...@@ -115,7 +114,7 @@ def delete_course_update(location, update, passed_id):
return HttpResponseBadRequest return HttpResponseBadRequest
try: try:
course_updates = get_modulestore(location).get_item(location) course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
return HttpResponseBadRequest return HttpResponseBadRequest
...@@ -134,7 +133,7 @@ def delete_course_update(location, update, passed_id): ...@@ -134,7 +133,7 @@ def delete_course_update(location, update, passed_id):
# update db record # update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed) course_updates.definition['data'] = etree.tostring(course_html_parsed)
store = get_modulestore(location) store = modulestore('direct')
store.update_item(location, course_updates.definition['data']) store.update_item(location, course_updates.definition['data'])
return get_course_updates(location) 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 import json
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from mock import patch, Mock
from override_settings import override_settings from override_settings import override_settings
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -9,11 +8,10 @@ from path import path ...@@ -9,11 +8,10 @@ from path import path
from student.models import Registration from student.models import Registration
from django.contrib.auth.models import User from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django import xmodule.modulestore.django
from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
import copy import copy
from factories import *
def parse_json(response): def parse_json(response):
...@@ -22,33 +20,33 @@ def parse_json(response): ...@@ -22,33 +20,33 @@ def parse_json(response):
def user(email): def user(email):
'''look up a user by email''' """look up a user by email"""
return User.objects.get(email=email) return User.objects.get(email=email)
def registration(email): def registration(email):
'''look up registration object by email''' """look up registration object by email"""
return Registration.objects.get(user__email=email) return Registration.objects.get(user__email=email)
class ContentStoreTestCase(TestCase): class ContentStoreTestCase(TestCase):
def _login(self, email, pw): def _login(self, email, pw):
'''Login. View should always return 200. The success/fail is in the """Login. View should always return 200. The success/fail is in the
returned json''' returned json"""
resp = self.client.post(reverse('login_post'), resp = self.client.post(reverse('login_post'),
{'email': email, 'password': pw}) {'email': email, 'password': pw})
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
return resp return resp
def login(self, email, pw): def login(self, email, pw):
'''Login, check that it worked.''' """Login, check that it worked."""
resp = self._login(email, pw) resp = self._login(email, pw)
data = parse_json(resp) data = parse_json(resp)
self.assertTrue(data['success']) self.assertTrue(data['success'])
return resp return resp
def _create_account(self, username, email, pw): 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', { resp = self.client.post('/create_account', {
'username': username, 'username': username,
'email': email, 'email': email,
...@@ -62,7 +60,7 @@ class ContentStoreTestCase(TestCase): ...@@ -62,7 +60,7 @@ class ContentStoreTestCase(TestCase):
return resp return resp
def create_account(self, username, email, pw): 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) resp = self._create_account(username, email, pw)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
data = parse_json(resp) data = parse_json(resp)
...@@ -74,8 +72,8 @@ class ContentStoreTestCase(TestCase): ...@@ -74,8 +72,8 @@ class ContentStoreTestCase(TestCase):
return resp return resp
def _activate_user(self, email): def _activate_user(self, email):
'''Look up the activation key for the user, then hit the activate view. """Look up the activation key for the user, then hit the activate view.
No error checking''' No error checking"""
activation_key = registration(email).activation_key activation_key = registration(email).activation_key
# and now we try to activate # and now we try to activate
...@@ -220,12 +218,6 @@ class ContentStoreTest(TestCase): ...@@ -220,12 +218,6 @@ class ContentStoreTest(TestCase):
'display_name': 'Robot Super Course', '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): def tearDown(self):
# Make sure you flush out the test modulestore after the end # Make sure you flush out the test modulestore after the end
# of the last test because otherwise on the next run # of the last test because otherwise on the next run
...@@ -262,6 +254,16 @@ class ContentStoreTest(TestCase): ...@@ -262,6 +254,16 @@ class ContentStoreTest(TestCase):
self.assertEqual(data['ErrMsg'], self.assertEqual(data['ErrMsg'],
'There is already a course defined with the same organization and course number.') '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): def test_course_index_view_with_no_courses(self):
"""Test viewing the index page with no courses""" """Test viewing the index page with no courses"""
# Create a course so there is something to view # Create a course so there is something to view
...@@ -271,20 +273,27 @@ class ContentStoreTest(TestCase): ...@@ -271,20 +273,27 @@ class ContentStoreTest(TestCase):
status_code=200, status_code=200,
html=True) 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): def test_course_index_view_with_course(self):
"""Test viewing the index page with an existing course""" """Test viewing the index page with an existing course"""
# Create a course so there is something to view CourseFactory.create(display_name='Robot Super Educational Course')
resp = self.client.post(reverse('create_new_course'), self.course_data)
resp = self.client.get(reverse('index')) resp = self.client.get(reverse('index'))
self.assertContains(resp, self.assertContains(resp,
'<span class="class-name">Robot Super Course</span>', '<span class="class-name">Robot Super Educational Course</span>',
status_code=200, status_code=200,
html=True) html=True)
def test_course_overview_view_with_course(self): def test_course_overview_view_with_course(self):
"""Test viewing the course overview page with an existing course""" """Test viewing the course overview page with an existing course"""
# Create a course so there is something to view CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
resp = self.client.post(reverse('create_new_course'), self.course_data)
data = { data = {
'org': 'MITx', 'org': 'MITx',
...@@ -300,8 +309,15 @@ class ContentStoreTest(TestCase): ...@@ -300,8 +309,15 @@ class ContentStoreTest(TestCase):
def test_clone_item(self): def test_clone_item(self):
"""Test cloning an item. E.g. creating a new section""" """Test cloning an item. E.g. creating a new section"""
resp = self.client.post(reverse('create_new_course'), self.course_data) CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
resp = self.client.post(reverse('clone_item'), self.section_data)
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) self.assertEqual(resp.status_code, 200)
data = parse_json(resp) data = parse_json(resp)
...@@ -323,3 +339,4 @@ class ContentStoreTest(TestCase): ...@@ -323,3 +339,4 @@ class ContentStoreTest(TestCase):
def test_edit_unit_full(self): def test_edit_unit_full(self):
self.check_edit_unit('full') self.check_edit_unit('full')
from .utils import get_course_location_for_item, get_lms_link_for_item, \ from util.json_request import expect_json
compute_unit_state, get_date_display, UnitState 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' # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image 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, \ from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
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.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.context_processors import csrf
from django.http import HttpResponse, Http404, HttpResponseBadRequest, \
HttpResponseForbidden
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from external_auth.views import ssl_login_shortcut from django.core.urlresolvers import reverse
from functools import partial from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string
from path import path from xmodule.modulestore import Location
from static_replace import replace_urls from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from util.json_request import expect_json from xmodule.x_module import ModuleSystem
from uuid import uuid4
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError from static_replace import replace_urls
from xmodule.modulestore import Location 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.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 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.timeparse import stringify_time
from xmodule.x_module import ModuleSystem from contentstore.module_info_model import get_module_info, set_module_info
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 cms.djangoapps.models.settings.course_details import CourseDetails,\ from cms.djangoapps.models.settings.course_details import CourseDetails,\
CourseSettingsEncoder CourseSettingsEncoder
from cms.djangoapps.models.settings.course_grading import CourseGradingModel 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' # 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_ ...@@ -474,11 +480,20 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
error_msg=exc_info_to_str(sys.exc_info()) error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None) ).xmodule_constructor(system)(None, None)
module.get_html = wrap_xmodule( # cdodge: Special case
module.get_html, if module.location.category == 'static_tab':
module, module.get_html = wrap_xmodule(
"xmodule_display.html", 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 = replace_static_urls(
module.get_html, module.get_html,
module.metadata.get('data_dir', module.location.course), module.metadata.get('data_dir', module.location.course),
...@@ -905,7 +920,8 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -905,7 +920,8 @@ def course_info(request, org, course, name, provided_id=None):
'active_tab': 'courseinfo-tab', 'active_tab': 'courseinfo-tab',
'context_course': course_module, 'context_course': course_module,
'url_base' : "/" + org + "/" + course + "/", '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 @expect_json
...@@ -928,13 +944,38 @@ def course_info_updates(request, org, course, provided_id=None): ...@@ -928,13 +944,38 @@ def course_info_updates(request, org, course, provided_id=None):
real_method = request.method real_method = request.method
if request.method == 'GET': 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': 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': elif real_method == 'PUT':
return HttpResponse(json.dumps(course_info_model.update_course_updates(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': elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
return HttpResponse(json.dumps(course_info_model.delete_course_update(location, request.POST, provided_id)), mimetype="application/json") 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 @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1079,7 +1120,10 @@ def create_new_course(request): ...@@ -1079,7 +1120,10 @@ def create_new_course(request):
number = request.POST.get('number') number = request.POST.get('number')
display_name = request.POST.get('display_name') 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 # see if the course already exists
existing_course = None existing_course = None
......
...@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log", ...@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev", logging_env="dev",
tracking_filename="tracking.log", tracking_filename="tracking.log",
dev_env=True, dev_env=True,
debug=True) debug=True,
local_loglevel='ERROR',
console_loglevel='ERROR')
PIPELINE_JS['js-test-source'] = { PIPELINE_JS['js-test-source'] = {
'source_filenames': sum([ '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() { ...@@ -11,6 +11,11 @@ $(document).ready(function() {
$body = $('body'); $body = $('body');
$modal = $('.history-modal'); $modal = $('.history-modal');
$modalCover = $('<div class="modal-cover">'); $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); $body.append($modalCover);
$newComponentItem = $('.new-component-item'); $newComponentItem = $('.new-component-item');
$newComponentTypePicker = $('.new-component'); $newComponentTypePicker = $('.new-component');
...@@ -93,7 +98,7 @@ $(document).ready(function() { ...@@ -93,7 +98,7 @@ $(document).ready(function() {
// section name editing // section name editing
$('.section-name').bind('click', editSectionName); $('.section-name').bind('click', editSectionName);
$('.edit-section-name-cancel').bind('click', cancelEditSectionName); $('.edit-section-name-cancel').bind('click', cancelEditSectionName);
$('.edit-section-name-save').bind('click', saveEditSectionName); // $('.edit-section-name-save').bind('click', saveEditSectionName);
// section date setting // section date setting
$('.set-publish-date').bind('click', setSectionScheduleDate); $('.set-publish-date').bind('click', setSectionScheduleDate);
...@@ -585,33 +590,44 @@ function hideToastMessage(e) { ...@@ -585,33 +590,44 @@ function hideToastMessage(e) {
$(this).closest('.toast-notification').remove(); $(this).closest('.toast-notification').remove();
} }
function addNewSection(e) { function addNewSection(e, isTemplate) {
e.preventDefault(); e.preventDefault();
var $newSection = $($('#new-section-template').html()); var $newSection = $($('#new-section-template').html());
var $cancelButton = $newSection.find('.new-section-name-cancel');
$('.new-courseware-section-button').after($newSection); $('.new-courseware-section-button').after($newSection);
$newSection.find('.new-section-name').focus().select(); $newSection.find('.new-section-name').focus().select();
$newSection.find('.new-section-name-save').bind('click', saveNewSection); $newSection.find('.section-name-form').bind('submit', saveNewSection);
$newSection.find('.new-section-name-cancel').bind('click', cancelNewSection); $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) { function saveNewSection(e) {
e.preventDefault(); e.preventDefault();
parent = $(this).data('parent'); var $saveButton = $(this).find('.new-section-name-save');
template = $(this).data('template'); var parent = $saveButton.data('parent');
var template = $saveButton.data('template');
display_name = $(this).prev('.new-section-name').val(); var display_name = $(this).find('.new-section-name').val();
$.post('/clone_item', $.post('/clone_item', {
{'parent_location' : parent, 'parent_location' : parent,
'template' : template, 'template' : template,
'display_name': display_name, 'display_name': display_name,
}, },
function(data) { function(data) {
if (data.id != undefined) if (data.id != undefined)
location.reload(); location.reload();
}); }
);
} }
function cancelNewSection(e) { function cancelNewSection(e) {
...@@ -619,44 +635,44 @@ function cancelNewSection(e) { ...@@ -619,44 +635,44 @@ function cancelNewSection(e) {
$(this).parents('section.new-section').remove(); $(this).parents('section.new-section').remove();
} }
function addNewCourse(e) { function addNewCourse(e) {
e.preventDefault(); e.preventDefault();
var $newCourse = $($('#new-course-template').html()); var $newCourse = $($('#new-course-template').html());
var $cancelButton = $newCourse.find('.new-course-cancel');
$('.new-course-button').after($newCourse); $('.new-course-button').after($newCourse);
$newCourse.find('.new-course-name').focus().select(); $newCourse.find('.new-course-name').focus().select();
$newCourse.find('.new-course-save').bind('click', saveNewCourse); $newCourse.find('form').bind('submit', saveNewCourse);
$newCourse.find('.new-course-cancel').bind('click', cancelNewCourse); $cancelButton.bind('click', cancelNewCourse);
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
} }
function saveNewCourse(e) { function saveNewCourse(e) {
e.preventDefault(); e.preventDefault();
var $newCourse = $(this).closest('.new-course'); var $newCourse = $(this).closest('.new-course');
var template = $(this).find('.new-course-save').data('template');
template = $(this).data('template'); var org = $newCourse.find('.new-course-org').val();
var number = $newCourse.find('.new-course-number').val();
org = $newCourse.find('.new-course-org').val(); var display_name = $newCourse.find('.new-course-name').val();
number = $newCourse.find('.new-course-number').val();
display_name = $newCourse.find('.new-course-name').val();
if (org == '' || number == '' || display_name == ''){ if (org == '' || number == '' || display_name == ''){
alert('You must specify all fields in order to create a new course.'); alert('You must specify all fields in order to create a new course.');
return; return;
} }
$.post('/create_new_course', $.post('/create_new_course', {
{ 'template' : template, 'template' : template,
'org' : org, 'org' : org,
'number' : number, 'number' : number,
'display_name': display_name, 'display_name': display_name,
}, },
function(data) { function(data) {
if (data.id != undefined) if (data.id != undefined) {
location.reload(); window.location = '/' + data.id.replace(/.*:\/\//, '');
else if (data.ErrMsg != undefined) } else if (data.ErrMsg != undefined) {
alert(data.ErrMsg); alert(data.ErrMsg);
}); }
});
} }
function cancelNewCourse(e) { function cancelNewCourse(e) {
...@@ -672,35 +688,37 @@ function addNewSubsection(e) { ...@@ -672,35 +688,37 @@ function addNewSubsection(e) {
$section.find('.new-subsection-name-input').focus().select(); $section.find('.new-subsection-name-input').focus().select();
var $saveButton = $newSubsection.find('.new-subsection-name-save'); 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('parent', parent)
$saveButton.data('template', $(this).data('template')); $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) { function saveNewSubsection(e) {
e.preventDefault(); e.preventDefault();
parent = $(this).data('parent'); var parent = $(this).find('.new-subsection-name-save').data('parent');
template = $(this).data('template'); 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', $.post('/clone_item', {
{'parent_location' : parent, 'parent_location' : parent,
'template' : template, 'template' : template,
'display_name': display_name, 'display_name': display_name
}, },
function(data) { function(data) {
if (data.id != undefined) { if (data.id != undefined) {
location.reload(); location.reload();
} }
}); }
);
} }
function cancelNewSubsection(e) { function cancelNewSubsection(e) {
...@@ -710,22 +728,30 @@ function cancelNewSubsection(e) { ...@@ -710,22 +728,30 @@ function cancelNewSubsection(e) {
function editSectionName(e) { function editSectionName(e) {
e.preventDefault(); e.preventDefault();
$(this).children('div.section-name-edit').show(); $(this).unbind('click', editSectionName);
$(this).children('span.section-name-span').hide(); $(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) { function cancelEditSectionName(e) {
e.preventDefault(); e.preventDefault();
$(this).parent().hide(); $(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(); e.stopPropagation();
} }
function saveEditSectionName(e) { function saveEditSectionName(e) {
e.preventDefault(); e.preventDefault();
id = $(this).closest("section.courseware-section").data("id"); $(this).closest('.section-name').unbind('click', editSectionName);
display_name = $.trim($(this).prev('.edit-section-name').val());
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); $(this).closest('.courseware-section .section-name').append($spinner);
$spinner.show(); $spinner.show();
...@@ -746,10 +772,10 @@ function saveEditSectionName(e) { ...@@ -746,10 +772,10 @@ function saveEditSectionName(e) {
}).success(function() }).success(function()
{ {
$spinner.delay(250).fadeOut(250); $spinner.delay(250).fadeOut(250);
$_this.parent().siblings('span.section-name-span').html(display_name); $_this.closest('h3').find('.section-name-span').html(display_name).show();
$_this.parent().siblings('span.section-name-span').show(); $_this.hide();
$_this.parent().hide(); $_this.closest('.section-name').bind('click', editSectionName);
e.stopPropagation(); e.stopPropagation();
}); });
} }
......
...@@ -15,7 +15,7 @@ CMS.Models.CourseInfo = Backbone.Model.extend({ ...@@ -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 // course update -- biggest kludge here is the lack of a real id to map updates to originals
CMS.Models.CourseUpdate = Backbone.Model.extend({ CMS.Models.CourseUpdate = Backbone.Model.extend({
defaults: { defaults: {
"date" : $.datepicker.formatDate('MM d', new Date()), "date" : $.datepicker.formatDate('MM d, yy', new Date()),
"content" : "" "content" : ""
} }
}); });
...@@ -29,6 +29,8 @@ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({ ...@@ -29,6 +29,8 @@ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
model : CMS.Models.CourseUpdate 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 @@ ...@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return; if (typeof window.templateLoader == 'function') return;
var templateLoader = { var templateLoader = {
templateVersion: "0.0.3", templateVersion: "0.0.8",
templates: {}, templates: {},
loadRemoteTemplate: function(templateName, filename, callback) { loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) { if (!this.templates[templateName]) {
......
...@@ -13,7 +13,11 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({ ...@@ -13,7 +13,11 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
el: this.$('#course-update-view'), el: this.$('#course-update-view'),
collection: this.model.get('updates') 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; return this;
} }
}); });
...@@ -34,11 +38,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -34,11 +38,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// instantiates an editor template for each update in the collection // instantiates an editor template for each update in the collection
window.templateLoader.loadRemoteTemplate("course_info_update", window.templateLoader.loadRemoteTemplate("course_info_update",
// TODO Where should the template reside? how to use the static.url to create the path? // 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", "/static/client_templates/course_info_update.html",
function (raw_template) { function (raw_template) {
self.template = _.template(raw_template); self.template = _.template(raw_template);
self.render(); self.render();
} }
); );
}, },
...@@ -53,28 +57,47 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -53,28 +57,47 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$(updateEle).append(newEle); $(updateEle).append(newEle);
}); });
this.$el.find(".new-update-form").hide(); this.$el.find(".new-update-form").hide();
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
return this; return this;
}, },
onNew: function(event) { onNew: function(event) {
var self = this;
// create new obj, insert into collection, and render this one ele overriding the hidden attr // create new obj, insert into collection, and render this one ele overriding the hidden attr
var newModel = new CMS.Models.CourseUpdate(); var newModel = new CMS.Models.CourseUpdate();
this.collection.add(newModel, {at : 0}); 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"); var updateEle = this.$el.find("#course-update-list");
$(updateEle).append(newForm); $(updateEle).prepend($newForm);
$(newForm).find(".new-update-form").show(); $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) { onSave: function(event) {
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
targetModel.set({ date : this.dateEntry(event).val(), content : this.contentEntry(event).val() }); console.log(this.contentEntry(event).val());
// push change to display, hide the editor, submit the change targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
$(this.dateDisplay(event)).val(targetModel.get('date')); // push change to display, hide the editor, submit the change
$(this.contentDisplay(event)).val(targetModel.get('content')); this.closeEditor(this);
$(this.editor(event)).hide();
targetModel.save(); targetModel.save();
}, },
...@@ -82,14 +105,31 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -82,14 +105,31 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// change editor contents back to model values and hide the editor // change editor contents back to model values and hide the editor
$(this.editor(event)).hide(); $(this.editor(event)).hide();
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
$(this.dateEntry(event)).val(targetModel.get('date')); this.closeEditor(this, !targetModel.id);
$(this.contentEntry(event)).val(targetModel.get('content'));
}, },
onEdit: function(event) { onEdit: function(event) {
var self = this;
this.$currentPost = $(event.target).closest('li');
this.$currentPost.addClass('editing');
$(this.editor(event)).show(); $(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) { onDelete: function(event) {
// TODO ask for confirmation // TODO ask for confirmation
// remove the dom element and delete the model // remove the dom element and delete the model
...@@ -101,6 +141,24 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -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 // Dereferencing from events to screen elements
eventModel: function(event) { eventModel: function(event) {
...@@ -119,7 +177,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -119,7 +177,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
dateEntry: function(event) { dateEntry: function(event) {
var li = $(event.currentTarget).closest("li"); var li = $(event.currentTarget).closest("li");
if (li) return $(li).find("#date-entry").first(); if (li) return $(li).find(".date").first();
}, },
contentEntry: function(event) { contentEntry: function(event) {
...@@ -135,4 +193,83 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -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 { ...@@ -181,6 +181,11 @@ code {
padding: 20px; padding: 20px;
} }
.details {
margin-bottom: 30px;
font-size: 14px;
}
h4 { h4 {
padding: 6px 14px; padding: 6px 14px;
border-bottom: 1px solid #cbd1db; border-bottom: 1px solid #cbd1db;
...@@ -338,4 +343,29 @@ body.show-wip { ...@@ -338,4 +343,29 @@ body.show-wip {
content: ''; content: '';
@extend .spinner-icon; @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 @@ ...@@ -51,14 +51,14 @@
@include button; @include button;
border: 1px solid $darkGrey; border: 1px solid $darkGrey;
border-radius: 3px; 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; background-color: #dfe5eb;
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset); @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
color: #5d6779; color: #778192;
&:hover { &:hover {
background-color: #f2f6f9; background-color: #f2f6f9;
color: #5d6779; color: #778192;
} }
} }
......
body.updates { .course-info {
h2 { h2 {
margin-bottom: 24px; margin-bottom: 24px;
font-size: 22px; font-size: 22px;
font-weight: 300; 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 { .course-updates {
padding: 30px 40px; padding: 30px 40px;
margin: 0;
li { .update-list > li {
padding: 24px 0 32px; padding: 34px 0 42px;
border-top: 1px solid #cbd1db; border-top: 1px solid #cbd1db;
}
h3 { &.editing {
margin-bottom: 18px; position: relative;
font-size: 14px; z-index: 1001;
font-weight: 700; padding: 0;
color: #646464; border-top: none;
letter-spacing: 1px; border-radius: 3px;
text-transform: uppercase; 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 { .update-contents {
padding-left: 30px;
p { p {
font-size: 14px; font-size: 16px;
line-height: 18px; line-height: 25px;
} }
p + p { p + p {
margin-top: 18px; margin-top: 25px;
}
.primary {
border: 1px solid #ddd;
background: #f6f6f6;
padding: 20px;
} }
} }
.new-update-button { .new-update-button {
@include grey-button; @include blue-button;
display: block; display: block;
text-align: center; text-align: center;
padding: 12px 0; padding: 18px 0;
margin-bottom: 28px; margin-bottom: 28px;
} }
.new-update-form { .new-update-form {
@include edit-box; @include edit-box;
margin-bottom: 24px; margin-bottom: 24px;
padding: 30px;
border: none;
textarea { textarea {
height: 180px; 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 { .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 { h2 {
@include grey-button; font-size: 18px;
display: block; font-weight: 700;
text-align: center;
padding: 12px 0;
margin-bottom: 28px;
} }
li { .edit-button {
margin-bottom: 10px; 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; font-size: 14px;
} }
.new-handout-form { .treeview-handoutsnav li {
@include edit-box; margin-bottom: 12px;
margin-bottom: 24px; }
}
.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 { ...@@ -5,12 +5,7 @@ input.courseware-unit-search-input {
} }
.courseware-overview { .courseware-overview {
.new-courseware-section-button {
@include grey-button;
display: block;
text-align: center;
padding: 12px 0;
}
} }
.courseware-section { .courseware-section {
...@@ -146,18 +141,18 @@ input.courseware-unit-search-input { ...@@ -146,18 +141,18 @@ input.courseware-unit-search-input {
.section-name-edit { .section-name-edit {
input { input {
font-size: 16px; font-size: 16px;
} }
.save-button { .save-button {
@include blue-button; @include blue-button;
padding: 7px 20px 7px; padding: 10px 20px;
margin-right: 5px; margin-right: 5px;
} }
.cancel-button { .cancel-button {
@include white-button; @include white-button;
padding: 7px 20px 7px; padding: 10px 20px;
} }
} }
...@@ -205,7 +200,7 @@ input.courseware-unit-search-input { ...@@ -205,7 +200,7 @@ input.courseware-unit-search-input {
.new-section-name-save, .new-section-name-save,
.new-subsection-name-save { .new-subsection-name-save {
@include blue-button; @include blue-button;
padding: 2px 20px 5px; padding: 6px 20px 8px;
margin: 0 5px; margin: 0 5px;
color: #fff !important; color: #fff !important;
} }
...@@ -213,7 +208,7 @@ input.courseware-unit-search-input { ...@@ -213,7 +208,7 @@ input.courseware-unit-search-input {
.new-section-name-cancel, .new-section-name-cancel,
.new-subsection-name-cancel { .new-subsection-name-cancel {
@include white-button; @include white-button;
padding: 2px 20px 5px; padding: 6px 20px 8px;
color: #8891a1 !important; color: #8891a1 !important;
} }
......
...@@ -89,6 +89,7 @@ ...@@ -89,6 +89,7 @@
.new-course-save { .new-course-save {
@include blue-button; @include blue-button;
// padding: ;
} }
.new-course-cancel { .new-course-cancel {
......
...@@ -137,6 +137,10 @@ ...@@ -137,6 +137,10 @@
height: 11px; height: 11px;
margin-right: 8px; margin-right: 8px;
background: url(../img/plus-icon.png) no-repeat; background: url(../img/plus-icon.png) no-repeat;
&.white {
background: url(../img/plus-icon-white.png) no-repeat;
}
} }
.plus-icon-small { .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 @@ ...@@ -6,6 +6,72 @@
padding: 12px 0; 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 { .static-page-item {
position: relative; position: relative;
margin: 10px 0; margin: 10px 0;
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
} }
.main-column { .main-column {
clear: both;
float: left; float: left;
width: 70%; width: 70%;
} }
...@@ -54,94 +55,11 @@ ...@@ -54,94 +55,11 @@
position: relative; position: relative;
z-index: 10; z-index: 10;
margin: 20px 40px; 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 { &.new-component-item {
padding: 0; padding: 0;
border: 1px solid #8891a1; border: none;
border-radius: 3px; border-radius: 0;
@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);
&.adding { &.adding {
background-color: $blue; background-color: $blue;
...@@ -223,8 +141,63 @@ ...@@ -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 { .xmodule_display {
padding: 10px 20px; padding: 40px 20px 20px;
} }
.component-editor { .component-editor {
......
...@@ -2,11 +2,6 @@ ...@@ -2,11 +2,6 @@
.user-overview { .user-overview {
@extend .window; @extend .window;
padding: 30px 40px; padding: 30px 40px;
.details {
margin-bottom: 20px;
font-size: 14px;
}
} }
.new-user-button { .new-user-button {
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
@import "modal"; @import "modal";
@import "alerts"; @import "alerts";
@import "login"; @import "login";
@import "lms";
@import 'jquery-ui-calendar'; @import 'jquery-ui-calendar';
@import 'content-types'; @import 'content-types';
......
...@@ -9,9 +9,6 @@ ...@@ -9,9 +9,6 @@
<%static:css group='base-style'/> <%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/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('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> <title><%block name="title"></%block></title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
...@@ -36,9 +33,8 @@ ...@@ -36,9 +33,8 @@
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script> <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.tablednd.js')}"></script>
<script src="${static.url('js/vendor/jquery.form.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/CodeMirror/htmlmixed.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/css.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
document.write('\x3Cscript type="text/javascript" src="' + document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>'); document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
......
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
<a href="#" class="cancel-button">Cancel</a> <a href="#" class="cancel-button">Cancel</a>
</div> </div>
<div class="component-actions"> <div class="component-actions">
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a> <a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a> <a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a>
</div> </div>
<a href="#" class="drag-handle"></a> <a href="#" class="drag-handle"></a>
${preview} ${preview}
\ No newline at end of file
...@@ -3,29 +3,39 @@ ...@@ -3,29 +3,39 @@
<!-- TODO decode course # from context_course into title --> <!-- TODO decode course # from context_course into title -->
<%block name="title">Course Info</%block> <%block name="title">Course Info</%block>
<%block name="bodyclass">course-info</%block>
<%block name="jsextra"> <%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script> <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/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> <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"> <script type="text/javascript" charset="utf-8">
$(document).ready(function(){ $(document).ready(function(){
var course_updates = new CMS.Models.CourseUpdateCollection();
var course_updates = new CMS.Models.CourseUpdateCollection(); course_updates.reset(${course_updates|n});
course_updates.reset(${course_updates|n}); course_updates.urlbase = '${url_base}';
course_updates.urlbase = '${url_base}';
var course_handouts = new CMS.Models.ModuleInfo({
var editor = new CMS.Views.CourseInfoEdit({ id: '${handouts_location}'
el: $('.main-wrapper'), });
model : new CMS.Models.CourseInfo({ course_handouts.urlbase = '${url_base}';
courseId : '${context_course.location}',
updates : course_updates, var editor = new CMS.Views.CourseInfoEdit({
// FIXME add handouts el: $('.main-wrapper'),
handouts : null}) model : new CMS.Models.CourseInfo({
}); courseId : '${context_course.location}',
editor.render(); updates : course_updates,
}); handouts : course_handouts
})
});
editor.render();
});
</script> </script>
</%block> </%block>
...@@ -33,16 +43,18 @@ ...@@ -33,16 +43,18 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h1>Course Info</h1> <h1>Course Info</h1>
<div class="main-column"> <div class="course-info-wrapper">
<div class="unit-body window" id="course-update-view"> <div class="main-column window">
<h2>Updates</h2> <article class="course-updates" id="course-update-view">
<a href="#" class="new-update-button">New Update</a> <h2>Course Updates & News</h2>
<ol class="update-list" id="course-update-list"></ol> <a href="#" class="new-update-button">New Update</a>
<!-- probably replace w/ a vertical where each element of the vertical is a separate update w/ a date and html field --> <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> <div class="sidebar window course-handouts" id="course-handouts-view"></div>
<div class="sidebar window"> </div>
</div> </div>
</div> </div>
</div> </div>
</%block> </%block>
\ No newline at end of file
...@@ -20,23 +20,24 @@ ...@@ -20,23 +20,24 @@
<div> <div>
<h1>Static Tabs</h1> <h1>Static Tabs</h1>
</div> </div>
<div class="main-column"> <article class="unit-body window">
<article class="unit-body window"> <div class="details">
<div class="tab-list"> <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>
<ol class='components'> </div>
% for id in components: <div class="tab-list">
<li class="component" data-id="${id}"/> <ol class='components'>
% endfor % for id in components:
<li class="component" data-id="${id}"/>
<li class="new-component-item"> % endfor
<a href="#" class="new-component-button new-tab">
<span class="plus-icon"></span>New Tab <li class="new-component-item">
</a> <a href="#" class="new-button big new-tab">
</li> <span class="plus-icon"></span>New Tab
</ol> </a>
</div> </li>
</article> </ol>
</div> </div>
</article>
</div> </div>
</div> </div>
</%block> </%block>
\ No newline at end of file
...@@ -22,14 +22,14 @@ ...@@ -22,14 +22,14 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<a href="#" class="new-course-save" data-template="${new_course_template}">Save</a> <input type="submit" value="Save" class="new-course-save" data-template="${new_course_template}" />
<a href="#" class="new-course-cancel">Cancel</a> <input type="button" value="Cancel" class="new-course-cancel" />
</div> </div>
</form> </form>
</div> </div>
</section> </section>
</script> </script>
</%block> </%block>
<%block name="content"> <%block name="content">
<div class="main-wrapper"> <div class="main-wrapper">
......
...@@ -12,10 +12,10 @@ ...@@ -12,10 +12,10 @@
<%namespace name="units" file="widgets/units.html" /> <%namespace name="units" file="widgets/units.html" />
<%block name="jsextra"> <%block name="jsextra">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" /> <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/jquery.timepicker.js')}"></script>
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script> <script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
<script src="${static.url('js/vendor/date.js')}"></script> <script src="${static.url('js/vendor/date.js')}"></script>
</%block> </%block>
<%block name="header_extras"> <%block name="header_extras">
...@@ -24,7 +24,33 @@ ...@@ -24,7 +24,33 @@
<header> <header>
<a href="#" class="expand-collapse-icon collapse"></a> <a href="#" class="expand-collapse-icon collapse"></a>
<div class="item-details"> <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> </div>
</header> </header>
</section> </section>
...@@ -33,14 +59,14 @@ ...@@ -33,14 +59,14 @@
<script type="text/template" id="new-subsection-template"> <script type="text/template" id="new-subsection-template">
<li class="branch collapsed"> <li class="branch collapsed">
<div class="section-item editing"> <div class="section-item editing">
<div> <form class="new-subsection-form">
<span class="folder-icon"></span> <span class="folder-icon"></span>
<span class="subsection-name"> <span class="subsection-name">
<input type="text" value="New Subsection" class="new-subsection-name-input" /> <input type="text" value="New Subsection" class="new-subsection-name-input" />
</span> </span>
<a href="#" class="new-subsection-name-save">Save</a> <input type="submit" value="Save" class="new-subsection-name-save" />
<a href="#" class="new-subsection-name-cancel">Cancel</a> <input type="button" value="Cancel" class="new-subsection-name-cancel" />
</div> </form>
</div> </div>
<ol> <ol>
<li> <li>
...@@ -75,7 +101,7 @@ ...@@ -75,7 +101,7 @@
<h1>Courseware</h1> <h1>Courseware</h1>
<div class="page-actions"></div> <div class="page-actions"></div>
<article class="courseware-overview" data-course-id="${context_course.location.url()}"> <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: % for section in sections:
<section class="courseware-section branch" data-id="${section.location}"> <section class="courseware-section branch" data-id="${section.location}">
<header> <header>
...@@ -83,10 +109,11 @@ ...@@ -83,10 +109,11 @@
<div class="item-details" data-id="${section.location}"> <div class="item-details" data-id="${section.location}">
<h3 class="section-name"> <h3 class="section-name">
<span class="section-name-span">${section.display_name}</span> <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"/> <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> <input type="submit" class="save-button edit-section-name-save" value="Save" />
</div> <input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form>
</h3> </h3>
<div class="section-published-date"> <div class="section-published-date">
<% <%
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
<li class="component" data-id="${id}"/> <li class="component" data-id="${id}"/>
% endfor % endfor
<li class="new-component-item"> <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 <span class="plus-icon"></span>New Component
</a> </a>
<div class="new-component"> <div class="new-component">
......
<section class="xmodule_display xmodule_${class_}" data-type="${module_name}">
${display_name}
</section>
...@@ -45,6 +45,10 @@ urlpatterns = ('', ...@@ -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'^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'), 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 # temporary landing page for a course
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'), 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): ...@@ -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_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
def replace_url(static_url): def replace_url(static_url):
return replace(static_url, staticfiles_prefix, course_namespace = course_namespace) return replace(static_url, staticfiles_prefix, course_namespace = course_namespace)
......
...@@ -8,7 +8,7 @@ def expect_json(view_function): ...@@ -8,7 +8,7 @@ def expect_json(view_function):
def expect_json_with_cloned_request(request, *args, **kwargs): def expect_json_with_cloned_request(request, *args, **kwargs):
# cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information # 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 # 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 = copy.copy(request)
cloned_request.POST = cloned_request.POST.copy() cloned_request.POST = cloned_request.POST.copy()
cloned_request.POST.update(json.loads(request.body)) cloned_request.POST.update(json.loads(request.body))
......
...@@ -21,6 +21,7 @@ def wrap_xmodule(get_html, module, template, context=None): ...@@ -21,6 +21,7 @@ def wrap_xmodule(get_html, module, template, context=None):
module: An XModule module: An XModule
template: A template that takes the variables: template: A template that takes the variables:
content: the results of get_html, content: the results of get_html,
display_name: the display name of the xmodule, if available (None otherwise)
class_: the module class name class_: the module class name
module_name: the js_module_name of the module module_name: the js_module_name of the module
""" """
...@@ -31,6 +32,7 @@ def wrap_xmodule(get_html, module, template, context=None): ...@@ -31,6 +32,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html(): def _get_html():
context.update({ context.update({
'content': get_html(), 'content': get_html(),
'display_name' : module.metadata.get('display_name') if module.metadata is not None else None,
'class_': module.__class__.__name__, 'class_': module.__class__.__name__,
'module_name': module.js_module_name 'module_name': module.js_module_name
}) })
......
*/jasmine_test_runner.html
...@@ -3,6 +3,7 @@ import platform ...@@ -3,6 +3,7 @@ import platform
import sys import sys
from logging.handlers import SysLogHandler from logging.handlers import SysLogHandler
LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
def get_logger_config(log_dir, def get_logger_config(log_dir,
logging_env="no_env", logging_env="no_env",
...@@ -11,7 +12,8 @@ def get_logger_config(log_dir, ...@@ -11,7 +12,8 @@ def get_logger_config(log_dir,
dev_env=False, dev_env=False,
syslog_addr=None, syslog_addr=None,
debug=False, debug=False,
local_loglevel='INFO'): local_loglevel='INFO',
console_loglevel=None):
""" """
...@@ -30,9 +32,12 @@ def get_logger_config(log_dir, ...@@ -30,9 +32,12 @@ def get_logger_config(log_dir,
""" """
# Revert to INFO if an invalid string is passed in # 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' 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] hostname = platform.node().split(".")[0]
syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s " syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s "
"[{hostname} %(process)d] [%(filename)s:%(lineno)d] " "[{hostname} %(process)d] [%(filename)s:%(lineno)d] "
...@@ -55,7 +60,7 @@ def get_logger_config(log_dir, ...@@ -55,7 +60,7 @@ def get_logger_config(log_dir,
}, },
'handlers': { 'handlers': {
'console': { 'console': {
'level': 'DEBUG' if debug else 'INFO', 'level': console_loglevel,
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'standard', 'formatter': 'standard',
'stream': sys.stdout, '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): ...@@ -347,7 +347,7 @@ class CapaModule(XModule):
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>" 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 # 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): def handle_ajax(self, dispatch, get):
''' '''
...@@ -451,7 +451,7 @@ class CapaModule(XModule): ...@@ -451,7 +451,7 @@ class CapaModule(XModule):
new_answers = dict() new_answers = dict()
for answer_id in answers: for answer_id in answers:
try: 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: except TypeError:
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id])) log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
new_answer = {answer_id: answers[answer_id]} new_answer = {answer_id: answers[answer_id]}
......
...@@ -47,6 +47,11 @@ class StaticContent(object): ...@@ -47,6 +47,11 @@ class StaticContent(object):
return None return None
@staticmethod @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): def get_id_from_location(location):
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course, return { 'tag':location.tag, 'org' : location.org, 'course' : location.course,
'category' : location.category, 'name' : location.name, 'category' : location.category, 'name' : location.name,
......
import abc import abc
import inspect import inspect
import json
import logging import logging
import random import random
import sys import sys
...@@ -66,17 +65,27 @@ def grader_from_conf(conf): ...@@ -66,17 +65,27 @@ def grader_from_conf(conf):
for subgraderconf in conf: for subgraderconf in conf:
subgraderconf = subgraderconf.copy() subgraderconf = subgraderconf.copy()
weight = subgraderconf.pop("weight", 0) 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: try:
if 'min_count' in subgraderconf: if 'min_count' in subgraderconf:
#This is an AssignmentFormatGrader #This is an AssignmentFormatGrader
subgrader_class = AssignmentFormatGrader subgrader_class = AssignmentFormatGrader
elif 'name' in subgraderconf: elif name in subgraderconf:
#This is an SingleSectionGrader #This is an SingleSectionGrader
subgrader_class = SingleSectionGrader subgrader_class = SingleSectionGrader
else: else:
raise ValueError("Configuration has no appropriate grader class.") raise ValueError("Configuration has no appropriate grader class.")
bad_args = invalid_args(subgrader_class.__init__, subgraderconf) 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: if len(bad_args) > 0:
log.warning("Invalid arguments for a subgrader: %s", bad_args) log.warning("Invalid arguments for a subgrader: %s", bad_args)
for key in 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', -> ...@@ -8,25 +8,43 @@ describe 'Problem', ->
MathJax.Hub.getAllJax.andReturn [@stubbedJax] MathJax.Hub.getAllJax.andReturn [@stubbedJax]
window.update_schematics = -> 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' loadFixtures 'problem.html'
spyOn Logger, 'log' spyOn Logger, 'log'
spyOn($.fn, 'load').andCallFake (url, callback) -> spyOn($.fn, 'load').andCallFake (url, callback) ->
$(@).html readFixtures('problem_content.html') $(@).html readFixtures('problem_content.html')
callback() callback()
jasmine.stubRequests()
describe 'constructor', -> describe 'constructor', ->
beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/"
it 'set the element', -> it 'set the element from html', ->
expect(@problem.el).toBe '#problem_1' @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', -> describe 'bind', ->
beforeEach -> beforeEach ->
spyOn window, 'update_schematics' spyOn window, 'update_schematics'
MathJax.Hub.getAllJax.andReturn [@stubbedJax] MathJax.Hub.getAllJax.andReturn [@stubbedJax]
@problem = new Problem 1, "problem_1", "/problem/url/" @problem = new Problem($('.xmodule_display'))
it 'set mathjax typeset', -> it 'set mathjax typeset', ->
expect(MathJax.Hub.Queue).toHaveBeenCalled() expect(MathJax.Hub.Queue).toHaveBeenCalled()
...@@ -38,7 +56,7 @@ describe 'Problem', -> ...@@ -38,7 +56,7 @@ describe 'Problem', ->
expect($('section.action input:button')).toHandleWith 'click', @problem.refreshAnswers expect($('section.action input:button')).toHandleWith 'click', @problem.refreshAnswers
it 'bind the check button', -> 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', -> it 'bind the reset button', ->
expect($('section.action input.reset')).toHandleWith 'click', @problem.reset expect($('section.action input.reset')).toHandleWith 'click', @problem.reset
...@@ -60,7 +78,7 @@ describe 'Problem', -> ...@@ -60,7 +78,7 @@ describe 'Problem', ->
describe 'render', -> describe 'render', ->
beforeEach -> beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/" @problem = new Problem($('.xmodule_display'))
@bind = @problem.bind @bind = @problem.bind
spyOn @problem, 'bind' spyOn @problem, 'bind'
...@@ -86,9 +104,13 @@ describe 'Problem', -> ...@@ -86,9 +104,13 @@ describe 'Problem', ->
it 're-bind the content', -> it 're-bind the content', ->
expect(@problem.bind).toHaveBeenCalled() expect(@problem.bind).toHaveBeenCalled()
describe 'check_fd', ->
xit 'should have specs written for this functionality', ->
expect(false)
describe 'check', -> describe 'check', ->
beforeEach -> beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/" @problem = new Problem($('.xmodule_display'))
@problem.answers = 'foo=1&bar=2' @problem.answers = 'foo=1&bar=2'
it 'log the problem_check event', -> it 'log the problem_check event', ->
...@@ -98,30 +120,34 @@ describe 'Problem', -> ...@@ -98,30 +120,34 @@ describe 'Problem', ->
it 'submit the answer for check', -> it 'submit the answer for check', ->
spyOn $, 'postWithPrefix' spyOn $, 'postWithPrefix'
@problem.check() @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', -> describe 'when the response is correct', ->
it 'call render with returned content', -> 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() @problem.check()
expect(@problem.el.html()).toEqual 'Correct!' expect(@problem.el.html()).toEqual 'Correct!'
describe 'when the response is incorrect', -> describe 'when the response is incorrect', ->
it 'call render with returned content', -> 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() @problem.check()
expect(@problem.el.html()).toEqual 'Correct!' expect(@problem.el.html()).toEqual 'Incorrect!'
describe 'when the response is undetermined', -> describe 'when the response is undetermined', ->
it 'alert the response', -> it 'alert the response', ->
spyOn window, 'alert' 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() @problem.check()
expect(window.alert).toHaveBeenCalledWith 'Number Only!' expect(window.alert).toHaveBeenCalledWith 'Number Only!'
describe 'reset', -> describe 'reset', ->
beforeEach -> beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/" @problem = new Problem($('.xmodule_display'))
it 'log the problem_reset event', -> it 'log the problem_reset event', ->
@problem.answers = 'foo=1&bar=2' @problem.answers = 'foo=1&bar=2'
...@@ -131,7 +157,8 @@ describe 'Problem', -> ...@@ -131,7 +157,8 @@ describe 'Problem', ->
it 'POST to the problem reset page', -> it 'POST to the problem reset page', ->
spyOn $, 'postWithPrefix' spyOn $, 'postWithPrefix'
@problem.reset() @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', -> it 'render the returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
...@@ -141,7 +168,7 @@ describe 'Problem', -> ...@@ -141,7 +168,7 @@ describe 'Problem', ->
describe 'show', -> describe 'show', ->
beforeEach -> 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" />' @problem.el.prepend '<div id="answer_1_1" /><div id="answer_1_2" />'
describe 'when the answer has not yet shown', -> describe 'when the answer has not yet shown', ->
...@@ -150,12 +177,14 @@ describe 'Problem', -> ...@@ -150,12 +177,14 @@ describe 'Problem', ->
it 'log the problem_show event', -> it 'log the problem_show event', ->
@problem.show() @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', -> it 'fetch the answers', ->
spyOn $, 'postWithPrefix' spyOn $, 'postWithPrefix'
@problem.show() @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', -> it 'show the answers', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
...@@ -220,7 +249,7 @@ describe 'Problem', -> ...@@ -220,7 +249,7 @@ describe 'Problem', ->
describe 'save', -> describe 'save', ->
beforeEach -> beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/" @problem = new Problem($('.xmodule_display'))
@problem.answers = 'foo=1&bar=2' @problem.answers = 'foo=1&bar=2'
it 'log the problem_save event', -> it 'log the problem_save event', ->
...@@ -230,7 +259,8 @@ describe 'Problem', -> ...@@ -230,7 +259,8 @@ describe 'Problem', ->
it 'POST to save problem', -> it 'POST to save problem', ->
spyOn $, 'postWithPrefix' spyOn $, 'postWithPrefix'
@problem.save() @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', -> it 'alert to the user', ->
spyOn window, 'alert' spyOn window, 'alert'
...@@ -240,7 +270,7 @@ describe 'Problem', -> ...@@ -240,7 +270,7 @@ describe 'Problem', ->
describe 'refreshMath', -> describe 'refreshMath', ->
beforeEach -> beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/" @problem = new Problem($('.xmodule_display'))
$('#input_example_1').val 'E=mc^2' $('#input_example_1').val 'E=mc^2'
@problem.refreshMath target: $('#input_example_1').get(0) @problem.refreshMath target: $('#input_example_1').get(0)
...@@ -250,7 +280,7 @@ describe 'Problem', -> ...@@ -250,7 +280,7 @@ describe 'Problem', ->
describe 'updateMathML', -> describe 'updateMathML', ->
beforeEach -> beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/" @problem = new Problem($('.xmodule_display'))
@stubbedJax.root.toMathML.andReturn '<MathML>' @stubbedJax.root.toMathML.andReturn '<MathML>'
describe 'when there is no exception', -> describe 'when there is no exception', ->
...@@ -270,7 +300,7 @@ describe 'Problem', -> ...@@ -270,7 +300,7 @@ describe 'Problem', ->
describe 'refreshAnswers', -> describe 'refreshAnswers', ->
beforeEach -> beforeEach ->
@problem = new Problem 1, "problem_1", "/problem/url/" @problem = new Problem($('.xmodule_display'))
@problem.el.html ''' @problem.el.html '''
<textarea class="CodeMirror" /> <textarea class="CodeMirror" />
<input id="input_1_1" name="input_1_1" class="schematic" value="one" /> <input id="input_1_1" name="input_1_1" class="schematic" value="one" />
...@@ -293,3 +323,6 @@ describe 'Problem', -> ...@@ -293,3 +323,6 @@ describe 'Problem', ->
it 'serialize all answers', -> it 'serialize all answers', ->
@problem.refreshAnswers() @problem.refreshAnswers()
expect(@problem.answers).toEqual "input_1_1=one&input_1_2=two" 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 ...@@ -3,6 +3,7 @@ class @Video
@el = $(element).find('.video') @el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '') @id = @el.attr('id').replace(/video_/, '')
@caption_data_dir = @el.data('caption-data-dir') @caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true" @show_captions = @el.data('show-captions') == "true"
window.player = null window.player = null
@el = $("#video_#{@id}") @el = $("#video_#{@id}")
......
...@@ -10,7 +10,7 @@ class @VideoCaption extends Subview ...@@ -10,7 +10,7 @@ class @VideoCaption extends Subview
.bind('DOMMouseScroll', @onMovement) .bind('DOMMouseScroll', @onMovement)
captionURL: -> captionURL: ->
"/static/#{@captionDataDir}/subs/#{@youtubeId}.srt.sjson" "#{@captionAssetPath}#{@youtubeId}.srt.sjson"
render: -> render: ->
# TODO: make it so you can have a video with no captions. # TODO: make it so you can have a video with no captions.
......
...@@ -31,7 +31,7 @@ class @VideoPlayer extends Subview ...@@ -31,7 +31,7 @@ class @VideoPlayer extends Subview
el: @el el: @el
youtubeId: @video.youtubeId('1.0') youtubeId: @video.youtubeId('1.0')
currentSpeed: @currentSpeed() currentSpeed: @currentSpeed()
captionDataDir: @video.caption_data_dir captionAssetPath: @video.caption_asset_path
unless onTouchBasedDevice() unless onTouchBasedDevice()
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls') @volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() @speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
......
...@@ -153,7 +153,7 @@ class Location(_LocationBase): ...@@ -153,7 +153,7 @@ class Location(_LocationBase):
def check(val, regexp): def check(val, regexp):
if val is not None and regexp.search(val) is not None: if val is not None and regexp.search(val) is not None:
log.debug('invalid characters val="%s", list_="%s"' % (val, list_)) log.debug('invalid characters val="%s", list_="%s"' % (val, list_))
raise InvalidLocationError(location) raise InvalidLocationError("Invalid characters in '%s'." % (val))
list_ = list(list_) list_ = list(list_)
for val in list_[:4] + [list_[5]]: for val in list_[:4] + [list_[5]]:
......
...@@ -53,6 +53,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -53,6 +53,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
self.unnamed = defaultdict(int) # category -> num of new url_names for that category 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.used_names = defaultdict(set) # category -> set of used url_names
self.org, self.course, self.url_name = course_id.split('/') 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 self.load_error_modules = load_error_modules
def process_xml(xml): def process_xml(xml):
...@@ -303,7 +305,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -303,7 +305,7 @@ class XMLModuleStore(ModuleStoreBase):
try: try:
course_descriptor = self.load_course(course_dir, errorlog.tracker) course_descriptor = self.load_course(course_dir, errorlog.tracker)
except Exception as e: 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) log.exception(msg)
errorlog.tracker(msg) errorlog.tracker(msg)
...@@ -337,7 +339,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -337,7 +339,7 @@ class XMLModuleStore(ModuleStoreBase):
with open(policy_path) as f: with open(policy_path) as f:
return json.load(f) return json.load(f)
except (IOError, ValueError) as err: 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) tracker(msg)
log.warning(msg + " " + str(err)) log.warning(msg + " " + str(err))
return {} return {}
...@@ -455,10 +457,18 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -455,10 +457,18 @@ class XMLModuleStore(ModuleStoreBase):
slug = os.path.splitext(os.path.basename(filepath))[0] slug = os.path.splitext(os.path.basename(filepath))[0]
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug) loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc}) 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 module.metadata['data_dir'] = course_dir
self.modules[course_descriptor.id][module.location] = module self.modules[course_descriptor.id][module.location] = module
except Exception, e: 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): def get_instance(self, course_id, location, depth=0):
""" """
......
...@@ -11,12 +11,12 @@ from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX ...@@ -11,12 +11,12 @@ from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
log = logging.getLogger(__name__) 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 = {} remap_dict = {}
# now import all static assets # 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 dirname, dirnames, filenames in os.walk(static_dir):
for filename in filenames: for filename in filenames:
...@@ -24,6 +24,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ ...@@ -24,6 +24,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
try: try:
content_path = os.path.join(dirname, filename) content_path = os.path.join(dirname, filename)
fullname_with_subpath = content_path.replace(static_dir, '') # strip away leading path from the name 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) content_loc = StaticContent.compute_location(target_location_namespace.org, target_location_namespace.course, fullname_with_subpath)
mime_type = mimetypes.guess_type(filename)[0] mime_type = mimetypes.guess_type(filename)[0]
...@@ -88,7 +90,7 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic ...@@ -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, def import_from_xml(store, data_dir, course_dirs=None,
default_class='xmodule.raw_module.RawDescriptor', 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, Import the specified xml data_dir into the "store" modulestore,
using org and course as the location org and course. using org and course as the location org and course.
...@@ -125,8 +127,11 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -125,8 +127,11 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_location = module.location course_location = module.location
if static_content_store is not None: 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, 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(): for module in module_store.modules[course_id].itervalues():
...@@ -159,6 +164,16 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -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. # HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
module.metadata['hide_progress_tab'] = True 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 # 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 # 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') 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, ...@@ -192,7 +207,6 @@ def import_from_xml(store, data_dir, course_dirs=None,
store.update_item(module.location, module_data) store.update_item(module.location, module_data)
if 'children' in module.definition: if 'children' in module.definition:
store.update_children(module.location, module.definition['children']) store.update_children(module.location, module.definition['children'])
...@@ -200,6 +214,100 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -200,6 +214,100 @@ def import_from_xml(store, data_dir, course_dirs=None,
# inherited metadata everywhere. # inherited metadata everywhere.
store.update_metadata(module.location, dict(module.own_metadata)) 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): ...@@ -127,8 +127,10 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
for child in xml_object: for child in xml_object:
try: try:
children.append(system.process_xml(etree.tostring(child)).location.url()) children.append(system.process_xml(etree.tostring(child)).location.url())
except: except Exception, e:
log.exception("Unable to load child when parsing Sequence. Continuing...") log.exception("Unable to load child when parsing Sequence. Continuing...")
if system.error_tracker is not None:
system.error_tracker("ERROR: " + str(e))
continue continue
return {'children': children} return {'children': children}
......
...@@ -3,7 +3,7 @@ from xmodule.raw_module import RawDescriptor ...@@ -3,7 +3,7 @@ from xmodule.raw_module import RawDescriptor
from lxml import etree from lxml import etree
from mako.template import Template from mako.template import Template
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import logging
class CustomTagModule(XModule): class CustomTagModule(XModule):
""" """
...@@ -61,7 +61,7 @@ class CustomTagDescriptor(RawDescriptor): ...@@ -61,7 +61,7 @@ class CustomTagDescriptor(RawDescriptor):
# cdodge: look up the template as a module # cdodge: look up the template as a module
template_loc = self.location._replace(category='custom_tag_template', name=template_name) 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_module_data = template_module.definition['data']
template = Template(template_module_data) template = Template(template_module_data)
return template.render(**params) return template.render(**params)
......
...@@ -6,6 +6,9 @@ from pkg_resources import resource_string, resource_listdir ...@@ -6,6 +6,9 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor 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__) log = logging.getLogger(__name__)
...@@ -93,6 +96,13 @@ class VideoModule(XModule): ...@@ -93,6 +96,13 @@ class VideoModule(XModule):
return self.youtube return self.youtube
def get_html(self): 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', { return self.system.render_template('video.html', {
'streams': self.video_list(), 'streams': self.video_list(),
'id': self.location.html_id(), 'id': self.location.html_id(),
...@@ -102,6 +112,7 @@ class VideoModule(XModule): ...@@ -102,6 +112,7 @@ class VideoModule(XModule):
'display_name': self.display_name, 'display_name': self.display_name,
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem # TODO (cpennington): This won't work when we move to data that isn't on the filesystem
'data_dir': self.metadata['data_dir'], 'data_dir': self.metadata['data_dir'],
'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions 'show_captions': self.show_captions
}) })
......
...@@ -108,7 +108,20 @@ class HTMLSnippet(object): ...@@ -108,7 +108,20 @@ class HTMLSnippet(object):
All of these will be loaded onto the page in the CMS 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 @classmethod
def get_css(cls): 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): ...@@ -148,7 +148,7 @@ def get_course_about_section(course, section_key):
request = get_request_for_thread() request = get_request_for_thread()
loc = course.location._replace(category='about', name=section_key) 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 = '' html = ''
...@@ -186,8 +186,7 @@ def get_course_info_section(request, cache, course, section_key): ...@@ -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) 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 = '' html = ''
if course_module is not None: if course_module is not None:
...@@ -196,7 +195,6 @@ def get_course_info_section(request, cache, course, section_key): ...@@ -196,7 +195,6 @@ def get_course_info_section(request, cache, course, section_key):
return html return html
# TODO: Fix this such that these are pulled in as extra course-specific tabs. # 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 # arjun will address this by the end of October if no one does so prior to
# then. # then.
...@@ -222,7 +220,7 @@ def get_course_syllabus_section(course, section_key): ...@@ -222,7 +220,7 @@ def get_course_syllabus_section(course, section_key):
filepath = find_file(fs, dirs, section_key + ".html") filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile: with fs.open(filepath) as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'), return replace_urls(htmlFile.read().decode('utf-8'),
course.metadata['data_dir']) course.metadata['data_dir'], course_namespace=course.location)
except ResourceNotFoundError: except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format( log.exception("Missing syllabus section {key} in course {url}".format(
key=section_key, url=course.location.url())) key=section_key, url=course.location.url()))
......
...@@ -115,7 +115,7 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -115,7 +115,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
return chapters 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, Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none 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 ...@@ -136,7 +136,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
if possible. If not possible, return None. if possible. If not possible, return None.
""" """
try: 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: except ItemNotFoundError:
if not not_found_ok: if not not_found_ok:
log.exception("Error in get_module") log.exception("Error in get_module")
...@@ -146,7 +146,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio ...@@ -146,7 +146,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
log.exception("Error in get_module") log.exception("Error in get_module")
return None 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. 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 ...@@ -261,8 +261,13 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
# Make an error module # Make an error module
return err_descriptor.xmodule_constructor(system)(None, None) 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( 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 '', module.metadata['data_dir'] if 'data_dir' in module.metadata else '',
course_namespace = module.location._replace(category=None, name=None)) course_namespace = module.location._replace(category=None, name=None))
......
...@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log", ...@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev", logging_env="dev",
tracking_filename="tracking.log", tracking_filename="tracking.log",
dev_env=True, dev_env=True,
debug=True) debug=True,
local_loglevel='ERROR',
console_loglevel='ERROR')
PIPELINE_JS['js-test-source'] = { PIPELINE_JS['js-test-source'] = {
'source_filenames': sum([ 'source_filenames': sum([
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<h2> ${display_name} </h2> <h2> ${display_name} </h2>
% endif % 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"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <section class="video-player">
......
...@@ -3,6 +3,8 @@ require 'tempfile' ...@@ -3,6 +3,8 @@ require 'tempfile'
require 'net/http' require 'net/http'
require 'launchy' require 'launchy'
require 'colorize' require 'colorize'
require 'erb'
require 'tempfile'
# Build Constants # Build Constants
REPO_ROOT = File.dirname(__FILE__) REPO_ROOT = File.dirname(__FILE__)
...@@ -47,7 +49,7 @@ def django_for_jasmine(system, django_reload) ...@@ -47,7 +49,7 @@ def django_for_jasmine(system, django_reload)
end end
django_pid = fork do 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 end
jasmine_url = 'http://localhost:12345/_jasmine/' jasmine_url = 'http://localhost:12345/_jasmine/'
up = false up = false
...@@ -79,6 +81,31 @@ def django_for_jasmine(system, django_reload) ...@@ -79,6 +81,31 @@ def django_for_jasmine(system, django_reload)
end end
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) def report_dir_path(dir)
return File.join(REPORT_DIR, dir.to_s) return File.join(REPORT_DIR, dir.to_s)
end end
...@@ -126,22 +153,6 @@ end ...@@ -126,22 +153,6 @@ end
end end
task :pylint => "pylint_#{system}" 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 end
$failed_tests = 0 $failed_tests = 0
...@@ -210,6 +221,23 @@ TEST_TASK_DIRS = [] ...@@ -210,6 +221,23 @@ TEST_TASK_DIRS = []
end end
end 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 end
desc "Reset the relational database used by django. WARNING: this will delete all of your existing users" 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| ...@@ -245,6 +273,22 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
sh("nosetests #{lib}") sh("nosetests #{lib}")
end 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 end
task :report_dirs task :report_dirs
...@@ -364,6 +408,20 @@ namespace :cms do ...@@ -364,6 +408,20 @@ namespace :cms do
end end
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" desc "Build a properties file used to trigger autodeploy builds"
task :autodeploy_properties do task :autodeploy_properties do
File.open("autodeploy.properties", "w") do |file| File.open("autodeploy.properties", "w") do |file|
......
...@@ -55,3 +55,4 @@ dogstatsd-python ...@@ -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. # Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs.
# MySQL-python # MySQL-python
sphinx 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