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
This commit is contained in:
@@ -4,7 +4,6 @@ from xmodule.modulestore.django import modulestore
|
||||
from lxml import etree
|
||||
import re
|
||||
from django.http import HttpResponseBadRequest
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
## TODO store as array of { date, content } and override course_info_module.definition_from_xml
|
||||
## This should be in a class which inherits from XmlDescriptor
|
||||
@@ -14,10 +13,10 @@ def get_course_updates(location):
|
||||
[{id : location.url() + idx to make unique, date : string, content : html string}]
|
||||
"""
|
||||
try:
|
||||
course_updates = get_modulestore(location).get_item(location)
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
template = Location(['i4x', 'edx', "templates", 'course_info', "Empty"])
|
||||
course_updates = get_modulestore(location).clone_item(template, Location(location))
|
||||
course_updates = modulestore('direct').clone_item(template, Location(location))
|
||||
|
||||
# current db rep: {"_id" : locationjson, "definition" : { "data" : "<ol>[<li><h2>date</h2>content</li>]</ol>"} "metadata" : ignored}
|
||||
location_base = course_updates.location.url()
|
||||
@@ -54,7 +53,7 @@ def update_course_updates(location, update, passed_id=None):
|
||||
into the html structure.
|
||||
"""
|
||||
try:
|
||||
course_updates = get_modulestore(location).get_item(location)
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest
|
||||
|
||||
@@ -94,13 +93,13 @@ def update_course_updates(location, update, passed_id=None):
|
||||
date_element = etree.SubElement(element, "h2")
|
||||
date_element.text = update['date']
|
||||
if new_html_parsed is not None:
|
||||
element[1] = new_html_parsed
|
||||
element.append(new_html_parsed)
|
||||
else:
|
||||
date_element.tail = update['content']
|
||||
|
||||
# update db record
|
||||
course_updates.definition['data'] = etree.tostring(course_html_parsed)
|
||||
get_modulestore(location).update_item(location, course_updates.definition['data'])
|
||||
modulestore('direct').update_item(location, course_updates.definition['data'])
|
||||
|
||||
return {"id" : passed_id,
|
||||
"date" : update['date'],
|
||||
@@ -115,7 +114,7 @@ def delete_course_update(location, update, passed_id):
|
||||
return HttpResponseBadRequest
|
||||
|
||||
try:
|
||||
course_updates = get_modulestore(location).get_item(location)
|
||||
course_updates = modulestore('direct').get_item(location)
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest
|
||||
|
||||
@@ -134,7 +133,7 @@ def delete_course_update(location, update, passed_id):
|
||||
|
||||
# update db record
|
||||
course_updates.definition['data'] = etree.tostring(course_html_parsed)
|
||||
store = get_modulestore(location)
|
||||
store = modulestore('direct')
|
||||
store.update_item(location, course_updates.definition['data'])
|
||||
|
||||
return get_course_updates(location)
|
||||
|
||||
28
cms/djangoapps/contentstore/management/commands/xlint.py
Normal file
28
cms/djangoapps/contentstore/management/commands/xlint.py
Normal file
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
83
cms/djangoapps/contentstore/module_info_model.py
Normal file
83
cms/djangoapps/contentstore/module_info_model.py
Normal file
@@ -0,0 +1,83 @@
|
||||
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)
|
||||
113
cms/djangoapps/contentstore/tests/factories.py
Normal file
113
cms/djangoapps/contentstore/tests/factories.py
Normal file
@@ -0,0 +1,113 @@
|
||||
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'
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from mock import patch, Mock
|
||||
from override_settings import override_settings
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -9,11 +8,10 @@ from path import path
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
import copy
|
||||
from factories import *
|
||||
|
||||
|
||||
def parse_json(response):
|
||||
@@ -22,33 +20,33 @@ def parse_json(response):
|
||||
|
||||
|
||||
def user(email):
|
||||
'''look up a user by email'''
|
||||
"""look up a user by email"""
|
||||
return User.objects.get(email=email)
|
||||
|
||||
|
||||
def registration(email):
|
||||
'''look up registration object by email'''
|
||||
"""look up registration object by email"""
|
||||
return Registration.objects.get(user__email=email)
|
||||
|
||||
|
||||
class ContentStoreTestCase(TestCase):
|
||||
def _login(self, email, pw):
|
||||
'''Login. View should always return 200. The success/fail is in the
|
||||
returned json'''
|
||||
"""Login. View should always return 200. The success/fail is in the
|
||||
returned json"""
|
||||
resp = self.client.post(reverse('login_post'),
|
||||
{'email': email, 'password': pw})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return resp
|
||||
|
||||
def login(self, email, pw):
|
||||
'''Login, check that it worked.'''
|
||||
"""Login, check that it worked."""
|
||||
resp = self._login(email, pw)
|
||||
data = parse_json(resp)
|
||||
self.assertTrue(data['success'])
|
||||
return resp
|
||||
|
||||
def _create_account(self, username, email, pw):
|
||||
'''Try to create an account. No error checking'''
|
||||
"""Try to create an account. No error checking"""
|
||||
resp = self.client.post('/create_account', {
|
||||
'username': username,
|
||||
'email': email,
|
||||
@@ -62,7 +60,7 @@ class ContentStoreTestCase(TestCase):
|
||||
return resp
|
||||
|
||||
def create_account(self, username, email, pw):
|
||||
'''Create the account and check that it worked'''
|
||||
"""Create the account and check that it worked"""
|
||||
resp = self._create_account(username, email, pw)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
@@ -74,8 +72,8 @@ class ContentStoreTestCase(TestCase):
|
||||
return resp
|
||||
|
||||
def _activate_user(self, email):
|
||||
'''Look up the activation key for the user, then hit the activate view.
|
||||
No error checking'''
|
||||
"""Look up the activation key for the user, then hit the activate view.
|
||||
No error checking"""
|
||||
activation_key = registration(email).activation_key
|
||||
|
||||
# and now we try to activate
|
||||
@@ -220,12 +218,6 @@ class ContentStoreTest(TestCase):
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
|
||||
self.section_data = {
|
||||
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template' : 'i4x://edx/templates/chapter/Empty',
|
||||
'display_name': 'Section One',
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
# Make sure you flush out the test modulestore after the end
|
||||
# of the last test because otherwise on the next run
|
||||
@@ -262,6 +254,16 @@ class ContentStoreTest(TestCase):
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
'There is already a course defined with the same organization and course number.')
|
||||
|
||||
def test_create_course_with_bad_organization(self):
|
||||
"""Test new course creation - error path for bad organization name"""
|
||||
self.course_data['org'] = 'University of California, Berkeley'
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
data = parse_json(resp)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
"Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
|
||||
|
||||
def test_course_index_view_with_no_courses(self):
|
||||
"""Test viewing the index page with no courses"""
|
||||
# Create a course so there is something to view
|
||||
@@ -271,20 +273,27 @@ class ContentStoreTest(TestCase):
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_factory(self):
|
||||
course = CourseFactory.create()
|
||||
self.assertIsInstance(course, xmodule.course_module.CourseDescriptor)
|
||||
|
||||
def test_item_factory(self):
|
||||
course = CourseFactory.create()
|
||||
item = ItemFactory.create(parent_location=course.location)
|
||||
self.assertIsInstance(item, xmodule.seq_module.SequenceDescriptor)
|
||||
|
||||
def test_course_index_view_with_course(self):
|
||||
"""Test viewing the index page with an existing course"""
|
||||
# Create a course so there is something to view
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
CourseFactory.create(display_name='Robot Super Educational Course')
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<span class="class-name">Robot Super Course</span>',
|
||||
'<span class="class-name">Robot Super Educational Course</span>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_overview_view_with_course(self):
|
||||
"""Test viewing the course overview page with an existing course"""
|
||||
# Create a course so there is something to view
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
data = {
|
||||
'org': 'MITx',
|
||||
@@ -300,8 +309,15 @@ class ContentStoreTest(TestCase):
|
||||
|
||||
def test_clone_item(self):
|
||||
"""Test cloning an item. E.g. creating a new section"""
|
||||
resp = self.client.post(reverse('create_new_course'), self.course_data)
|
||||
resp = self.client.post(reverse('clone_item'), self.section_data)
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
section_data = {
|
||||
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template' : 'i4x://edx/templates/chapter/Empty',
|
||||
'display_name': 'Section One',
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse('clone_item'), section_data)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
@@ -323,3 +339,4 @@ class ContentStoreTest(TestCase):
|
||||
def test_edit_unit_full(self):
|
||||
self.check_edit_unit('full')
|
||||
|
||||
|
||||
|
||||
@@ -1,53 +1,59 @@
|
||||
from .utils import get_course_location_for_item, get_lms_link_for_item, \
|
||||
compute_unit_state, get_date_display, UnitState
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
from PIL import Image
|
||||
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, \
|
||||
create_all_course_groups, get_user_by_email, add_user_to_course_group, \
|
||||
remove_user_from_course_group, is_user_in_course_group_role, \
|
||||
get_users_in_course_group_by_role
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, Http404, HttpResponseBadRequest, \
|
||||
HttpResponseForbidden
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from external_auth.views import ssl_login_shortcut
|
||||
from functools import partial
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from path import path
|
||||
from static_replace import replace_urls
|
||||
from util.json_request import expect_json
|
||||
from uuid import uuid4
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.timeparse import stringify_time
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xmodule
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
import time
|
||||
from contentstore import course_info_model
|
||||
from contentstore.utils import get_modulestore
|
||||
import tarfile
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from uuid import uuid4
|
||||
from path import path
|
||||
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
from PIL import Image
|
||||
|
||||
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.context_processors import csrf
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from static_replace import replace_urls
|
||||
from external_auth.views import ssl_login_shortcut
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xmodule
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from functools import partial
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
|
||||
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
|
||||
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
|
||||
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item
|
||||
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from contentstore.course_info_model import get_course_updates,\
|
||||
update_course_updates, delete_course_update
|
||||
from cache_toolbox.core import del_cached_content
|
||||
from xmodule.timeparse import stringify_time
|
||||
from contentstore.module_info_model import get_module_info, set_module_info
|
||||
from cms.djangoapps.models.settings.course_details import CourseDetails,\
|
||||
CourseSettingsEncoder
|
||||
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
|
||||
from cms.djangoapps.contentstore.utils import get_modulestore
|
||||
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
|
||||
@@ -474,11 +480,20 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
||||
error_msg=exc_info_to_str(sys.exc_info())
|
||||
).xmodule_constructor(system)(None, None)
|
||||
|
||||
module.get_html = wrap_xmodule(
|
||||
module.get_html,
|
||||
module,
|
||||
"xmodule_display.html",
|
||||
)
|
||||
# cdodge: Special case
|
||||
if module.location.category == 'static_tab':
|
||||
module.get_html = wrap_xmodule(
|
||||
module.get_html,
|
||||
module,
|
||||
"xmodule_tab_display.html",
|
||||
)
|
||||
else:
|
||||
module.get_html = wrap_xmodule(
|
||||
module.get_html,
|
||||
module,
|
||||
"xmodule_display.html",
|
||||
)
|
||||
|
||||
module.get_html = replace_static_urls(
|
||||
module.get_html,
|
||||
module.metadata.get('data_dir', module.location.course),
|
||||
@@ -905,7 +920,8 @@ def course_info(request, org, course, name, provided_id=None):
|
||||
'active_tab': 'courseinfo-tab',
|
||||
'context_course': course_module,
|
||||
'url_base' : "/" + org + "/" + course + "/",
|
||||
'course_updates' : json.dumps(course_info_model.get_course_updates(location))
|
||||
'course_updates' : json.dumps(get_course_updates(location)),
|
||||
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
|
||||
})
|
||||
|
||||
@expect_json
|
||||
@@ -928,13 +944,38 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
real_method = request.method
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(course_info_model.get_course_updates(location)), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
|
||||
elif real_method == 'POST':
|
||||
return HttpResponse(json.dumps(course_info_model.update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
# new instance (unless django makes PUT a POST): updates are coming as POST. Not sure why.
|
||||
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
elif real_method == 'PUT':
|
||||
return HttpResponse(json.dumps(course_info_model.update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
return HttpResponse(json.dumps(course_info_model.delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
|
||||
elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
|
||||
return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
|
||||
|
||||
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def module_info(request, module_location):
|
||||
location = Location(module_location)
|
||||
|
||||
# NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
|
||||
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
else:
|
||||
real_method = request.method
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location)), mimetype="application/json")
|
||||
elif real_method == 'POST' or real_method == 'PUT':
|
||||
return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
|
||||
else:
|
||||
return HttpResponseBadRequest
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -1079,7 +1120,10 @@ def create_new_course(request):
|
||||
number = request.POST.get('number')
|
||||
display_name = request.POST.get('display_name')
|
||||
|
||||
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
|
||||
try:
|
||||
dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
|
||||
except InvalidLocationError as e:
|
||||
return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
|
||||
|
||||
# see if the course already exists
|
||||
existing_course = None
|
||||
|
||||
@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log",
|
||||
logging_env="dev",
|
||||
tracking_filename="tracking.log",
|
||||
dev_env=True,
|
||||
debug=True)
|
||||
debug=True,
|
||||
local_loglevel='ERROR',
|
||||
console_loglevel='ERROR')
|
||||
|
||||
PIPELINE_JS['js-test-source'] = {
|
||||
'source_filenames': sum([
|
||||
|
||||
19
cms/static/client_templates/course_info_handouts.html
Normal file
19
cms/static/client_templates/course_info_handouts.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<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>
|
||||
29
cms/static/client_templates/course_info_update.html
Normal file
29
cms/static/client_templates/course_info_update.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<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>
|
||||
14
cms/static/client_templates/load_templates.html
Normal file
14
cms/static/client_templates/load_templates.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!-- 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>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 970 B After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.9 KiB |
BIN
cms/static/img/plus-icon-white.png
Normal file
BIN
cms/static/img/plus-icon-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 951 B |
@@ -11,6 +11,11 @@ $(document).ready(function() {
|
||||
$body = $('body');
|
||||
$modal = $('.history-modal');
|
||||
$modalCover = $('<div class="modal-cover">');
|
||||
// cdodge: this looks funny, but on AWS instances, this base.js get's wrapped in a separate scope as part of Django static
|
||||
// pipelining (note, this doesn't happen on local runtimes). So if we set it on window, when we can access it from other
|
||||
// scopes (namely the course-info tab)
|
||||
window.$modalCover = $modalCover;
|
||||
|
||||
$body.append($modalCover);
|
||||
$newComponentItem = $('.new-component-item');
|
||||
$newComponentTypePicker = $('.new-component');
|
||||
@@ -93,7 +98,7 @@ $(document).ready(function() {
|
||||
// section name editing
|
||||
$('.section-name').bind('click', editSectionName);
|
||||
$('.edit-section-name-cancel').bind('click', cancelEditSectionName);
|
||||
$('.edit-section-name-save').bind('click', saveEditSectionName);
|
||||
// $('.edit-section-name-save').bind('click', saveEditSectionName);
|
||||
|
||||
// section date setting
|
||||
$('.set-publish-date').bind('click', setSectionScheduleDate);
|
||||
@@ -585,33 +590,44 @@ function hideToastMessage(e) {
|
||||
$(this).closest('.toast-notification').remove();
|
||||
}
|
||||
|
||||
function addNewSection(e) {
|
||||
function addNewSection(e, isTemplate) {
|
||||
e.preventDefault();
|
||||
|
||||
var $newSection = $($('#new-section-template').html());
|
||||
var $cancelButton = $newSection.find('.new-section-name-cancel');
|
||||
$('.new-courseware-section-button').after($newSection);
|
||||
$newSection.find('.new-section-name').focus().select();
|
||||
$newSection.find('.new-section-name-save').bind('click', saveNewSection);
|
||||
$newSection.find('.new-section-name-cancel').bind('click', cancelNewSection);
|
||||
$newSection.find('.section-name-form').bind('submit', saveNewSection);
|
||||
$cancelButton.bind('click', cancelNewSection);
|
||||
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
|
||||
}
|
||||
|
||||
function checkForCancel(e) {
|
||||
if(e.which == 27) {
|
||||
$body.unbind('keyup', checkForCancel);
|
||||
e.data.$cancelButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function saveNewSection(e) {
|
||||
e.preventDefault();
|
||||
|
||||
parent = $(this).data('parent');
|
||||
template = $(this).data('template');
|
||||
var $saveButton = $(this).find('.new-section-name-save');
|
||||
var parent = $saveButton.data('parent');
|
||||
var template = $saveButton.data('template');
|
||||
var display_name = $(this).find('.new-section-name').val();
|
||||
|
||||
display_name = $(this).prev('.new-section-name').val();
|
||||
|
||||
$.post('/clone_item',
|
||||
{'parent_location' : parent,
|
||||
'template' : template,
|
||||
'display_name': display_name,
|
||||
},
|
||||
function(data) {
|
||||
$.post('/clone_item', {
|
||||
'parent_location' : parent,
|
||||
'template' : template,
|
||||
'display_name': display_name,
|
||||
},
|
||||
function(data) {
|
||||
if (data.id != undefined)
|
||||
location.reload();
|
||||
});
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function cancelNewSection(e) {
|
||||
@@ -619,44 +635,44 @@ function cancelNewSection(e) {
|
||||
$(this).parents('section.new-section').remove();
|
||||
}
|
||||
|
||||
|
||||
function addNewCourse(e) {
|
||||
e.preventDefault();
|
||||
var $newCourse = $($('#new-course-template').html());
|
||||
var $cancelButton = $newCourse.find('.new-course-cancel');
|
||||
$('.new-course-button').after($newCourse);
|
||||
$newCourse.find('.new-course-name').focus().select();
|
||||
$newCourse.find('.new-course-save').bind('click', saveNewCourse);
|
||||
$newCourse.find('.new-course-cancel').bind('click', cancelNewCourse);
|
||||
$newCourse.find('form').bind('submit', saveNewCourse);
|
||||
$cancelButton.bind('click', cancelNewCourse);
|
||||
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
|
||||
}
|
||||
|
||||
function saveNewCourse(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $newCourse = $(this).closest('.new-course');
|
||||
|
||||
template = $(this).data('template');
|
||||
|
||||
org = $newCourse.find('.new-course-org').val();
|
||||
number = $newCourse.find('.new-course-number').val();
|
||||
display_name = $newCourse.find('.new-course-name').val();
|
||||
var template = $(this).find('.new-course-save').data('template');
|
||||
var org = $newCourse.find('.new-course-org').val();
|
||||
var number = $newCourse.find('.new-course-number').val();
|
||||
var display_name = $newCourse.find('.new-course-name').val();
|
||||
|
||||
if (org == '' || number == '' || display_name == ''){
|
||||
alert('You must specify all fields in order to create a new course.');
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('/create_new_course',
|
||||
{ 'template' : template,
|
||||
'org' : org,
|
||||
'number' : number,
|
||||
'display_name': display_name,
|
||||
},
|
||||
function(data) {
|
||||
if (data.id != undefined)
|
||||
location.reload();
|
||||
else if (data.ErrMsg != undefined)
|
||||
$.post('/create_new_course', {
|
||||
'template' : template,
|
||||
'org' : org,
|
||||
'number' : number,
|
||||
'display_name': display_name,
|
||||
},
|
||||
function(data) {
|
||||
if (data.id != undefined) {
|
||||
window.location = '/' + data.id.replace(/.*:\/\//, '');
|
||||
} else if (data.ErrMsg != undefined) {
|
||||
alert(data.ErrMsg);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancelNewCourse(e) {
|
||||
@@ -672,35 +688,37 @@ function addNewSubsection(e) {
|
||||
$section.find('.new-subsection-name-input').focus().select();
|
||||
|
||||
var $saveButton = $newSubsection.find('.new-subsection-name-save');
|
||||
$saveButton.bind('click', saveNewSubsection);
|
||||
var $cancelButton = $newSubsection.find('.new-subsection-name-cancel');
|
||||
|
||||
parent = $(this).parents("section.branch").data("id");
|
||||
var parent = $(this).parents("section.branch").data("id");
|
||||
|
||||
$saveButton.data('parent', parent)
|
||||
$saveButton.data('template', $(this).data('template'));
|
||||
|
||||
$newSubsection.find('.new-subsection-name-cancel').bind('click', cancelNewSubsection);
|
||||
$newSubsection.find('.new-subsection-form').bind('submit', saveNewSubsection);
|
||||
$cancelButton.bind('click', cancelNewSubsection);
|
||||
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
|
||||
}
|
||||
|
||||
function saveNewSubsection(e) {
|
||||
e.preventDefault();
|
||||
|
||||
parent = $(this).data('parent');
|
||||
template = $(this).data('template');
|
||||
var parent = $(this).find('.new-subsection-name-save').data('parent');
|
||||
var template = $(this).find('.new-subsection-name-save').data('template');
|
||||
|
||||
var display_name = $(this).find('.new-subsection-name-input').val();
|
||||
|
||||
display_name = $(this).prev('.subsection-name').find('.new-subsection-name-input').val()
|
||||
|
||||
$.post('/clone_item',
|
||||
{'parent_location' : parent,
|
||||
'template' : template,
|
||||
'display_name': display_name,
|
||||
},
|
||||
function(data) {
|
||||
$.post('/clone_item', {
|
||||
'parent_location' : parent,
|
||||
'template' : template,
|
||||
'display_name': display_name
|
||||
},
|
||||
function(data) {
|
||||
if (data.id != undefined) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function cancelNewSubsection(e) {
|
||||
@@ -710,22 +728,30 @@ function cancelNewSubsection(e) {
|
||||
|
||||
function editSectionName(e) {
|
||||
e.preventDefault();
|
||||
$(this).children('div.section-name-edit').show();
|
||||
$(this).children('span.section-name-span').hide();
|
||||
$(this).unbind('click', editSectionName);
|
||||
$(this).children('.section-name-edit').show();
|
||||
$(this).find('.edit-section-name').focus();
|
||||
$(this).children('.section-name-span').hide();
|
||||
$(this).find('.section-name-edit').bind('submit', saveEditSectionName);
|
||||
$(this).find('.edit-section-name-cancel').bind('click', cancelNewSection);
|
||||
$body.bind('keyup', { $cancelButton: $(this).find('.edit-section-name-cancel') }, checkForCancel);
|
||||
}
|
||||
|
||||
function cancelEditSectionName(e) {
|
||||
e.preventDefault();
|
||||
$(this).parent().hide();
|
||||
$(this).parent().siblings('span.section-name-span').show();
|
||||
$(this).parent().siblings('.section-name-span').show();
|
||||
$(this).closest('.section-name').bind('click', editSectionName);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function saveEditSectionName(e) {
|
||||
e.preventDefault();
|
||||
|
||||
id = $(this).closest("section.courseware-section").data("id");
|
||||
display_name = $.trim($(this).prev('.edit-section-name').val());
|
||||
$(this).closest('.section-name').unbind('click', editSectionName);
|
||||
|
||||
var id = $(this).closest('.courseware-section').data('id');
|
||||
var display_name = $.trim($(this).find('.edit-section-name').val());
|
||||
|
||||
$(this).closest('.courseware-section .section-name').append($spinner);
|
||||
$spinner.show();
|
||||
@@ -746,10 +772,10 @@ function saveEditSectionName(e) {
|
||||
}).success(function()
|
||||
{
|
||||
$spinner.delay(250).fadeOut(250);
|
||||
$_this.parent().siblings('span.section-name-span').html(display_name);
|
||||
$_this.parent().siblings('span.section-name-span').show();
|
||||
$_this.parent().hide();
|
||||
e.stopPropagation();
|
||||
$_this.closest('h3').find('.section-name-span').html(display_name).show();
|
||||
$_this.hide();
|
||||
$_this.closest('.section-name').bind('click', editSectionName);
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ CMS.Models.CourseInfo = Backbone.Model.extend({
|
||||
// course update -- biggest kludge here is the lack of a real id to map updates to originals
|
||||
CMS.Models.CourseUpdate = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"date" : $.datepicker.formatDate('MM d', new Date()),
|
||||
"date" : $.datepicker.formatDate('MM d, yy', new Date()),
|
||||
"content" : ""
|
||||
}
|
||||
});
|
||||
@@ -29,6 +29,8 @@ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
|
||||
|
||||
model : CMS.Models.CourseUpdate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
10
cms/static/js/models/module_info.js
Normal file
10
cms/static/js/models/module_info.js
Normal file
@@ -0,0 +1,10 @@
|
||||
CMS.Models.ModuleInfo = Backbone.Model.extend({
|
||||
url: function() {return "/module_info/" + this.id;},
|
||||
|
||||
defaults: {
|
||||
"id": null,
|
||||
"data": null,
|
||||
"metadata" : null,
|
||||
"children" : null
|
||||
},
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
if (typeof window.templateLoader == 'function') return;
|
||||
|
||||
var templateLoader = {
|
||||
templateVersion: "0.0.3",
|
||||
templateVersion: "0.0.8",
|
||||
templates: {},
|
||||
loadRemoteTemplate: function(templateName, filename, callback) {
|
||||
if (!this.templates[templateName]) {
|
||||
|
||||
@@ -13,7 +13,11 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
|
||||
el: this.$('#course-update-view'),
|
||||
collection: this.model.get('updates')
|
||||
});
|
||||
// TODO instantiate the handouts view
|
||||
|
||||
new CMS.Views.ClassInfoHandoutsView({
|
||||
el: this.$('#course-handouts-view'),
|
||||
model: this.model.get('handouts')
|
||||
});
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -34,11 +38,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// instantiates an editor template for each update in the collection
|
||||
window.templateLoader.loadRemoteTemplate("course_info_update",
|
||||
// TODO Where should the template reside? how to use the static.url to create the path?
|
||||
"/static/coffee/src/client_templates/course_info_update.html",
|
||||
function (raw_template) {
|
||||
"/static/client_templates/course_info_update.html",
|
||||
function (raw_template) {
|
||||
self.template = _.template(raw_template);
|
||||
self.render();
|
||||
}
|
||||
self.render();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -53,28 +57,47 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
$(updateEle).append(newEle);
|
||||
});
|
||||
this.$el.find(".new-update-form").hide();
|
||||
this.$el.find('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
return this;
|
||||
},
|
||||
|
||||
onNew: function(event) {
|
||||
var self = this;
|
||||
// create new obj, insert into collection, and render this one ele overriding the hidden attr
|
||||
var newModel = new CMS.Models.CourseUpdate();
|
||||
this.collection.add(newModel, {at : 0});
|
||||
|
||||
var newForm = this.template({ updateModel : newModel });
|
||||
var $newForm = $(this.template({ updateModel : newModel }));
|
||||
|
||||
var $textArea = $newForm.find(".new-update-content").first();
|
||||
if (this.$codeMirror == null ) {
|
||||
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
});
|
||||
}
|
||||
|
||||
var updateEle = this.$el.find("#course-update-list");
|
||||
$(updateEle).append(newForm);
|
||||
$(newForm).find(".new-update-form").show();
|
||||
$(updateEle).prepend($newForm);
|
||||
$newForm.addClass('editing');
|
||||
this.$currentPost = $newForm.closest('li');
|
||||
|
||||
window.$modalCover.show();
|
||||
window.$modalCover.bind('click', function() {
|
||||
self.closeEditor(self, true);
|
||||
});
|
||||
|
||||
$('.date').datepicker('destroy');
|
||||
$('.date').datepicker({ 'dateFormat': 'MM d, yy' });
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
var targetModel = this.eventModel(event);
|
||||
targetModel.set({ date : this.dateEntry(event).val(), content : this.contentEntry(event).val() });
|
||||
// push change to display, hide the editor, submit the change
|
||||
$(this.dateDisplay(event)).val(targetModel.get('date'));
|
||||
$(this.contentDisplay(event)).val(targetModel.get('content'));
|
||||
$(this.editor(event)).hide();
|
||||
|
||||
console.log(this.contentEntry(event).val());
|
||||
targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() });
|
||||
// push change to display, hide the editor, submit the change
|
||||
this.closeEditor(this);
|
||||
targetModel.save();
|
||||
},
|
||||
|
||||
@@ -82,14 +105,31 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// change editor contents back to model values and hide the editor
|
||||
$(this.editor(event)).hide();
|
||||
var targetModel = this.eventModel(event);
|
||||
$(this.dateEntry(event)).val(targetModel.get('date'));
|
||||
$(this.contentEntry(event)).val(targetModel.get('content'));
|
||||
this.closeEditor(this, !targetModel.id);
|
||||
},
|
||||
|
||||
onEdit: function(event) {
|
||||
var self = this;
|
||||
this.$currentPost = $(event.target).closest('li');
|
||||
this.$currentPost.addClass('editing');
|
||||
|
||||
$(this.editor(event)).show();
|
||||
var $textArea = this.$currentPost.find(".new-update-content").first();
|
||||
if (this.$codeMirror == null ) {
|
||||
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
});
|
||||
}
|
||||
|
||||
window.$modalCover.show();
|
||||
var targetModel = this.eventModel(event);
|
||||
window.$modalCover.bind('click', function() {
|
||||
self.closeEditor(self);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
onDelete: function(event) {
|
||||
// TODO ask for confirmation
|
||||
// remove the dom element and delete the model
|
||||
@@ -101,6 +141,24 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
closeEditor: function(self, removePost) {
|
||||
var targetModel = self.collection.getByCid(self.$currentPost.attr('name'));
|
||||
|
||||
if(removePost) {
|
||||
self.$currentPost.remove();
|
||||
}
|
||||
|
||||
// close the modal and insert the appropriate data
|
||||
self.$currentPost.removeClass('editing');
|
||||
self.$currentPost.find('.date-display').html(targetModel.get('date'));
|
||||
self.$currentPost.find('.date').val(targetModel.get('date'));
|
||||
self.$currentPost.find('.update-contents').html(targetModel.get('content'));
|
||||
self.$currentPost.find('.new-update-content').val(targetModel.get('content'));
|
||||
self.$currentPost.find('form').hide();
|
||||
window.$modalCover.unbind('click');
|
||||
window.$modalCover.hide();
|
||||
},
|
||||
|
||||
// Dereferencing from events to screen elements
|
||||
eventModel: function(event) {
|
||||
@@ -119,7 +177,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
|
||||
dateEntry: function(event) {
|
||||
var li = $(event.currentTarget).closest("li");
|
||||
if (li) return $(li).find("#date-entry").first();
|
||||
if (li) return $(li).find(".date").first();
|
||||
},
|
||||
|
||||
contentEntry: function(event) {
|
||||
@@ -135,4 +193,83 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
@@ -181,6 +181,11 @@ code {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding: 6px 14px;
|
||||
border-bottom: 1px solid #cbd1db;
|
||||
@@ -338,4 +343,29 @@ body.show-wip {
|
||||
content: '';
|
||||
@extend .spinner-icon;
|
||||
}
|
||||
}
|
||||
|
||||
.new-button {
|
||||
@include grey-button;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
|
||||
&.big {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-button.standard,
|
||||
.delete-button.standard {
|
||||
float: left;
|
||||
@include white-button;
|
||||
padding: 3px 10px 4px;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
.edit-icon,
|
||||
.delete-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
@@ -51,14 +51,14 @@
|
||||
@include button;
|
||||
border: 1px solid $darkGrey;
|
||||
border-radius: 3px;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0) 60%);
|
||||
@include linear-gradient(top, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
|
||||
background-color: #dfe5eb;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset);
|
||||
color: #5d6779;
|
||||
color: #778192;
|
||||
|
||||
&:hover {
|
||||
background-color: #f2f6f9;
|
||||
color: #5d6779;
|
||||
color: #778192;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +1,179 @@
|
||||
body.updates {
|
||||
.course-info {
|
||||
h2 {
|
||||
margin-bottom: 24px;
|
||||
font-size: 22px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.course-info-wrapper {
|
||||
display: table;
|
||||
width: 100%;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.main-column,
|
||||
.course-handouts {
|
||||
float: none;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.main-column {
|
||||
border-radius: 3px 0 0 3px;
|
||||
border-right-color: $mediumGrey;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border: 1px solid #3c3c3c;
|
||||
background: #fff;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
}
|
||||
|
||||
.course-updates {
|
||||
padding: 30px 40px;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
padding: 24px 0 32px;
|
||||
.update-list > li {
|
||||
padding: 34px 0 42px;
|
||||
border-top: 1px solid #cbd1db;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #646464;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
&.editing {
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
|
||||
.post-preview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
float: none;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 30px;
|
||||
color: #646464;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 34px 0 11px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.update-contents {
|
||||
padding-left: 30px;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 18px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border: 1px solid #ddd;
|
||||
background: #f6f6f6;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.new-update-button {
|
||||
@include grey-button;
|
||||
@include blue-button;
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
padding: 18px 0;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.new-update-form {
|
||||
@include edit-box;
|
||||
margin-bottom: 24px;
|
||||
padding: 30px;
|
||||
border: none;
|
||||
|
||||
textarea {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-actions {
|
||||
float: right;
|
||||
|
||||
.edit-button,
|
||||
.delete-button{
|
||||
float: left;
|
||||
@include white-button;
|
||||
padding: 3px 10px 4px;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
.edit-icon,
|
||||
.delete-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-handouts {
|
||||
padding: 15px 20px;
|
||||
width: 30%;
|
||||
padding: 20px 30px;
|
||||
margin: 0;
|
||||
border-radius: 0 3px 3px 0;
|
||||
border-left: none;
|
||||
background: $lightGrey;
|
||||
|
||||
.new-handout-button {
|
||||
@include grey-button;
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
margin-bottom: 28px;
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
.edit-button {
|
||||
float: right;
|
||||
@include white-button;
|
||||
padding: 3px 10px 4px;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
.edit-icon,
|
||||
.delete-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.handouts-content {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.new-handout-form {
|
||||
@include edit-box;
|
||||
margin-bottom: 24px;
|
||||
.treeview-handoutsnav li {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-handouts-form {
|
||||
@include edit-box;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 10001;
|
||||
width: 800px;
|
||||
padding: 30px;
|
||||
|
||||
textarea {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,7 @@ input.courseware-unit-search-input {
|
||||
}
|
||||
|
||||
.courseware-overview {
|
||||
.new-courseware-section-button {
|
||||
@include grey-button;
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.courseware-section {
|
||||
@@ -146,18 +141,18 @@ input.courseware-unit-search-input {
|
||||
|
||||
.section-name-edit {
|
||||
input {
|
||||
font-size: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
@include blue-button;
|
||||
padding: 7px 20px 7px;
|
||||
padding: 10px 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@include white-button;
|
||||
padding: 7px 20px 7px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +200,7 @@ input.courseware-unit-search-input {
|
||||
.new-section-name-save,
|
||||
.new-subsection-name-save {
|
||||
@include blue-button;
|
||||
padding: 2px 20px 5px;
|
||||
padding: 6px 20px 8px;
|
||||
margin: 0 5px;
|
||||
color: #fff !important;
|
||||
}
|
||||
@@ -213,7 +208,7 @@ input.courseware-unit-search-input {
|
||||
.new-section-name-cancel,
|
||||
.new-subsection-name-cancel {
|
||||
@include white-button;
|
||||
padding: 2px 20px 5px;
|
||||
padding: 6px 20px 8px;
|
||||
color: #8891a1 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
|
||||
.new-course-save {
|
||||
@include blue-button;
|
||||
// padding: ;
|
||||
}
|
||||
|
||||
.new-course-cancel {
|
||||
|
||||
@@ -137,6 +137,10 @@
|
||||
height: 11px;
|
||||
margin-right: 8px;
|
||||
background: url(../img/plus-icon.png) no-repeat;
|
||||
|
||||
&.white {
|
||||
background: url(../img/plus-icon-white.png) no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.plus-icon-small {
|
||||
|
||||
69
cms/static/sass/_lms.scss
Normal file
69
cms/static/sass/_lms.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,72 @@
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.unit-body {
|
||||
padding: 30px 40px;
|
||||
}
|
||||
|
||||
.components > li {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&.new-component-item {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.component {
|
||||
border: 1px solid $mediumGrey;
|
||||
border-top: none;
|
||||
|
||||
&:first-child {
|
||||
border-top: 1px solid $mediumGrey;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $mediumGrey;
|
||||
border-top: none;
|
||||
|
||||
&:first-child {
|
||||
border-top: 1px solid $mediumGrey;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
background: url(../img/drag-handles.png) center no-repeat $lightGrey;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 11;
|
||||
width: 35px;
|
||||
border: none;
|
||||
background: url(../img/drag-handles.png) center no-repeat $lightGrey;
|
||||
|
||||
&:hover {
|
||||
background: url(../img/drag-handles.png) center no-repeat $lightGrey;
|
||||
}
|
||||
}
|
||||
|
||||
.component-actions {
|
||||
top: 26px;
|
||||
right: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.component.editing {
|
||||
.xmodule_display {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 20px 20px 22px;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
background: $lightGrey;
|
||||
}
|
||||
|
||||
.static-page-item {
|
||||
position: relative;
|
||||
margin: 10px 0;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
}
|
||||
|
||||
.main-column {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 70%;
|
||||
}
|
||||
@@ -54,94 +55,11 @@
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 20px 40px;
|
||||
border: 1px solid #d1ddec;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
border-color: #6696d7;
|
||||
|
||||
.drag-handle,
|
||||
.component-actions a {
|
||||
background-color: $blue;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
border-color: #6696d7;
|
||||
|
||||
.drag-handle,
|
||||
.component-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.component-placeholder {
|
||||
border-color: #6696d7;
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
}
|
||||
|
||||
.component-actions {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
@include transition(opacity .15s);
|
||||
}
|
||||
|
||||
.edit-button,
|
||||
.delete-button {
|
||||
float: left;
|
||||
padding: 3px 10px 4px;
|
||||
margin-left: 3px;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
background: #d1ddec;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
@include transition(all .15s);
|
||||
|
||||
&:hover {
|
||||
background-color: $blue;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.edit-icon,
|
||||
.delete-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -1px;
|
||||
right: -16px;
|
||||
z-index: -1;
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
border: 1px solid #d1ddec;
|
||||
background: url(../img/white-drag-handles.png) center no-repeat #d1ddec;
|
||||
cursor: move;
|
||||
@include transition(all .15s);
|
||||
}
|
||||
|
||||
&.new-component-item {
|
||||
padding: 0;
|
||||
border: 1px solid #8891a1;
|
||||
border-radius: 3px;
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0));
|
||||
background-color: #d1dae3;
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .2) inset);
|
||||
@include transition(background-color .15s, border-color .15s);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
&.adding {
|
||||
background-color: $blue;
|
||||
@@ -223,8 +141,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
.component {
|
||||
border: 1px solid #d1ddec;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
border-color: #6696d7;
|
||||
|
||||
.drag-handle {
|
||||
background-color: $blue;
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
border-color: #6696d7;
|
||||
|
||||
.drag-handle,
|
||||
.component-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.component-placeholder {
|
||||
border-color: #6696d7;
|
||||
}
|
||||
|
||||
.component-actions {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 9px;
|
||||
@include transition(opacity .15s);
|
||||
|
||||
a {
|
||||
color: $darkGrey;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -1px;
|
||||
right: -16px;
|
||||
z-index: -1;
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
border: 1px solid #d1ddec;
|
||||
background: url(../img/white-drag-handles.png) center no-repeat #d1ddec;
|
||||
cursor: move;
|
||||
@include transition(all .15s);
|
||||
}
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 10px 20px;
|
||||
padding: 40px 20px 20px;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
.user-overview {
|
||||
@extend .window;
|
||||
padding: 30px 40px;
|
||||
|
||||
.details {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.new-user-button {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
@import "modal";
|
||||
@import "alerts";
|
||||
@import "login";
|
||||
@import "lms";
|
||||
@import 'jquery-ui-calendar';
|
||||
|
||||
@import 'content-types';
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<%static:css group='base-style'/>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/skins/simple/style.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/markitup/sets/wiki/style.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
|
||||
|
||||
<title><%block name="title"></%block></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
@@ -36,9 +33,8 @@
|
||||
<script src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.tablednd.js')}"></script>
|
||||
<script src="${static.url('js/vendor/jquery.form.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/symbolset.ss-standard.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/symbolset.ss-symbolicons.js')}"></script>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
|
||||
<script type="text/javascript">
|
||||
document.write('\x3Cscript type="text/javascript" src="' +
|
||||
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
<div class="component-actions">
|
||||
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
|
||||
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
|
||||
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a>
|
||||
<a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a>
|
||||
</div>
|
||||
<a href="#" class="drag-handle"></a>
|
||||
${preview}
|
||||
@@ -3,29 +3,39 @@
|
||||
|
||||
<!-- TODO decode course # from context_course into title -->
|
||||
<%block name="title">Course Info</%block>
|
||||
<%block name="bodyclass">course-info</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/module_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
|
||||
<script src="${static.url('js/vendor/date.js')}"></script>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(document).ready(function(){
|
||||
|
||||
var course_updates = new CMS.Models.CourseUpdateCollection();
|
||||
course_updates.reset(${course_updates|n});
|
||||
course_updates.urlbase = '${url_base}';
|
||||
|
||||
var editor = new CMS.Views.CourseInfoEdit({
|
||||
el: $('.main-wrapper'),
|
||||
model : new CMS.Models.CourseInfo({
|
||||
courseId : '${context_course.location}',
|
||||
updates : course_updates,
|
||||
// FIXME add handouts
|
||||
handouts : null})
|
||||
});
|
||||
editor.render();
|
||||
});
|
||||
$(document).ready(function(){
|
||||
var course_updates = new CMS.Models.CourseUpdateCollection();
|
||||
course_updates.reset(${course_updates|n});
|
||||
course_updates.urlbase = '${url_base}';
|
||||
|
||||
var course_handouts = new CMS.Models.ModuleInfo({
|
||||
id: '${handouts_location}'
|
||||
});
|
||||
course_handouts.urlbase = '${url_base}';
|
||||
|
||||
var editor = new CMS.Views.CourseInfoEdit({
|
||||
el: $('.main-wrapper'),
|
||||
model : new CMS.Models.CourseInfo({
|
||||
courseId : '${context_course.location}',
|
||||
updates : course_updates,
|
||||
handouts : course_handouts
|
||||
})
|
||||
});
|
||||
editor.render();
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -33,16 +43,18 @@
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>Course Info</h1>
|
||||
<div class="main-column">
|
||||
<div class="unit-body window" id="course-update-view">
|
||||
<h2>Updates</h2>
|
||||
<a href="#" class="new-update-button">New Update</a>
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
<!-- probably replace w/ a vertical where each element of the vertical is a separate update w/ a date and html field -->
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<h2>Course Updates & News</h2>
|
||||
<a href="#" class="new-update-button">New Update</a>
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
<!-- probably replace w/ a vertical where each element of the vertical is a separate update w/ a date and html field -->
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar window">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -20,23 +20,24 @@
|
||||
<div>
|
||||
<h1>Static Tabs</h1>
|
||||
</div>
|
||||
<div class="main-column">
|
||||
<article class="unit-body window">
|
||||
<div class="tab-list">
|
||||
<ol class='components'>
|
||||
% for id in components:
|
||||
<li class="component" data-id="${id}"/>
|
||||
% endfor
|
||||
|
||||
<li class="new-component-item">
|
||||
<a href="#" class="new-component-button new-tab">
|
||||
<span class="plus-icon"></span>New Tab
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<article class="unit-body window">
|
||||
<div class="details">
|
||||
<p>Here you can add and manage additional pages for your course. These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.</p>
|
||||
</div>
|
||||
<div class="tab-list">
|
||||
<ol class='components'>
|
||||
% for id in components:
|
||||
<li class="component" data-id="${id}"/>
|
||||
% endfor
|
||||
|
||||
<li class="new-component-item">
|
||||
<a href="#" class="new-button big new-tab">
|
||||
<span class="plus-icon"></span>New Tab
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -22,14 +22,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a href="#" class="new-course-save" data-template="${new_course_template}">Save</a>
|
||||
<a href="#" class="new-course-cancel">Cancel</a>
|
||||
<input type="submit" value="Save" class="new-course-save" data-template="${new_course_template}" />
|
||||
<input type="button" value="Cancel" class="new-course-cancel" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</script>
|
||||
</%block>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="main-wrapper">
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
<%namespace name="units" file="widgets/units.html" />
|
||||
|
||||
<%block name="jsextra">
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
|
||||
<script src="${static.url('js/vendor/date.js')}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
|
||||
<script src="${static.url('js/vendor/date.js')}"></script>
|
||||
</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
@@ -24,7 +24,33 @@
|
||||
<header>
|
||||
<a href="#" class="expand-collapse-icon collapse"></a>
|
||||
<div class="item-details">
|
||||
<h3 class="section-name"><input type="text" value="New Section Name" class="new-section-name" /><a href="#" class="new-section-name-save" data-parent="${parent_location}" data-template="${new_section_template}">Save</a><a href="#" class="new-section-name-cancel">Cancel</a></h3>
|
||||
<h3 class="section-name">
|
||||
<form class="section-name-form">
|
||||
<input type="text" value="New Section Name" class="new-section-name" />
|
||||
<input type="submit" class="new-section-name-save" data-parent="${parent_location}" data-template="${new_section_template}" value="Save" />
|
||||
<input type="button" class="new-section-name-cancel" value="Cancel" /></h3>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="blank-slate-template">
|
||||
<section class="courseware-section branch new-section">
|
||||
<header>
|
||||
<a href="#" class="expand-collapse-icon collapse"></a>
|
||||
<div class="item-details">
|
||||
<h3 class="section-name">
|
||||
<span class="section-name-span">Click here to set the section name</span>
|
||||
<form class="section-name-form">
|
||||
<input type="text" value="New Section Name" class="new-section-name" />
|
||||
<input type="submit" class="new-section-name-save" data-parent="${parent_location}" data-template="${new_section_template}" value="Save" />
|
||||
<input type="button" class="new-section-name-cancel" value="Cancel" /></h3>
|
||||
</form>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<a href="#" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
|
||||
<a href="#" class="drag-handle"></a>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
@@ -33,14 +59,14 @@
|
||||
<script type="text/template" id="new-subsection-template">
|
||||
<li class="branch collapsed">
|
||||
<div class="section-item editing">
|
||||
<div>
|
||||
<form class="new-subsection-form">
|
||||
<span class="folder-icon"></span>
|
||||
<span class="subsection-name">
|
||||
<input type="text" value="New Subsection" class="new-subsection-name-input" />
|
||||
</span>
|
||||
<a href="#" class="new-subsection-name-save">Save</a>
|
||||
<a href="#" class="new-subsection-name-cancel">Cancel</a>
|
||||
</div>
|
||||
<input type="submit" value="Save" class="new-subsection-name-save" />
|
||||
<input type="button" value="Cancel" class="new-subsection-name-cancel" />
|
||||
</form>
|
||||
</div>
|
||||
<ol>
|
||||
<li>
|
||||
@@ -75,7 +101,7 @@
|
||||
<h1>Courseware</h1>
|
||||
<div class="page-actions"></div>
|
||||
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
|
||||
<a href="#" class="new-courseware-section-button"><span class="plus-icon"></span> New Section</a>
|
||||
<a href="#" class="new-button big new-courseware-section-button"><span class="plus-icon"></span> New Section</a>
|
||||
% for section in sections:
|
||||
<section class="courseware-section branch" data-id="${section.location}">
|
||||
<header>
|
||||
@@ -83,10 +109,11 @@
|
||||
<div class="item-details" data-id="${section.location}">
|
||||
<h3 class="section-name">
|
||||
<span class="section-name-span">${section.display_name}</span>
|
||||
<div class="section-name-edit" style="display:none">
|
||||
<form class="section-name-edit" style="display:none">
|
||||
<input type="text" value="${section.display_name}" class="edit-section-name" autocomplete="off"/>
|
||||
<a href="#" class="save-button edit-section-name-save">Save</a><a href="#" class="cancel-button edit-section-name-cancel">Cancel</a>
|
||||
</div>
|
||||
<input type="submit" class="save-button edit-section-name-save" value="Save" />
|
||||
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
|
||||
</form>
|
||||
</h3>
|
||||
<div class="section-published-date">
|
||||
<%
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<li class="component" data-id="${id}"/>
|
||||
% endfor
|
||||
<li class="new-component-item">
|
||||
<a href="#" class="new-component-button">
|
||||
<a href="#" class="new-component-button new-button big">
|
||||
<span class="plus-icon"></span>New Component
|
||||
</a>
|
||||
<div class="new-component">
|
||||
|
||||
3
cms/templates/xmodule_tab_display.html
Normal file
3
cms/templates/xmodule_tab_display.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<section class="xmodule_display xmodule_${class_}" data-type="${module_name}">
|
||||
${display_name}
|
||||
</section>
|
||||
@@ -45,6 +45,10 @@ urlpatterns = ('',
|
||||
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', 'contentstore.views.asset_index', name='asset_index'),
|
||||
|
||||
# this is a generic method to return the data/metadata associated with a xmodule
|
||||
url(r'^module_info/(?P<module_location>.*)$', 'contentstore.views.module_info', name='module_info'),
|
||||
|
||||
|
||||
# temporary landing page for a course
|
||||
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'),
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ def replace(static_url, prefix=None, course_namespace=None):
|
||||
|
||||
|
||||
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
|
||||
|
||||
def replace_url(static_url):
|
||||
return replace(static_url, staticfiles_prefix, course_namespace = course_namespace)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ def expect_json(view_function):
|
||||
def expect_json_with_cloned_request(request, *args, **kwargs):
|
||||
# cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information
|
||||
# e.g. 'charset', so we can't do a direct string compare
|
||||
if request.META['CONTENT_TYPE'].lower().startswith("application/json"):
|
||||
if request.META.get('CONTENT_TYPE','').lower().startswith("application/json"):
|
||||
cloned_request = copy.copy(request)
|
||||
cloned_request.POST = cloned_request.POST.copy()
|
||||
cloned_request.POST.update(json.loads(request.body))
|
||||
|
||||
@@ -21,6 +21,7 @@ def wrap_xmodule(get_html, module, template, context=None):
|
||||
module: An XModule
|
||||
template: A template that takes the variables:
|
||||
content: the results of get_html,
|
||||
display_name: the display name of the xmodule, if available (None otherwise)
|
||||
class_: the module class name
|
||||
module_name: the js_module_name of the module
|
||||
"""
|
||||
@@ -31,6 +32,7 @@ def wrap_xmodule(get_html, module, template, context=None):
|
||||
def _get_html():
|
||||
context.update({
|
||||
'content': get_html(),
|
||||
'display_name' : module.metadata.get('display_name') if module.metadata is not None else None,
|
||||
'class_': module.__class__.__name__,
|
||||
'module_name': module.js_module_name
|
||||
})
|
||||
|
||||
1
common/lib/.gitignore
vendored
Normal file
1
common/lib/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*/jasmine_test_runner.html
|
||||
@@ -3,6 +3,7 @@ import platform
|
||||
import sys
|
||||
from logging.handlers import SysLogHandler
|
||||
|
||||
LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
||||
|
||||
def get_logger_config(log_dir,
|
||||
logging_env="no_env",
|
||||
@@ -11,7 +12,8 @@ def get_logger_config(log_dir,
|
||||
dev_env=False,
|
||||
syslog_addr=None,
|
||||
debug=False,
|
||||
local_loglevel='INFO'):
|
||||
local_loglevel='INFO',
|
||||
console_loglevel=None):
|
||||
|
||||
"""
|
||||
|
||||
@@ -30,9 +32,12 @@ def get_logger_config(log_dir,
|
||||
"""
|
||||
|
||||
# Revert to INFO if an invalid string is passed in
|
||||
if local_loglevel not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||
if local_loglevel not in LOG_LEVELS:
|
||||
local_loglevel = 'INFO'
|
||||
|
||||
if console_loglevel is None or console_loglevel not in LOG_LEVELS:
|
||||
console_loglevel = 'DEBUG' if debug else 'INFO'
|
||||
|
||||
hostname = platform.node().split(".")[0]
|
||||
syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s "
|
||||
"[{hostname} %(process)d] [%(filename)s:%(lineno)d] "
|
||||
@@ -55,7 +60,7 @@ def get_logger_config(log_dir,
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'level': 'DEBUG' if debug else 'INFO',
|
||||
'level': console_loglevel,
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'standard',
|
||||
'stream': sys.stdout,
|
||||
|
||||
44
common/lib/xmodule/jasmine_test_runner.html.erb
Normal file
44
common/lib/xmodule/jasmine_test_runner.html.erb
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
|
||||
"http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Jasmine Test Runner</title>
|
||||
<link rel="stylesheet" type="text/css" href="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.css">
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.js"></script>
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine-html.js"></script>
|
||||
|
||||
<script type="text/javascript" src="<%= phantom_jasmine_path %>/lib/console-runner.js"></script>
|
||||
<script type="text/javascript" src="<%= common_coffee_root %>/ajax_prefix.js"></script>
|
||||
<script type="text/javascript" src="<%= common_coffee_root %>/logger.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/jasmine-jquery.js"></script>
|
||||
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, function() {
|
||||
return "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- SOURCE FILES -->
|
||||
<% for src in js_source %>
|
||||
<script type="text/javascript" src="<%= src %>"></script>
|
||||
<% end %>
|
||||
|
||||
<!-- SPEC FILES -->
|
||||
<% for src in js_specs %>
|
||||
<script type="text/javascript" src="<%= src %>"></script>
|
||||
<% end %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script type="text/javascript">
|
||||
var console_reporter = new jasmine.ConsoleReporter()
|
||||
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
|
||||
jasmine.getEnv().addReporter(console_reporter);
|
||||
jasmine.getEnv().execute();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -347,7 +347,7 @@ class CapaModule(XModule):
|
||||
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
|
||||
|
||||
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
|
||||
return self.system.replace_urls(html, self.metadata['data_dir'])
|
||||
return self.system.replace_urls(html, self.metadata['data_dir'], course_namespace=self.location)
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
@@ -451,7 +451,7 @@ class CapaModule(XModule):
|
||||
new_answers = dict()
|
||||
for answer_id in answers:
|
||||
try:
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'])}
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id], self.metadata['data_dir'], course_namespace=self.location)}
|
||||
except TypeError:
|
||||
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
|
||||
new_answer = {answer_id: answers[answer_id]}
|
||||
|
||||
@@ -46,6 +46,11 @@ class StaticContent(object):
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_base_url_path_for_course_assets(loc):
|
||||
if loc is not None:
|
||||
return "/c4x/{org}/{course}/asset".format(**loc.dict())
|
||||
|
||||
@staticmethod
|
||||
def get_id_from_location(location):
|
||||
return { 'tag':location.tag, 'org' : location.org, 'course' : location.course,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import abc
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
@@ -66,17 +65,27 @@ def grader_from_conf(conf):
|
||||
for subgraderconf in conf:
|
||||
subgraderconf = subgraderconf.copy()
|
||||
weight = subgraderconf.pop("weight", 0)
|
||||
# NOTE: 'name' used to exist in SingleSectionGrader. We are deprecating SingleSectionGrader
|
||||
# and converting everything into an AssignmentFormatGrader by adding 'min_count' and
|
||||
# 'drop_count'. AssignmentFormatGrader does not expect 'name', so if it appears
|
||||
# in bad_args, go ahead remove it (this causes no errors). Eventually, SingleSectionGrader
|
||||
# should be completely removed.
|
||||
name = 'name'
|
||||
try:
|
||||
if 'min_count' in subgraderconf:
|
||||
#This is an AssignmentFormatGrader
|
||||
subgrader_class = AssignmentFormatGrader
|
||||
elif 'name' in subgraderconf:
|
||||
elif name in subgraderconf:
|
||||
#This is an SingleSectionGrader
|
||||
subgrader_class = SingleSectionGrader
|
||||
else:
|
||||
raise ValueError("Configuration has no appropriate grader class.")
|
||||
|
||||
bad_args = invalid_args(subgrader_class.__init__, subgraderconf)
|
||||
# See note above concerning 'name'.
|
||||
if bad_args.issuperset({name}):
|
||||
bad_args = bad_args - {name}
|
||||
del subgraderconf[name]
|
||||
if len(bad_args) > 0:
|
||||
log.warning("Invalid arguments for a subgrader: %s", bad_args)
|
||||
for key in bad_args:
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
<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>
|
||||
@@ -8,25 +8,43 @@ describe 'Problem', ->
|
||||
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
|
||||
window.update_schematics = ->
|
||||
|
||||
# Load this function from spec/helper.coffee
|
||||
# Note that if your test fails with a message like:
|
||||
# 'External request attempted for blah, which is not defined.'
|
||||
# this msg is coming from the stubRequests function else clause.
|
||||
jasmine.stubRequests()
|
||||
|
||||
# note that the fixturesPath is set in spec/helper.coffee
|
||||
loadFixtures 'problem.html'
|
||||
|
||||
spyOn Logger, 'log'
|
||||
spyOn($.fn, 'load').andCallFake (url, callback) ->
|
||||
$(@).html readFixtures('problem_content.html')
|
||||
callback()
|
||||
jasmine.stubRequests()
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@problem.el).toBe '#problem_1'
|
||||
it 'set the element from html', ->
|
||||
@problem999 = new Problem ("
|
||||
<section class='xmodule_display xmodule_CapaModule' data-type='Problem'>
|
||||
<section id='problem_999'
|
||||
class='problems-wrapper'
|
||||
data-problem-id='i4x://edX/999/problem/Quiz'
|
||||
data-url='/problem/quiz/'>
|
||||
</section>
|
||||
</section>
|
||||
")
|
||||
expect(@problem999.element_id).toBe 'problem_999'
|
||||
|
||||
it 'set the element from loadFixtures', ->
|
||||
@problem1 = new Problem($('.xmodule_display'))
|
||||
expect(@problem1.element_id).toBe 'problem_1'
|
||||
|
||||
describe 'bind', ->
|
||||
beforeEach ->
|
||||
spyOn window, 'update_schematics'
|
||||
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
|
||||
it 'set mathjax typeset', ->
|
||||
expect(MathJax.Hub.Queue).toHaveBeenCalled()
|
||||
@@ -38,7 +56,7 @@ describe 'Problem', ->
|
||||
expect($('section.action input:button')).toHandleWith 'click', @problem.refreshAnswers
|
||||
|
||||
it 'bind the check button', ->
|
||||
expect($('section.action input.check')).toHandleWith 'click', @problem.check
|
||||
expect($('section.action input.check')).toHandleWith 'click', @problem.check_fd
|
||||
|
||||
it 'bind the reset button', ->
|
||||
expect($('section.action input.reset')).toHandleWith 'click', @problem.reset
|
||||
@@ -60,7 +78,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'render', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@bind = @problem.bind
|
||||
spyOn @problem, 'bind'
|
||||
|
||||
@@ -86,9 +104,13 @@ describe 'Problem', ->
|
||||
it 're-bind the content', ->
|
||||
expect(@problem.bind).toHaveBeenCalled()
|
||||
|
||||
describe 'check_fd', ->
|
||||
xit 'should have specs written for this functionality', ->
|
||||
expect(false)
|
||||
|
||||
describe 'check', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_check event', ->
|
||||
@@ -98,30 +120,34 @@ describe 'Problem', ->
|
||||
it 'submit the answer for check', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.check()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_check', 'foo=1&bar=2', jasmine.any(Function)
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_check',
|
||||
'foo=1&bar=2', jasmine.any(Function)
|
||||
|
||||
describe 'when the response is correct', ->
|
||||
it 'call render with returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'correct', contents: 'Correct!')
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
|
||||
callback(success: 'correct', contents: 'Correct!')
|
||||
@problem.check()
|
||||
expect(@problem.el.html()).toEqual 'Correct!'
|
||||
|
||||
describe 'when the response is incorrect', ->
|
||||
it 'call render with returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'incorrect', contents: 'Correct!')
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
|
||||
callback(success: 'incorrect', contents: 'Incorrect!')
|
||||
@problem.check()
|
||||
expect(@problem.el.html()).toEqual 'Correct!'
|
||||
expect(@problem.el.html()).toEqual 'Incorrect!'
|
||||
|
||||
describe 'when the response is undetermined', ->
|
||||
it 'alert the response', ->
|
||||
spyOn window, 'alert'
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'Number Only!')
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
|
||||
callback(success: 'Number Only!')
|
||||
@problem.check()
|
||||
expect(window.alert).toHaveBeenCalledWith 'Number Only!'
|
||||
|
||||
describe 'reset', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
|
||||
it 'log the problem_reset event', ->
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
@@ -131,7 +157,8 @@ describe 'Problem', ->
|
||||
it 'POST to the problem reset page', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.reset()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_reset', { id: 1 }, jasmine.any(Function)
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_reset',
|
||||
{ id: 'i4x://edX/101/problem/Problem1' }, jasmine.any(Function)
|
||||
|
||||
it 'render the returned content', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
|
||||
@@ -141,7 +168,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'show', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@problem.el.prepend '<div id="answer_1_1" /><div id="answer_1_2" />'
|
||||
|
||||
describe 'when the answer has not yet shown', ->
|
||||
@@ -150,12 +177,14 @@ describe 'Problem', ->
|
||||
|
||||
it 'log the problem_show event', ->
|
||||
@problem.show()
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_show', problem: 1
|
||||
expect(Logger.log).toHaveBeenCalledWith 'problem_show',
|
||||
problem: 'i4x://edX/101/problem/Problem1'
|
||||
|
||||
it 'fetch the answers', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.show()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_show', jasmine.any(Function)
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_show',
|
||||
jasmine.any(Function)
|
||||
|
||||
it 'show the answers', ->
|
||||
spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
|
||||
@@ -220,7 +249,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'save', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@problem.answers = 'foo=1&bar=2'
|
||||
|
||||
it 'log the problem_save event', ->
|
||||
@@ -230,7 +259,8 @@ describe 'Problem', ->
|
||||
it 'POST to save problem', ->
|
||||
spyOn $, 'postWithPrefix'
|
||||
@problem.save()
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/1/problem_save', 'foo=1&bar=2', jasmine.any(Function)
|
||||
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_save',
|
||||
'foo=1&bar=2', jasmine.any(Function)
|
||||
|
||||
it 'alert to the user', ->
|
||||
spyOn window, 'alert'
|
||||
@@ -240,7 +270,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'refreshMath', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
$('#input_example_1').val 'E=mc^2'
|
||||
@problem.refreshMath target: $('#input_example_1').get(0)
|
||||
|
||||
@@ -250,7 +280,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'updateMathML', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@stubbedJax.root.toMathML.andReturn '<MathML>'
|
||||
|
||||
describe 'when there is no exception', ->
|
||||
@@ -270,7 +300,7 @@ describe 'Problem', ->
|
||||
|
||||
describe 'refreshAnswers', ->
|
||||
beforeEach ->
|
||||
@problem = new Problem 1, "problem_1", "/problem/url/"
|
||||
@problem = new Problem($('.xmodule_display'))
|
||||
@problem.el.html '''
|
||||
<textarea class="CodeMirror" />
|
||||
<input id="input_1_1" name="input_1_1" class="schematic" value="one" />
|
||||
@@ -293,3 +323,6 @@ describe 'Problem', ->
|
||||
it 'serialize all answers', ->
|
||||
@problem.refreshAnswers()
|
||||
expect(@problem.answers).toEqual "input_1_1=one&input_1_2=two"
|
||||
|
||||
|
||||
|
||||
|
||||
76
common/lib/xmodule/xmodule/js/spec/helper.coffee
Normal file
76
common/lib/xmodule/xmodule/js/spec/helper.coffee
Normal file
@@ -0,0 +1,76 @@
|
||||
jasmine.getFixtures().fixturesPath = 'xmodule/js/fixtures'
|
||||
|
||||
jasmine.stubbedMetadata =
|
||||
slowerSpeedYoutubeId:
|
||||
id: 'slowerSpeedYoutubeId'
|
||||
duration: 300
|
||||
normalSpeedYoutubeId:
|
||||
id: 'normalSpeedYoutubeId'
|
||||
duration: 200
|
||||
bogus:
|
||||
duration: 100
|
||||
|
||||
jasmine.stubbedCaption =
|
||||
start: [0, 10000, 20000, 30000]
|
||||
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000']
|
||||
|
||||
jasmine.stubRequests = ->
|
||||
spyOn($, 'ajax').andCallFake (settings) ->
|
||||
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
|
||||
settings.success data: jasmine.stubbedMetadata[match[1]]
|
||||
else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/
|
||||
settings.success jasmine.stubbedCaption
|
||||
else if settings.url.match /.+\/problem_get$/
|
||||
settings.success html: readFixtures('problem_content.html')
|
||||
else if settings.url == '/calculate' ||
|
||||
settings.url.match(/.+\/goto_position$/) ||
|
||||
settings.url.match(/event$/) ||
|
||||
settings.url.match(/.+\/problem_(check|reset|show|save)$/)
|
||||
# do nothing
|
||||
else
|
||||
throw "External request attempted for #{settings.url}, which is not defined."
|
||||
|
||||
jasmine.stubYoutubePlayer = ->
|
||||
YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
|
||||
'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById',
|
||||
'playVideo', 'pauseVideo', 'seekTo']
|
||||
|
||||
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
|
||||
enableParts = [enableParts] unless $.isArray(enableParts)
|
||||
|
||||
suite = context.suite
|
||||
currentPartName = suite.description while suite = suite.parentSuite
|
||||
enableParts.push currentPartName
|
||||
|
||||
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider']
|
||||
unless $.inArray(part, enableParts) >= 0
|
||||
spyOn window, part
|
||||
|
||||
loadFixtures 'video.html'
|
||||
jasmine.stubRequests()
|
||||
YT.Player = undefined
|
||||
context.video = new Video 'example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
|
||||
jasmine.stubYoutubePlayer()
|
||||
if createPlayer
|
||||
return new VideoPlayer(video: context.video)
|
||||
|
||||
spyOn(window, 'onunload')
|
||||
|
||||
# Stub Youtube API
|
||||
window.YT =
|
||||
PlayerState:
|
||||
UNSTARTED: -1
|
||||
ENDED: 0
|
||||
PLAYING: 1
|
||||
PAUSED: 2
|
||||
BUFFERING: 3
|
||||
CUED: 5
|
||||
|
||||
# Stub jQuery.cookie
|
||||
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0'
|
||||
|
||||
# Stub jQuery.qtip
|
||||
$.fn.qtip = jasmine.createSpy 'jQuery.qtip'
|
||||
|
||||
# Stub jQuery.scrollTo
|
||||
$.fn.scrollTo = jasmine.createSpy 'jQuery.scrollTo'
|
||||
@@ -3,6 +3,7 @@ class @Video
|
||||
@el = $(element).find('.video')
|
||||
@id = @el.attr('id').replace(/video_/, '')
|
||||
@caption_data_dir = @el.data('caption-data-dir')
|
||||
@caption_asset_path = @el.data('caption-asset-path')
|
||||
@show_captions = @el.data('show-captions') == "true"
|
||||
window.player = null
|
||||
@el = $("#video_#{@id}")
|
||||
|
||||
@@ -10,7 +10,7 @@ class @VideoCaption extends Subview
|
||||
.bind('DOMMouseScroll', @onMovement)
|
||||
|
||||
captionURL: ->
|
||||
"/static/#{@captionDataDir}/subs/#{@youtubeId}.srt.sjson"
|
||||
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
|
||||
|
||||
render: ->
|
||||
# TODO: make it so you can have a video with no captions.
|
||||
|
||||
@@ -31,7 +31,7 @@ class @VideoPlayer extends Subview
|
||||
el: @el
|
||||
youtubeId: @video.youtubeId('1.0')
|
||||
currentSpeed: @currentSpeed()
|
||||
captionDataDir: @video.caption_data_dir
|
||||
captionAssetPath: @video.caption_asset_path
|
||||
unless onTouchBasedDevice()
|
||||
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
|
||||
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
|
||||
|
||||
@@ -153,7 +153,7 @@ class Location(_LocationBase):
|
||||
def check(val, regexp):
|
||||
if val is not None and regexp.search(val) is not None:
|
||||
log.debug('invalid characters val="%s", list_="%s"' % (val, list_))
|
||||
raise InvalidLocationError(location)
|
||||
raise InvalidLocationError("Invalid characters in '%s'." % (val))
|
||||
|
||||
list_ = list(list_)
|
||||
for val in list_[:4] + [list_[5]]:
|
||||
|
||||
@@ -53,6 +53,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
self.unnamed = defaultdict(int) # category -> num of new url_names for that category
|
||||
self.used_names = defaultdict(set) # category -> set of used url_names
|
||||
self.org, self.course, self.url_name = course_id.split('/')
|
||||
# cdodge: adding the course_id as passed in for later reference rather than having to recomine the org/course/url_name
|
||||
self.course_id = course_id
|
||||
self.load_error_modules = load_error_modules
|
||||
|
||||
def process_xml(xml):
|
||||
@@ -303,7 +305,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
try:
|
||||
course_descriptor = self.load_course(course_dir, errorlog.tracker)
|
||||
except Exception as e:
|
||||
msg = "Failed to load course '{0}': {1}".format(course_dir, str(e))
|
||||
msg = "ERROR: Failed to load course '{0}': {1}".format(course_dir, str(e))
|
||||
log.exception(msg)
|
||||
errorlog.tracker(msg)
|
||||
|
||||
@@ -337,7 +339,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
with open(policy_path) as f:
|
||||
return json.load(f)
|
||||
except (IOError, ValueError) as err:
|
||||
msg = "Error loading course policy from {0}".format(policy_path)
|
||||
msg = "ERROR: loading course policy from {0}".format(policy_path)
|
||||
tracker(msg)
|
||||
log.warning(msg + " " + str(err))
|
||||
return {}
|
||||
@@ -455,10 +457,18 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
slug = os.path.splitext(os.path.basename(filepath))[0]
|
||||
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
|
||||
module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
|
||||
# VS[compat]:
|
||||
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
|
||||
# from the course policy
|
||||
if category == "static_tab":
|
||||
for tab in course_descriptor.tabs or []:
|
||||
if tab.get('url_slug') == slug:
|
||||
module.metadata['display_name'] = tab['name']
|
||||
module.metadata['data_dir'] = course_dir
|
||||
self.modules[course_descriptor.id][module.location] = module
|
||||
except Exception, e:
|
||||
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
|
||||
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
|
||||
def get_instance(self, course_id, location, depth=0):
|
||||
"""
|
||||
|
||||
@@ -11,12 +11,12 @@ from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace):
|
||||
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace, subpath = 'static'):
|
||||
|
||||
remap_dict = {}
|
||||
|
||||
|
||||
# now import all static assets
|
||||
static_dir = course_data_path / 'static/'
|
||||
static_dir = course_data_path / subpath
|
||||
|
||||
for dirname, dirnames, filenames in os.walk(static_dir):
|
||||
for filename in filenames:
|
||||
@@ -24,6 +24,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
|
||||
try:
|
||||
content_path = os.path.join(dirname, filename)
|
||||
fullname_with_subpath = content_path.replace(static_dir, '') # strip away leading path from the name
|
||||
if fullname_with_subpath.startswith('/'):
|
||||
fullname_with_subpath = fullname_with_subpath[1:]
|
||||
content_loc = StaticContent.compute_location(target_location_namespace.org, target_location_namespace.course, fullname_with_subpath)
|
||||
mime_type = mimetypes.guess_type(filename)[0]
|
||||
|
||||
@@ -88,7 +90,7 @@ def verify_content_links(module, base_dir, static_content_store, link, remap_dic
|
||||
|
||||
def import_from_xml(store, data_dir, course_dirs=None,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True, static_content_store=None, target_location_namespace = None):
|
||||
load_error_modules=True, static_content_store=None, target_location_namespace=None):
|
||||
"""
|
||||
Import the specified xml data_dir into the "store" modulestore,
|
||||
using org and course as the location org and course.
|
||||
@@ -125,8 +127,11 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
course_location = module.location
|
||||
|
||||
if static_content_store is not None:
|
||||
_namespace_rename = target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
target_location_namespace if target_location_namespace is not None else module_store.modules[course_id].location)
|
||||
_namespace_rename, subpath='static')
|
||||
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
|
||||
@@ -159,6 +164,16 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
# HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
|
||||
module.metadata['hide_progress_tab'] = True
|
||||
|
||||
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
|
||||
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
|
||||
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
|
||||
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
|
||||
# so let's make sure we import in case there are no other references to it in the modules
|
||||
verify_content_links(module, course_data_path, static_content_store, '/static/images/course_image.jpg')
|
||||
@@ -192,7 +207,6 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
store.update_item(module.location, module_data)
|
||||
|
||||
|
||||
if 'children' in module.definition:
|
||||
store.update_children(module.location, module.definition['children'])
|
||||
|
||||
@@ -200,6 +214,100 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(module.own_metadata))
|
||||
|
||||
|
||||
|
||||
return module_store, course_items
|
||||
|
||||
|
||||
def validate_category_hierarcy(module_store, course_id, parent_category, expected_child_category):
|
||||
err_cnt = 0
|
||||
|
||||
parents = []
|
||||
# get all modules of parent_category
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
if module.location.category == parent_category:
|
||||
parents.append(module)
|
||||
|
||||
for parent in parents:
|
||||
for child_loc in [Location(child) for child in parent.definition.get('children', [])]:
|
||||
if child_loc.category != expected_child_category:
|
||||
err_cnt += 1
|
||||
print 'ERROR: child {0} of parent {1} was expected to be category of {2} but was {3}'.format(
|
||||
child_loc, parent.location, expected_child_category, child_loc.category)
|
||||
|
||||
return err_cnt
|
||||
|
||||
def validate_data_source_path_existence(path, is_err = True, extra_msg = None):
|
||||
_cnt = 0
|
||||
if not os.path.exists(path):
|
||||
print ("{0}: Expected folder at {1}. {2}".format('ERROR' if is_err == True else 'WARNING', path, extra_msg if
|
||||
extra_msg is not None else ''))
|
||||
_cnt = 1
|
||||
return _cnt
|
||||
|
||||
def validate_data_source_paths(data_dir, course_dir):
|
||||
# check that there is a '/static/' directory
|
||||
course_path = data_dir / course_dir
|
||||
err_cnt = 0
|
||||
warn_cnt = 0
|
||||
err_cnt += validate_data_source_path_existence(course_path / 'static')
|
||||
warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err = False,
|
||||
extra_msg = 'Video captions (if they are used) will not work unless they are static/subs.')
|
||||
return err_cnt, warn_cnt
|
||||
|
||||
|
||||
def perform_xlint(data_dir, course_dirs,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True):
|
||||
err_cnt = 0
|
||||
warn_cnt = 0
|
||||
|
||||
module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class=default_class,
|
||||
course_dirs=course_dirs,
|
||||
load_error_modules=load_error_modules
|
||||
)
|
||||
|
||||
# check all data source path information
|
||||
for course_dir in course_dirs:
|
||||
_err_cnt, _warn_cnt = validate_data_source_paths(path(data_dir), course_dir)
|
||||
err_cnt += _err_cnt
|
||||
warn_cnt += _warn_cnt
|
||||
|
||||
# first count all errors and warnings as part of the XMLModuleStore import
|
||||
for err_log in module_store._location_errors.itervalues():
|
||||
for err_log_entry in err_log.errors:
|
||||
msg = err_log_entry[0]
|
||||
if msg.startswith('ERROR:'):
|
||||
err_cnt+=1
|
||||
else:
|
||||
warn_cnt+=1
|
||||
|
||||
# then count outright all courses that failed to load at all
|
||||
for err_log in module_store.errored_courses.itervalues():
|
||||
for err_log_entry in err_log.errors:
|
||||
msg = err_log_entry[0]
|
||||
print msg
|
||||
if msg.startswith('ERROR:'):
|
||||
err_cnt+=1
|
||||
else:
|
||||
warn_cnt+=1
|
||||
|
||||
for course_id in module_store.modules.keys():
|
||||
# constrain that courses only have 'chapter' children
|
||||
err_cnt += validate_category_hierarcy(module_store, course_id, "course", "chapter")
|
||||
# constrain that chapters only have 'sequentials'
|
||||
err_cnt += validate_category_hierarcy(module_store, course_id, "chapter", "sequential")
|
||||
# constrain that sequentials only have 'verticals'
|
||||
err_cnt += validate_category_hierarcy(module_store, course_id, "sequential", "vertical")
|
||||
|
||||
print "\n\n------------------------------------------\nVALIDATION SUMMARY: {0} Errors {1} Warnings\n".format(err_cnt, warn_cnt)
|
||||
|
||||
if err_cnt > 0:
|
||||
print "This course is not suitable for importing. Please fix courseware according to specifications before importing."
|
||||
elif warn_cnt > 0:
|
||||
print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing"
|
||||
else:
|
||||
print "This course can be imported successfully."
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -127,8 +127,10 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
for child in xml_object:
|
||||
try:
|
||||
children.append(system.process_xml(etree.tostring(child)).location.url())
|
||||
except:
|
||||
except Exception, e:
|
||||
log.exception("Unable to load child when parsing Sequence. Continuing...")
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
continue
|
||||
return {'children': children}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from xmodule.raw_module import RawDescriptor
|
||||
from lxml import etree
|
||||
from mako.template import Template
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
import logging
|
||||
|
||||
class CustomTagModule(XModule):
|
||||
"""
|
||||
@@ -61,7 +61,7 @@ class CustomTagDescriptor(RawDescriptor):
|
||||
# cdodge: look up the template as a module
|
||||
template_loc = self.location._replace(category='custom_tag_template', name=template_name)
|
||||
|
||||
template_module = modulestore().get_item(template_loc)
|
||||
template_module = modulestore().get_instance(system.course_id, template_loc)
|
||||
template_module_data = template_module.definition['data']
|
||||
template = Template(template_module_data)
|
||||
return template.render(**params)
|
||||
|
||||
@@ -6,6 +6,9 @@ from pkg_resources import resource_string, resource_listdir
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -93,6 +96,13 @@ class VideoModule(XModule):
|
||||
return self.youtube
|
||||
|
||||
def get_html(self):
|
||||
if isinstance(modulestore(), MongoModuleStore) :
|
||||
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
|
||||
else:
|
||||
# VS[compat]
|
||||
# cdodge: filesystem static content support.
|
||||
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
|
||||
|
||||
return self.system.render_template('video.html', {
|
||||
'streams': self.video_list(),
|
||||
'id': self.location.html_id(),
|
||||
@@ -102,6 +112,7 @@ class VideoModule(XModule):
|
||||
'display_name': self.display_name,
|
||||
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
|
||||
'data_dir': self.metadata['data_dir'],
|
||||
'caption_asset_path': caption_asset_path,
|
||||
'show_captions': self.show_captions
|
||||
})
|
||||
|
||||
|
||||
@@ -108,7 +108,20 @@ class HTMLSnippet(object):
|
||||
|
||||
All of these will be loaded onto the page in the CMS
|
||||
"""
|
||||
return cls.js
|
||||
# cdodge: We've moved the xmodule.coffee script from an outside directory into the xmodule area of common
|
||||
# this means we need to make sure that all xmodules include this dependency which had been previously implicitly
|
||||
# fulfilled in a different area of code
|
||||
js = cls.js
|
||||
|
||||
if js is None:
|
||||
js = {}
|
||||
|
||||
if 'coffee' not in js:
|
||||
js['coffee'] = []
|
||||
|
||||
js['coffee'].append(resource_string(__name__, 'js/src/xmodule.coffee'))
|
||||
|
||||
return js
|
||||
|
||||
@classmethod
|
||||
def get_css(cls):
|
||||
|
||||
448
common/static/js/vendor/CodeMirror/css.js
vendored
Normal file
448
common/static/js/vendor/CodeMirror/css.js
vendored
Normal file
@@ -0,0 +1,448 @@
|
||||
CodeMirror.defineMode("css", function(config) {
|
||||
var indentUnit = config.indentUnit, type;
|
||||
|
||||
var atMediaTypes = keySet([
|
||||
"all", "aural", "braille", "handheld", "print", "projection", "screen",
|
||||
"tty", "tv", "embossed"
|
||||
]);
|
||||
|
||||
var atMediaFeatures = keySet([
|
||||
"width", "min-width", "max-width", "height", "min-height", "max-height",
|
||||
"device-width", "min-device-width", "max-device-width", "device-height",
|
||||
"min-device-height", "max-device-height", "aspect-ratio",
|
||||
"min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
|
||||
"min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
|
||||
"max-color", "color-index", "min-color-index", "max-color-index",
|
||||
"monochrome", "min-monochrome", "max-monochrome", "resolution",
|
||||
"min-resolution", "max-resolution", "scan", "grid"
|
||||
]);
|
||||
|
||||
var propertyKeywords = keySet([
|
||||
"align-content", "align-items", "align-self", "alignment-adjust",
|
||||
"alignment-baseline", "anchor-point", "animation", "animation-delay",
|
||||
"animation-direction", "animation-duration", "animation-iteration-count",
|
||||
"animation-name", "animation-play-state", "animation-timing-function",
|
||||
"appearance", "azimuth", "backface-visibility", "background",
|
||||
"background-attachment", "background-clip", "background-color",
|
||||
"background-image", "background-origin", "background-position",
|
||||
"background-repeat", "background-size", "baseline-shift", "binding",
|
||||
"bleed", "bookmark-label", "bookmark-level", "bookmark-state",
|
||||
"bookmark-target", "border", "border-bottom", "border-bottom-color",
|
||||
"border-bottom-left-radius", "border-bottom-right-radius",
|
||||
"border-bottom-style", "border-bottom-width", "border-collapse",
|
||||
"border-color", "border-image", "border-image-outset",
|
||||
"border-image-repeat", "border-image-slice", "border-image-source",
|
||||
"border-image-width", "border-left", "border-left-color",
|
||||
"border-left-style", "border-left-width", "border-radius", "border-right",
|
||||
"border-right-color", "border-right-style", "border-right-width",
|
||||
"border-spacing", "border-style", "border-top", "border-top-color",
|
||||
"border-top-left-radius", "border-top-right-radius", "border-top-style",
|
||||
"border-top-width", "border-width", "bottom", "box-decoration-break",
|
||||
"box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
|
||||
"caption-side", "clear", "clip", "color", "color-profile", "column-count",
|
||||
"column-fill", "column-gap", "column-rule", "column-rule-color",
|
||||
"column-rule-style", "column-rule-width", "column-span", "column-width",
|
||||
"columns", "content", "counter-increment", "counter-reset", "crop", "cue",
|
||||
"cue-after", "cue-before", "cursor", "direction", "display",
|
||||
"dominant-baseline", "drop-initial-after-adjust",
|
||||
"drop-initial-after-align", "drop-initial-before-adjust",
|
||||
"drop-initial-before-align", "drop-initial-size", "drop-initial-value",
|
||||
"elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis",
|
||||
"flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap",
|
||||
"float", "float-offset", "font", "font-feature-settings", "font-family",
|
||||
"font-kerning", "font-language-override", "font-size", "font-size-adjust",
|
||||
"font-stretch", "font-style", "font-synthesis", "font-variant",
|
||||
"font-variant-alternates", "font-variant-caps", "font-variant-east-asian",
|
||||
"font-variant-ligatures", "font-variant-numeric", "font-variant-position",
|
||||
"font-weight", "grid-cell", "grid-column", "grid-column-align",
|
||||
"grid-column-sizing", "grid-column-span", "grid-columns", "grid-flow",
|
||||
"grid-row", "grid-row-align", "grid-row-sizing", "grid-row-span",
|
||||
"grid-rows", "grid-template", "hanging-punctuation", "height", "hyphens",
|
||||
"icon", "image-orientation", "image-rendering", "image-resolution",
|
||||
"inline-box-align", "justify-content", "left", "letter-spacing",
|
||||
"line-break", "line-height", "line-stacking", "line-stacking-ruby",
|
||||
"line-stacking-shift", "line-stacking-strategy", "list-style",
|
||||
"list-style-image", "list-style-position", "list-style-type", "margin",
|
||||
"margin-bottom", "margin-left", "margin-right", "margin-top",
|
||||
"marker-offset", "marks", "marquee-direction", "marquee-loop",
|
||||
"marquee-play-count", "marquee-speed", "marquee-style", "max-height",
|
||||
"max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index",
|
||||
"nav-left", "nav-right", "nav-up", "opacity", "order", "orphans", "outline",
|
||||
"outline-color", "outline-offset", "outline-style", "outline-width",
|
||||
"overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y",
|
||||
"padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
|
||||
"page", "page-break-after", "page-break-before", "page-break-inside",
|
||||
"page-policy", "pause", "pause-after", "pause-before", "perspective",
|
||||
"perspective-origin", "pitch", "pitch-range", "play-during", "position",
|
||||
"presentation-level", "punctuation-trim", "quotes", "rendering-intent",
|
||||
"resize", "rest", "rest-after", "rest-before", "richness", "right",
|
||||
"rotation", "rotation-point", "ruby-align", "ruby-overhang",
|
||||
"ruby-position", "ruby-span", "size", "speak", "speak-as", "speak-header",
|
||||
"speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set",
|
||||
"tab-size", "table-layout", "target", "target-name", "target-new",
|
||||
"target-position", "text-align", "text-align-last", "text-decoration",
|
||||
"text-decoration-color", "text-decoration-line", "text-decoration-skip",
|
||||
"text-decoration-style", "text-emphasis", "text-emphasis-color",
|
||||
"text-emphasis-position", "text-emphasis-style", "text-height",
|
||||
"text-indent", "text-justify", "text-outline", "text-shadow",
|
||||
"text-space-collapse", "text-transform", "text-underline-position",
|
||||
"text-wrap", "top", "transform", "transform-origin", "transform-style",
|
||||
"transition", "transition-delay", "transition-duration",
|
||||
"transition-property", "transition-timing-function", "unicode-bidi",
|
||||
"vertical-align", "visibility", "voice-balance", "voice-duration",
|
||||
"voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
|
||||
"voice-volume", "volume", "white-space", "widows", "width", "word-break",
|
||||
"word-spacing", "word-wrap", "z-index"
|
||||
]);
|
||||
|
||||
var colorKeywords = keySet([
|
||||
"black", "silver", "gray", "white", "maroon", "red", "purple", "fuchsia",
|
||||
"green", "lime", "olive", "yellow", "navy", "blue", "teal", "aqua"
|
||||
]);
|
||||
|
||||
var valueKeywords = keySet([
|
||||
"above", "absolute", "activeborder", "activecaption", "afar",
|
||||
"after-white-space", "ahead", "alias", "all", "all-scroll", "alternate",
|
||||
"always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
|
||||
"arabic-indic", "armenian", "asterisks", "auto", "avoid", "background",
|
||||
"backwards", "baseline", "below", "bidi-override", "binary", "bengali",
|
||||
"blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
|
||||
"both", "bottom", "break-all", "break-word", "button", "button-bevel",
|
||||
"buttonface", "buttonhighlight", "buttonshadow", "buttontext", "cambodian",
|
||||
"capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
|
||||
"cell", "center", "checkbox", "circle", "cjk-earthly-branch",
|
||||
"cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
|
||||
"col-resize", "collapse", "compact", "condensed", "contain", "content",
|
||||
"content-box", "context-menu", "continuous", "copy", "cover", "crop",
|
||||
"cross", "crosshair", "currentcolor", "cursive", "dashed", "decimal",
|
||||
"decimal-leading-zero", "default", "default-button", "destination-atop",
|
||||
"destination-in", "destination-out", "destination-over", "devanagari",
|
||||
"disc", "discard", "document", "dot-dash", "dot-dot-dash", "dotted",
|
||||
"double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
|
||||
"element", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
|
||||
"ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
|
||||
"ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
|
||||
"ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
|
||||
"ethiopic-halehame-gez", "ethiopic-halehame-om-et",
|
||||
"ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
|
||||
"ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et",
|
||||
"ethiopic-halehame-tig", "ew-resize", "expanded", "extra-condensed",
|
||||
"extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "footnotes",
|
||||
"forwards", "from", "geometricPrecision", "georgian", "graytext", "groove",
|
||||
"gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew",
|
||||
"help", "hidden", "hide", "higher", "highlight", "highlighttext",
|
||||
"hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore",
|
||||
"inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
|
||||
"infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
|
||||
"inline-block", "inline-table", "inset", "inside", "intrinsic", "invert",
|
||||
"italic", "justify", "kannada", "katakana", "katakana-iroha", "khmer",
|
||||
"landscape", "lao", "large", "larger", "left", "level", "lighter",
|
||||
"line-through", "linear", "lines", "list-item", "listbox", "listitem",
|
||||
"local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
|
||||
"lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
|
||||
"lower-roman", "lowercase", "ltr", "malayalam", "match",
|
||||
"media-controls-background", "media-current-time-display",
|
||||
"media-fullscreen-button", "media-mute-button", "media-play-button",
|
||||
"media-return-to-realtime-button", "media-rewind-button",
|
||||
"media-seek-back-button", "media-seek-forward-button", "media-slider",
|
||||
"media-sliderthumb", "media-time-remaining-display", "media-volume-slider",
|
||||
"media-volume-slider-container", "media-volume-sliderthumb", "medium",
|
||||
"menu", "menulist", "menulist-button", "menulist-text",
|
||||
"menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic",
|
||||
"mix", "mongolian", "monospace", "move", "multiple", "myanmar", "n-resize",
|
||||
"narrower", "navy", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
|
||||
"no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
|
||||
"ns-resize", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote",
|
||||
"optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
|
||||
"outside", "overlay", "overline", "padding", "padding-box", "painted",
|
||||
"paused", "persian", "plus-darker", "plus-lighter", "pointer", "portrait",
|
||||
"pre", "pre-line", "pre-wrap", "preserve-3d", "progress", "push-button",
|
||||
"radio", "read-only", "read-write", "read-write-plaintext-only", "relative",
|
||||
"repeat", "repeat-x", "repeat-y", "reset", "reverse", "rgb", "rgba",
|
||||
"ridge", "right", "round", "row-resize", "rtl", "run-in", "running",
|
||||
"s-resize", "sans-serif", "scroll", "scrollbar", "se-resize", "searchfield",
|
||||
"searchfield-cancel-button", "searchfield-decoration",
|
||||
"searchfield-results-button", "searchfield-results-decoration",
|
||||
"semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
|
||||
"single", "skip-white-space", "slide", "slider-horizontal",
|
||||
"slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
|
||||
"small", "small-caps", "small-caption", "smaller", "solid", "somali",
|
||||
"source-atop", "source-in", "source-out", "source-over", "space", "square",
|
||||
"square-button", "start", "static", "status-bar", "stretch", "stroke",
|
||||
"sub", "subpixel-antialiased", "super", "sw-resize", "table",
|
||||
"table-caption", "table-cell", "table-column", "table-column-group",
|
||||
"table-footer-group", "table-header-group", "table-row", "table-row-group",
|
||||
"telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
|
||||
"thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
|
||||
"threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
|
||||
"tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
|
||||
"transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
|
||||
"upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
|
||||
"upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
|
||||
"vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
|
||||
"visibleStroke", "visual", "w-resize", "wait", "wave", "white", "wider",
|
||||
"window", "windowframe", "windowtext", "x-large", "x-small", "xor",
|
||||
"xx-large", "xx-small", "yellow"
|
||||
]);
|
||||
|
||||
function keySet(array) { var keys = {}; for (var i = 0; i < array.length; ++i) keys[array[i]] = true; return keys; }
|
||||
function ret(style, tp) {type = tp; return style;}
|
||||
|
||||
function tokenBase(stream, state) {
|
||||
var ch = stream.next();
|
||||
if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("def", stream.current());}
|
||||
else if (ch == "/" && stream.eat("*")) {
|
||||
state.tokenize = tokenCComment;
|
||||
return tokenCComment(stream, state);
|
||||
}
|
||||
else if (ch == "<" && stream.eat("!")) {
|
||||
state.tokenize = tokenSGMLComment;
|
||||
return tokenSGMLComment(stream, state);
|
||||
}
|
||||
else if (ch == "=") ret(null, "compare");
|
||||
else if ((ch == "~" || ch == "|") && stream.eat("=")) return ret(null, "compare");
|
||||
else if (ch == "\"" || ch == "'") {
|
||||
state.tokenize = tokenString(ch);
|
||||
return state.tokenize(stream, state);
|
||||
}
|
||||
else if (ch == "#") {
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
return ret("atom", "hash");
|
||||
}
|
||||
else if (ch == "!") {
|
||||
stream.match(/^\s*\w*/);
|
||||
return ret("keyword", "important");
|
||||
}
|
||||
else if (/\d/.test(ch)) {
|
||||
stream.eatWhile(/[\w.%]/);
|
||||
return ret("number", "unit");
|
||||
}
|
||||
else if (ch === "-") {
|
||||
if (/\d/.test(stream.peek())) {
|
||||
stream.eatWhile(/[\w.%]/);
|
||||
return ret("number", "unit");
|
||||
} else if (stream.match(/^[^-]+-/)) {
|
||||
return ret("meta", type);
|
||||
}
|
||||
}
|
||||
else if (/[,+>*\/]/.test(ch)) {
|
||||
return ret(null, "select-op");
|
||||
}
|
||||
else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
|
||||
return ret("qualifier", type);
|
||||
}
|
||||
else if (ch == ":") {
|
||||
return ret("operator", ch);
|
||||
}
|
||||
else if (/[;{}\[\]\(\)]/.test(ch)) {
|
||||
return ret(null, ch);
|
||||
}
|
||||
else {
|
||||
stream.eatWhile(/[\w\\\-]/);
|
||||
return ret("property", "variable");
|
||||
}
|
||||
}
|
||||
|
||||
function tokenCComment(stream, state) {
|
||||
var maybeEnd = false, ch;
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (maybeEnd && ch == "/") {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
maybeEnd = (ch == "*");
|
||||
}
|
||||
return ret("comment", "comment");
|
||||
}
|
||||
|
||||
function tokenSGMLComment(stream, state) {
|
||||
var dashes = 0, ch;
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (dashes >= 2 && ch == ">") {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
dashes = (ch == "-") ? dashes + 1 : 0;
|
||||
}
|
||||
return ret("comment", "comment");
|
||||
}
|
||||
|
||||
function tokenString(quote) {
|
||||
return function(stream, state) {
|
||||
var escaped = false, ch;
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch == quote && !escaped)
|
||||
break;
|
||||
escaped = !escaped && ch == "\\";
|
||||
}
|
||||
if (!escaped) state.tokenize = tokenBase;
|
||||
return ret("string", "string");
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
startState: function(base) {
|
||||
return {tokenize: tokenBase,
|
||||
baseIndent: base || 0,
|
||||
stack: []};
|
||||
},
|
||||
|
||||
token: function(stream, state) {
|
||||
|
||||
// Use these terms when applicable (see http://www.xanthir.com/blog/b4E50)
|
||||
//
|
||||
// rule** or **ruleset:
|
||||
// A selector + braces combo, or an at-rule.
|
||||
//
|
||||
// declaration block:
|
||||
// A sequence of declarations.
|
||||
//
|
||||
// declaration:
|
||||
// A property + colon + value combo.
|
||||
//
|
||||
// property value:
|
||||
// The entire value of a property.
|
||||
//
|
||||
// component value:
|
||||
// A single piece of a property value. Like the 5px in
|
||||
// text-shadow: 0 0 5px blue;. Can also refer to things that are
|
||||
// multiple terms, like the 1-4 terms that make up the background-size
|
||||
// portion of the background shorthand.
|
||||
//
|
||||
// term:
|
||||
// The basic unit of author-facing CSS, like a single number (5),
|
||||
// dimension (5px), string ("foo"), or function. Officially defined
|
||||
// by the CSS 2.1 grammar (look for the 'term' production)
|
||||
//
|
||||
//
|
||||
// simple selector:
|
||||
// A single atomic selector, like a type selector, an attr selector, a
|
||||
// class selector, etc.
|
||||
//
|
||||
// compound selector:
|
||||
// One or more simple selectors without a combinator. div.example is
|
||||
// compound, div > .example is not.
|
||||
//
|
||||
// complex selector:
|
||||
// One or more compound selectors chained with combinators.
|
||||
//
|
||||
// combinator:
|
||||
// The parts of selectors that express relationships. There are four
|
||||
// currently - the space (descendant combinator), the greater-than
|
||||
// bracket (child combinator), the plus sign (next sibling combinator),
|
||||
// and the tilda (following sibling combinator).
|
||||
//
|
||||
// sequence of selectors:
|
||||
// One or more of the named type of selector chained with commas.
|
||||
|
||||
if (stream.eatSpace()) return null;
|
||||
var style = state.tokenize(stream, state);
|
||||
|
||||
// Changing style returned based on context
|
||||
var context = state.stack[state.stack.length-1];
|
||||
if (style == "property") {
|
||||
if (context == "propertyValue"){
|
||||
if (valueKeywords[stream.current()]) {
|
||||
style = "string-2";
|
||||
} else if (colorKeywords[stream.current()]) {
|
||||
style = "keyword";
|
||||
} else {
|
||||
style = "variable-2";
|
||||
}
|
||||
} else if (context == "rule") {
|
||||
if (!propertyKeywords[stream.current()]) {
|
||||
style += " error";
|
||||
}
|
||||
} else if (!context || context == "@media{") {
|
||||
style = "tag";
|
||||
} else if (context == "@media") {
|
||||
if (atMediaTypes[stream.current()]) {
|
||||
style = "attribute"; // Known attribute
|
||||
} else if (/^(only|not)$/i.test(stream.current())) {
|
||||
style = "keyword";
|
||||
} else if (stream.current().toLowerCase() == "and") {
|
||||
style = "error"; // "and" is only allowed in @mediaType
|
||||
} else if (atMediaFeatures[stream.current()]) {
|
||||
style = "error"; // Known property, should be in @mediaType(
|
||||
} else {
|
||||
// Unknown, expecting keyword or attribute, assuming attribute
|
||||
style = "attribute error";
|
||||
}
|
||||
} else if (context == "@mediaType") {
|
||||
if (atMediaTypes[stream.current()]) {
|
||||
style = "attribute";
|
||||
} else if (stream.current().toLowerCase() == "and") {
|
||||
style = "operator";
|
||||
} else if (/^(only|not)$/i.test(stream.current())) {
|
||||
style = "error"; // Only allowed in @media
|
||||
} else if (atMediaFeatures[stream.current()]) {
|
||||
style = "error"; // Known property, should be in parentheses
|
||||
} else {
|
||||
// Unknown attribute or property, but expecting property (preceded
|
||||
// by "and"). Should be in parentheses
|
||||
style = "error";
|
||||
}
|
||||
} else if (context == "@mediaType(") {
|
||||
if (propertyKeywords[stream.current()]) {
|
||||
// do nothing, remains "property"
|
||||
} else if (atMediaTypes[stream.current()]) {
|
||||
style = "error"; // Known property, should be in parentheses
|
||||
} else if (stream.current().toLowerCase() == "and") {
|
||||
style = "operator";
|
||||
} else if (/^(only|not)$/i.test(stream.current())) {
|
||||
style = "error"; // Only allowed in @media
|
||||
} else {
|
||||
style += " error";
|
||||
}
|
||||
} else {
|
||||
style = "error";
|
||||
}
|
||||
} else if (style == "atom") {
|
||||
if(!context || context == "@media{") {
|
||||
style = "builtin";
|
||||
} else if (context == "propertyValue") {
|
||||
if (!/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) {
|
||||
style += " error";
|
||||
}
|
||||
} else {
|
||||
style = "error";
|
||||
}
|
||||
} else if (context == "@media" && type == "{") {
|
||||
style = "error";
|
||||
}
|
||||
|
||||
// Push/pop context stack
|
||||
if (type == "{") {
|
||||
if (context == "@media" || context == "@mediaType") {
|
||||
state.stack.pop();
|
||||
state.stack[state.stack.length-1] = "@media{";
|
||||
}
|
||||
else state.stack.push("rule");
|
||||
}
|
||||
else if (type == "}") {
|
||||
state.stack.pop();
|
||||
if (context == "propertyValue") state.stack.pop();
|
||||
}
|
||||
else if (type == "@media") state.stack.push("@media");
|
||||
else if (context == "@media" && /\b(keyword|attribute)\b/.test(style))
|
||||
state.stack.push("@mediaType");
|
||||
else if (context == "@mediaType" && stream.current() == ",") state.stack.pop();
|
||||
else if (context == "@mediaType" && type == "(") state.stack.push("@mediaType(");
|
||||
else if (context == "@mediaType(" && type == ")") state.stack.pop();
|
||||
else if (context == "rule" && type == ":") state.stack.push("propertyValue");
|
||||
else if (context == "propertyValue" && type == ";") state.stack.pop();
|
||||
return style;
|
||||
},
|
||||
|
||||
indent: function(state, textAfter) {
|
||||
var n = state.stack.length;
|
||||
if (/^\}/.test(textAfter))
|
||||
n -= state.stack[state.stack.length-1] == "propertyValue" ? 2 : 1;
|
||||
return state.baseIndent + n * indentUnit;
|
||||
},
|
||||
|
||||
electricChars: "}"
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/css", "css");
|
||||
84
common/static/js/vendor/CodeMirror/htmlmixed.js
vendored
Normal file
84
common/static/js/vendor/CodeMirror/htmlmixed.js
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
CodeMirror.defineMode("htmlmixed", function(config) {
|
||||
var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true});
|
||||
var jsMode = CodeMirror.getMode(config, "javascript");
|
||||
var cssMode = CodeMirror.getMode(config, "css");
|
||||
|
||||
function html(stream, state) {
|
||||
var style = htmlMode.token(stream, state.htmlState);
|
||||
if (style == "tag" && stream.current() == ">" && state.htmlState.context) {
|
||||
if (/^script$/i.test(state.htmlState.context.tagName)) {
|
||||
state.token = javascript;
|
||||
state.localState = jsMode.startState(htmlMode.indent(state.htmlState, ""));
|
||||
}
|
||||
else if (/^style$/i.test(state.htmlState.context.tagName)) {
|
||||
state.token = css;
|
||||
state.localState = cssMode.startState(htmlMode.indent(state.htmlState, ""));
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}
|
||||
function maybeBackup(stream, pat, style) {
|
||||
var cur = stream.current();
|
||||
var close = cur.search(pat), m;
|
||||
if (close > -1) stream.backUp(cur.length - close);
|
||||
else if (m = cur.match(/<\/?$/)) {
|
||||
stream.backUp(cur[0].length);
|
||||
if (!stream.match(pat, false)) stream.match(cur[0]);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
function javascript(stream, state) {
|
||||
if (stream.match(/^<\/\s*script\s*>/i, false)) {
|
||||
state.token = html;
|
||||
state.localState = null;
|
||||
return html(stream, state);
|
||||
}
|
||||
return maybeBackup(stream, /<\/\s*script\s*>/,
|
||||
jsMode.token(stream, state.localState));
|
||||
}
|
||||
function css(stream, state) {
|
||||
if (stream.match(/^<\/\s*style\s*>/i, false)) {
|
||||
state.token = html;
|
||||
state.localState = null;
|
||||
return html(stream, state);
|
||||
}
|
||||
return maybeBackup(stream, /<\/\s*style\s*>/,
|
||||
cssMode.token(stream, state.localState));
|
||||
}
|
||||
|
||||
return {
|
||||
startState: function() {
|
||||
var state = htmlMode.startState();
|
||||
return {token: html, localState: null, mode: "html", htmlState: state};
|
||||
},
|
||||
|
||||
copyState: function(state) {
|
||||
if (state.localState)
|
||||
var local = CodeMirror.copyState(state.token == css ? cssMode : jsMode, state.localState);
|
||||
return {token: state.token, localState: local, mode: state.mode,
|
||||
htmlState: CodeMirror.copyState(htmlMode, state.htmlState)};
|
||||
},
|
||||
|
||||
token: function(stream, state) {
|
||||
return state.token(stream, state);
|
||||
},
|
||||
|
||||
indent: function(state, textAfter) {
|
||||
if (state.token == html || /^\s*<\//.test(textAfter))
|
||||
return htmlMode.indent(state.htmlState, textAfter);
|
||||
else if (state.token == javascript)
|
||||
return jsMode.indent(state.localState, textAfter);
|
||||
else
|
||||
return cssMode.indent(state.localState, textAfter);
|
||||
},
|
||||
|
||||
electricChars: "/{}:",
|
||||
|
||||
innerMode: function(state) {
|
||||
var mode = state.token == html ? htmlMode : state.token == javascript ? jsMode : cssMode;
|
||||
return {state: state.localState || state.htmlState, mode: mode};
|
||||
}
|
||||
};
|
||||
}, "xml", "javascript", "css");
|
||||
|
||||
CodeMirror.defineMIME("text/html", "htmlmixed");
|
||||
@@ -148,7 +148,7 @@ def get_course_about_section(course, section_key):
|
||||
request = get_request_for_thread()
|
||||
|
||||
loc = course.location._replace(category='about', name=section_key)
|
||||
course_module = get_module(request.user, request, loc, None, course.id, not_found_ok = True)
|
||||
course_module = get_module(request.user, request, loc, None, course.id, not_found_ok = True, wrap_xmodule_display = False)
|
||||
|
||||
html = ''
|
||||
|
||||
@@ -186,8 +186,7 @@ def get_course_info_section(request, cache, course, section_key):
|
||||
|
||||
|
||||
loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
|
||||
course_module = get_module(request.user, request, loc, cache, course.id)
|
||||
|
||||
course_module = get_module(request.user, request, loc, cache, course.id, wrap_xmodule_display = False)
|
||||
html = ''
|
||||
|
||||
if course_module is not None:
|
||||
@@ -196,7 +195,6 @@ def get_course_info_section(request, cache, course, section_key):
|
||||
return html
|
||||
|
||||
|
||||
|
||||
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
|
||||
# arjun will address this by the end of October if no one does so prior to
|
||||
# then.
|
||||
@@ -222,7 +220,7 @@ def get_course_syllabus_section(course, section_key):
|
||||
filepath = find_file(fs, dirs, section_key + ".html")
|
||||
with fs.open(filepath) as htmlFile:
|
||||
return replace_urls(htmlFile.read().decode('utf-8'),
|
||||
course.metadata['data_dir'])
|
||||
course.metadata['data_dir'], course_namespace=course.location)
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing syllabus section {key} in course {url}".format(
|
||||
key=section_key, url=course.location.url()))
|
||||
|
||||
@@ -115,7 +115,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
return chapters
|
||||
|
||||
|
||||
def get_module(user, request, location, student_module_cache, course_id, position=None, not_found_ok = False):
|
||||
def get_module(user, request, location, student_module_cache, course_id, position=None, not_found_ok = False, wrap_xmodule_display = True):
|
||||
"""
|
||||
Get an instance of the xmodule class identified by location,
|
||||
setting the state based on an existing StudentModule, or creating one if none
|
||||
@@ -136,7 +136,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
|
||||
if possible. If not possible, return None.
|
||||
"""
|
||||
try:
|
||||
return _get_module(user, request, location, student_module_cache, course_id, position)
|
||||
return _get_module(user, request, location, student_module_cache, course_id, position, wrap_xmodule_display)
|
||||
except ItemNotFoundError:
|
||||
if not not_found_ok:
|
||||
log.exception("Error in get_module")
|
||||
@@ -146,7 +146,7 @@ def get_module(user, request, location, student_module_cache, course_id, positio
|
||||
log.exception("Error in get_module")
|
||||
return None
|
||||
|
||||
def _get_module(user, request, location, student_module_cache, course_id, position=None):
|
||||
def _get_module(user, request, location, student_module_cache, course_id, position=None, wrap_xmodule_display = True):
|
||||
"""
|
||||
Actually implement get_module. See docstring there for details.
|
||||
"""
|
||||
@@ -261,8 +261,13 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
# Make an error module
|
||||
return err_descriptor.xmodule_constructor(system)(None, None)
|
||||
|
||||
_get_html = module.get_html
|
||||
|
||||
if wrap_xmodule_display == True:
|
||||
_get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html')
|
||||
|
||||
module.get_html = replace_static_urls(
|
||||
wrap_xmodule(module.get_html, module, 'xmodule_display.html'),
|
||||
_get_html,
|
||||
module.metadata['data_dir'] if 'data_dir' in module.metadata else '',
|
||||
course_namespace = module.location._replace(category=None, name=None))
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log",
|
||||
logging_env="dev",
|
||||
tracking_filename="tracking.log",
|
||||
dev_env=True,
|
||||
debug=True)
|
||||
debug=True,
|
||||
local_loglevel='ERROR',
|
||||
console_loglevel='ERROR')
|
||||
|
||||
PIPELINE_JS['js-test-source'] = {
|
||||
'source_filenames': sum([
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2> ${display_name} </h2>
|
||||
% endif
|
||||
|
||||
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-data-dir="${data_dir}" data-show-captions="${show_captions}">
|
||||
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-data-dir="${data_dir}" data-caption-asset-path="${caption_asset_path}" data-show-captions="${show_captions}">
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
|
||||
92
rakefile
92
rakefile
@@ -3,6 +3,8 @@ require 'tempfile'
|
||||
require 'net/http'
|
||||
require 'launchy'
|
||||
require 'colorize'
|
||||
require 'erb'
|
||||
require 'tempfile'
|
||||
|
||||
# Build Constants
|
||||
REPO_ROOT = File.dirname(__FILE__)
|
||||
@@ -47,7 +49,7 @@ def django_for_jasmine(system, django_reload)
|
||||
end
|
||||
|
||||
django_pid = fork do
|
||||
exec(*django_admin(system, 'jasmine', 'runserver', "12345", reload_arg).split(' '))
|
||||
exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', "12345", reload_arg).split(' '))
|
||||
end
|
||||
jasmine_url = 'http://localhost:12345/_jasmine/'
|
||||
up = false
|
||||
@@ -79,6 +81,31 @@ def django_for_jasmine(system, django_reload)
|
||||
end
|
||||
end
|
||||
|
||||
def template_jasmine_runner(lib)
|
||||
coffee_files = Dir["#{lib}/**/js/**/*.coffee", "common/static/coffee/src/**/*.coffee"]
|
||||
if !coffee_files.empty?
|
||||
sh("coffee -c #{coffee_files.join(' ')}")
|
||||
end
|
||||
phantom_jasmine_path = File.expand_path("common/test/phantom-jasmine")
|
||||
common_js_root = File.expand_path("common/static/js")
|
||||
common_coffee_root = File.expand_path("common/static/coffee/src")
|
||||
|
||||
# Get arrays of spec and source files, ordered by how deep they are nested below the library
|
||||
# (and then alphabetically) and expanded from a relative to an absolute path
|
||||
spec_glob = File.join("#{lib}", "**", "spec", "**", "*.js")
|
||||
src_glob = File.join("#{lib}", "**", "src", "**", "*.js")
|
||||
js_specs = Dir[spec_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)}
|
||||
js_source = Dir[src_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)}
|
||||
|
||||
template = ERB.new(File.read("#{lib}/jasmine_test_runner.html.erb"))
|
||||
template_output = "#{lib}/jasmine_test_runner.html"
|
||||
File.open(template_output, 'w') do |f|
|
||||
f.write(template.result(binding))
|
||||
end
|
||||
yield File.expand_path(template_output)
|
||||
end
|
||||
|
||||
|
||||
def report_dir_path(dir)
|
||||
return File.join(REPORT_DIR, dir.to_s)
|
||||
end
|
||||
@@ -126,22 +153,6 @@ end
|
||||
end
|
||||
task :pylint => "pylint_#{system}"
|
||||
|
||||
desc "Open jasmine tests in your default browser"
|
||||
task "browse_jasmine_#{system}" do
|
||||
django_for_jasmine(system, true) do |jasmine_url|
|
||||
Launchy.open(jasmine_url)
|
||||
puts "Press ENTER to terminate".red
|
||||
$stdin.gets
|
||||
end
|
||||
end
|
||||
|
||||
desc "Use phantomjs to run jasmine tests from the console"
|
||||
task "phantomjs_jasmine_#{system}" do
|
||||
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
|
||||
django_for_jasmine(system, false) do |jasmine_url|
|
||||
sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
$failed_tests = 0
|
||||
@@ -210,6 +221,23 @@ TEST_TASK_DIRS = []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Open jasmine tests for #{system} in your default browser"
|
||||
task "browse_jasmine_#{system}" do
|
||||
django_for_jasmine(system, true) do |jasmine_url|
|
||||
Launchy.open(jasmine_url)
|
||||
puts "Press ENTER to terminate".red
|
||||
$stdin.gets
|
||||
end
|
||||
end
|
||||
|
||||
desc "Use phantomjs to run jasmine tests for #{system} from the console"
|
||||
task "phantomjs_jasmine_#{system}" do
|
||||
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
|
||||
django_for_jasmine(system, false) do |jasmine_url|
|
||||
sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Reset the relational database used by django. WARNING: this will delete all of your existing users"
|
||||
@@ -245,6 +273,22 @@ Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
|
||||
sh("nosetests #{lib}")
|
||||
end
|
||||
|
||||
desc "Open jasmine tests for #{lib} in your default browser"
|
||||
task "browse_jasmine_#{lib}" do
|
||||
template_jasmine_runner(lib) do |f|
|
||||
sh("python -m webbrowser -t 'file://#{f}'")
|
||||
puts "Press ENTER to terminate".red
|
||||
$stdin.gets
|
||||
end
|
||||
end
|
||||
|
||||
desc "Use phantomjs to run jasmine tests for #{lib} from the console"
|
||||
task "phantomjs_jasmine_#{lib}" do
|
||||
phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs'
|
||||
template_jasmine_runner(lib) do |f|
|
||||
sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{f}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
task :report_dirs
|
||||
@@ -364,6 +408,20 @@ namespace :cms do
|
||||
end
|
||||
end
|
||||
|
||||
namespace :cms do
|
||||
desc "Import course data within the given DATA_DIR variable"
|
||||
task :xlint do
|
||||
if ENV['DATA_DIR'] and ENV['COURSE_DIR']
|
||||
sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'], ENV['COURSE_DIR']))
|
||||
elsif ENV['DATA_DIR']
|
||||
sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR']))
|
||||
else
|
||||
raise "Please specify a DATA_DIR variable that point to your data directory.\n" +
|
||||
"Example: \`rake cms:import DATA_DIR=../data\`"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Build a properties file used to trigger autodeploy builds"
|
||||
task :autodeploy_properties do
|
||||
File.open("autodeploy.properties", "w") do |file|
|
||||
|
||||
@@ -55,3 +55,4 @@ dogstatsd-python
|
||||
# Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs.
|
||||
# MySQL-python
|
||||
sphinx
|
||||
factory_boy
|
||||
|
||||
@@ -3,3 +3,4 @@ coverage
|
||||
nosexcover
|
||||
pylint
|
||||
pep8
|
||||
factory_boy
|
||||
|
||||
Reference in New Issue
Block a user