Commit 1bfb9335 by Calen Pennington

Merge remote-tracking branch 'origin/master' into feature/alex/poll-merged

Conflicts:
	cms/djangoapps/contentstore/tests/test_contentstore.py
	cms/djangoapps/contentstore/views.py
	cms/templates/edit_subsection.html
	cms/templates/overview.html
	cms/templates/unit.html
	cms/templates/widgets/header.html
	common/djangoapps/course_groups/tests/tests.py
	common/lib/xmodule/setup.py
	common/lib/xmodule/xmodule/capa_module.py
	common/lib/xmodule/xmodule/combined_open_ended_module.py
	common/lib/xmodule/xmodule/course_module.py
	common/lib/xmodule/xmodule/js/src/.gitignore
	common/lib/xmodule/xmodule/mako_module.py
	common/lib/xmodule/xmodule/modulestore/mongo.py
	common/lib/xmodule/xmodule/modulestore/store_utilities.py
	common/lib/xmodule/xmodule/modulestore/xml_exporter.py
	common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
	common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py
	common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
	common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
	common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
	common/lib/xmodule/xmodule/peer_grading_module.py
	common/lib/xmodule/xmodule/tests/__init__.py
	common/lib/xmodule/xmodule/tests/test_capa_module.py
	common/lib/xmodule/xmodule/tests/test_self_assessment.py
	common/lib/xmodule/xmodule/x_module.py
	lms/djangoapps/courseware/grades.py
	lms/djangoapps/courseware/models.py
	lms/djangoapps/courseware/views.py
	lms/djangoapps/open_ended_grading/tests.py
parents 908bfa3c 9d2a2cbc
......@@ -27,4 +27,5 @@ lms/lib/comment_client/python
nosetests.xml
cover_html/
.idea/
.redcar/
chromedriver.log
\ No newline at end of file
......@@ -2,7 +2,7 @@
[run]
data_file = reports/cms/.coverage
source = cms,common/djangoapps
omit = cms/envs/*, cms/manage.py
omit = cms/envs/*, cms/manage.py, common/djangoapps/terrain/*, common/djangoapps/*/migrations/*
[report]
ignore_errors = True
......
Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key/value pairs
Scenario: A course author sees only display_name on a newly created course
Given I have opened a new course in Studio
When I select the Advanced Settings
Then I see only the display name
Scenario: Test if there are no policy settings without existing UI controls
Given I am on the Advanced Course Settings page in Studio
When I delete the display name
Then there are no advanced policy settings
And I reload the page
Then there are no advanced policy settings
Scenario: Test cancel editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
And I press the "Cancel" notification button
Then the policy key name is unchanged
Scenario: Test editing key name
Given I am on the Advanced Course Settings page in Studio
When I edit the name of a policy key
And I press the "Save" notification button
Then the policy key name is changed
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Cancel" notification button
Then the policy key value is unchanged
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
And I press the "Save" notification button
Then the policy key value is changed
Scenario: Add new entries, and they appear alphabetically after save
Given I am on the Advanced Course Settings page in Studio
When I create New Entries
Then they are alphabetized
And I reload the page
Then they are alphabetized
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object
Then it is displayed as formatted
from lettuce import world, step
from common import *
import time
from nose.tools import assert_equal
from nose.tools import assert_true
"""
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
"""
from selenium.webdriver.common.keys import Keys
############### ACTIONS ####################
@step('I select the Advanced Settings$')
def i_select_advanced_settings(step):
expand_icon_css = 'li.nav-course-settings i.icon-expand'
if world.browser.is_element_present_by_css(expand_icon_css):
css_click(expand_icon_css)
link_css = 'li.nav-course-settings-advanced a'
css_click(link_css)
@step('I am on the Advanced Course Settings page in Studio$')
def i_am_on_advanced_course_settings(step):
step.given('I have opened a new course in Studio')
step.given('I select the Advanced Settings')
# TODO: this is copied from terrain's step.py. Need to figure out how to share that code.
@step('I reload the page$')
def reload_the_page(step):
world.browser.reload()
@step(u'I edit the name of a policy key$')
def edit_the_name_of_a_policy_key(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e.fill('new')
@step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name):
world.browser.click_link_by_text(name)
@step(u'I edit the value of a policy key$')
def edit_the_value_of_a_policy_key(step):
"""
It is hard to figure out how to get into the CodeMirror
area, so cheat and do it from the policy key field :)
"""
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
e._element.send_keys(Keys.TAB, Keys.END, Keys.ARROW_LEFT, ' ', 'X')
@step('I delete the display name$')
def delete_the_display_name(step):
delete_entry(0)
click_save()
@step('create New Entries$')
def create_new_entries(step):
create_entry("z", "apple")
create_entry("a", "zebra")
click_save()
@step('I create a JSON object$')
def create_JSON_object(step):
create_entry("json", '{"key": "value", "key_2": "value_2"}')
click_save()
############### RESULTS ####################
@step('I see only the display name$')
def i_see_only_display_name(step):
assert_policy_entries(["display_name"], ['"Robot Super Course"'])
@step('there are no advanced policy settings$')
def no_policy_settings(step):
assert_policy_entries([], [])
@step('they are alphabetized$')
def they_are_alphabetized(step):
assert_policy_entries(["a", "display_name", "z"], ['"zebra"', '"Robot Super Course"', '"apple"'])
@step('it is displayed as formatted$')
def it_is_formatted(step):
assert_policy_entries(["display_name", "json"], ['"Robot Super Course"', '{\n "key": "value",\n "key_2": "value_2"\n}'])
@step(u'the policy key name is unchanged$')
def the_policy_key_name_is_unchanged(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
assert_equal(e.value, 'display_name')
@step(u'the policy key name is changed$')
def the_policy_key_name_is_changed(step):
policy_key_css = 'input.policy-key'
e = css_find(policy_key_css).first
assert_equal(e.value, 'new')
@step(u'the policy key value is unchanged$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first
assert_equal(e.value, '"Robot Super Course"')
@step(u'the policy key value is changed$')
def the_policy_key_value_is_unchanged(step):
policy_value_css = 'li.course-advanced-policy-list-item div.value textarea'
e = css_find(policy_value_css).first
assert_equal(e.value, '"Robot Super Course X"')
############# HELPERS ###############
def create_entry(key, value):
# Scroll down the page so the button is visible
world.scroll_to_bottom()
css_click_at('a.new-advanced-policy-item', 10, 10)
new_key_css = 'div#__new_advanced_key__ input'
new_key_element = css_find(new_key_css).first
new_key_element.fill(key)
# For some reason have to get the instance for each command (get error that it is no longer attached to the DOM)
# Have to do all this because Selenium has a bug that fill does not remove existing text
new_value_css = 'div.CodeMirror textarea'
css_find(new_value_css).last.fill("")
css_find(new_value_css).last._element.send_keys(Keys.DELETE, Keys.DELETE)
css_find(new_value_css).last.fill(value)
def delete_entry(index):
"""
Delete the nth entry where index is 0-based
"""
css = '.delete-button'
assert_true(world.browser.is_element_present_by_css(css, 5))
delete_buttons = css_find(css)
assert_true(len(delete_buttons) > index, "no delete button exists for entry " + str(index))
delete_buttons[index].click()
def assert_policy_entries(expected_keys, expected_values):
assert_entries('.key input', expected_keys)
assert_entries('.json', expected_values)
def assert_entries(css, expected_values):
webElements = css_find(css)
assert_equal(len(expected_values), len(webElements))
# Sometimes get stale reference if I hold on to the array of elements
for counter in range(len(expected_values)):
assert_equal(expected_values[counter], css_find(css)[counter].value)
def click_save():
css = ".save-button"
def is_shown(driver):
visible = css_find(css).first.visible
if visible:
# Even when waiting for visible, this fails sporadically. Adding in a small wait.
time.sleep(float(1))
return visible
wait_for(is_shown)
css_click(css)
def fill_last_field(value):
newValue = css_find('#__new_advanced_key__ input').first
newValue.fill(value)
from lettuce import world, step
from factories import *
from django.core.management import call_command
from lettuce.django import django_url
from django.conf import settings
from django.core.management import call_command
from nose.tools import assert_true
from nose.tools import assert_equal
from selenium.webdriver.support.ui import WebDriverWait
from terrain.factories import UserFactory, RegistrationFactory, UserProfileFactory
from terrain.factories import CourseFactory, GroupFactory
import xmodule.modulestore.django
from auth.authz import get_user_by_email
from logging import getLogger
logger = getLogger(__name__)
......@@ -20,7 +21,8 @@ def i_visit_the_studio_homepage(step):
# LETTUCE_SERVER_PORT = 8001
# in your settings.py file.
world.browser.visit(django_url('/'))
assert world.browser.is_element_present_by_css('body.no-header', 10)
signin_css = 'a.action-signin'
assert world.browser.is_element_present_by_css(signin_css, 10)
@step('I am logged into Studio$')
......@@ -43,6 +45,13 @@ def i_press_the_category_delete_icon(step, category):
assert False, 'Invalid category: %s' % category
css_click(css)
@step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step):
clear_courses()
log_into_studio()
create_a_course()
####### HELPER FUNCTIONS ##############
......@@ -85,13 +94,38 @@ def assert_css_with_text(css, text):
def css_click(css):
assert_true(world.browser.is_element_present_by_css(css, 5))
world.browser.find_by_css(css).first.click()
def css_click_at(css, x=10, y=10):
'''
A method to click at x,y coordinates of the element
rather than in the center of the element
'''
assert_true(world.browser.is_element_present_by_css(css, 5))
e = world.browser.find_by_css(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y)
e.action_chains.click()
e.action_chains.perform()
def css_fill(css, value):
world.browser.find_by_css(css).first.fill(value)
def css_find(css):
return world.browser.find_by_css(css)
def wait_for(func):
WebDriverWait(world.browser.driver, 10).until(func)
def id_find(id):
return world.browser.find_by_id(id)
def clear_courses():
flush_xmodule_store()
......@@ -113,7 +147,11 @@ def log_into_studio(
create_studio_user(uname=uname, email=email, is_staff=is_staff)
world.browser.cookies.delete()
world.browser.visit(django_url('/'))
world.browser.is_element_present_by_css('body.no-header', 10)
signin_css = 'a.action-signin'
world.browser.is_element_present_by_css(signin_css, 10)
# click the signin button
css_click(signin_css)
login_form = world.browser.find_by_css('form#login_form')
login_form.find_by_name('email').fill(email)
......@@ -124,19 +162,31 @@ def log_into_studio(
def create_a_course():
css_click('a.new-course-button')
fill_in_course_info()
css_click('input.new-course-save')
assert_true(world.browser.is_element_present_by_css('a#courseware-tab', 5))
c = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
# Add the user to the instructor group of the course
# so they will have the permissions to see it in studio
g = GroupFactory.create(name='instructor_MITx/999/Robot_Super_Course')
u = get_user_by_email('robot+studio@edx.org')
u.groups.add(g)
u.save()
world.browser.reload()
course_link_css = 'span.class-name'
css_click(course_link_css)
course_title_css = 'span.course-title'
assert_true(world.browser.is_element_present_by_css(course_title_css, 5))
def add_section(name='My Section'):
link_css = 'a.new-courseware-section-button'
css_click(link_css)
name_css = '.new-section-name'
save_css = '.new-section-name-save'
name_css = 'input.new-section-name'
save_css = 'input.new-section-name-save'
css_fill(name_css, name)
css_click(save_css)
span_css = 'span.section-name-span'
assert_true(world.browser.is_element_present_by_css(span_css, 5))
def add_subsection(name='Subsection One'):
......
......@@ -34,8 +34,8 @@ def i_click_the_course_link_in_my_courses(step):
@step('the Courseware page has loaded in Studio$')
def courseware_page_has_loaded_in_studio(step):
courseware_css = 'a#courseware-tab'
assert world.browser.is_element_present_by_css(courseware_css)
course_title_css = 'span.course-title'
assert world.browser.is_element_present_by_css(course_title_css)
@step('I see the course listed in My Courses$')
......@@ -59,4 +59,4 @@ def i_am_on_tab(step, tab_name):
@step('I see a link for adding a new section$')
def i_see_new_section_link(step):
link_css = 'a.new-courseware-section-button'
assert_css_with_text(link_css, 'New Section')
assert_css_with_text(link_css, '+ New Section')
......@@ -4,13 +4,6 @@ from common import *
############### ACTIONS ####################
@step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step):
clear_courses()
log_into_studio()
create_a_course()
@step('I click the new section link$')
def i_click_new_section_link(step):
link_css = 'a.new-courseware-section-button'
......@@ -46,6 +39,7 @@ def i_save_a_new_section_release_date(step):
css_fill(time_css, '12:00am')
css_click('a.save-button')
############ ASSERTIONS ###################
......
......@@ -5,8 +5,8 @@ Feature: Sign in
Scenario: Sign up from the homepage
Given I visit the Studio homepage
When I click the link with the text "Sign up"
When I click the link with the text "Sign Up"
And I fill in the registration form
And I press the "Create My Account" button on the registration form
And I press the Create My Account button on the registration form
Then I should see be on the studio home page
And I should see the message "please click on the activation link in your email."
\ No newline at end of file
And I should see the message "please click on the activation link in your email."
......@@ -11,10 +11,11 @@ def i_fill_in_the_registration_form(step):
register_form.find_by_name('terms_of_service').check()
@step('I press the "([^"]*)" button on the registration form$')
def i_press_the_button_on_the_registration_form(step, button):
@step('I press the Create My Account button on the registration form$')
def i_press_the_button_on_the_registration_form(step):
register_form = world.browser.find_by_css('form#register_form')
register_form.find_by_value(button).click()
submit_css = 'button#submit'
register_form.find_by_css(submit_css).click()
@step('I should see be on the studio home page$')
......
......@@ -17,22 +17,29 @@ from auth.authz import _delete_course_group
class Command(BaseCommand):
help = \
'''Delete a MongoDB backed course'''
help = '''Delete a MongoDB backed course'''
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("delete_course requires one argument: <location>")
if len(args) != 1 and len(args) != 2:
raise CommandError("delete_course requires one or more arguments: <location> |commit|")
loc_str = args[0]
commit = False
if len(args) == 2:
commit = args[1] == 'commit'
if commit:
print 'Actually going to delete the course from DB....'
ms = modulestore('direct')
cs = contentstore()
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"):
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
loc = CourseDescriptor.id_to_location(loc_str)
if delete_course(ms, cs, loc) == True:
print 'removing User permissions from course....'
# in the django layer, we need to remove all the user permissions groups associated with this course
_delete_course_group(loc)
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
loc = CourseDescriptor.id_to_location(loc_str)
if delete_course(ms, cs, loc, commit) == True:
print 'removing User permissions from course....'
# in the django layer, we need to remove all the user permissions groups associated with this course
if commit:
_delete_course_group(loc)
......@@ -13,7 +13,7 @@ def query_yes_no(question, default="yes"):
"""
valid = {"yes":True, "y":True, "ye":True,
"no":False, "n":False}
if default == None:
if default is None:
prompt = " [y/n] "
elif default == "yes":
prompt = " [Y/n] "
......
......@@ -5,7 +5,7 @@ from django.test.utils import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
from tempfile import mkdtemp
from tempdir import mkdtemp_clean
import json
from fs.osfs import OSFS
import copy
......@@ -27,10 +27,12 @@ from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.templates import update_templates
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
......@@ -169,7 +171,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
delete_course(ms, cs, location)
delete_course(ms, cs, location, commit=True)
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0)
......@@ -192,7 +194,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(ms, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp())
root_dir = path(mkdtemp_clean())
print 'Exporting to tempdir = {0}'.format(root_dir)
......@@ -212,11 +214,20 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(fs.exists('grading_policy.json'))
course = ms.get_item(location)
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json','r') as grading_policy:
on_disk = loads(grading_policy.read())
course = ms.get_item(location)
self.assertEqual(on_disk, course.grading_policy)
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
# compare what's on disk to what we have in the course module
with fs.open('policy.json','r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
# remove old course
delete_course(ms, cs, location)
......@@ -253,6 +264,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
class ContentStoreTest(ModuleStoreTestCase):
"""
Tests for the CMS ContentStore application.
......@@ -331,7 +343,7 @@ class ContentStoreTest(ModuleStoreTestCase):
# Create a course so there is something to view
resp = self.client.get(reverse('index'))
self.assertContains(resp,
'<h1>My Courses</h1>',
'<h1 class="title-1">My Courses</h1>',
status_code=200,
html=True)
......@@ -367,7 +379,7 @@ class ContentStoreTest(ModuleStoreTestCase):
resp = self.client.get(reverse('course_index', kwargs=data))
self.assertContains(resp,
'<a href="/MITx/999/course/Robot_Super_Course" class="class-name">Robot Super Course</a>',
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
status_code=200,
html=True)
......@@ -390,11 +402,11 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template': 'i4x://edx/templates/problem/Empty'
'template': 'i4x://edx/templates/problem/Blank_Common_Problem'
}
resp = self.client.post(reverse('clone_item'), problem_data)
......@@ -408,3 +420,92 @@ class ContentStoreTest(ModuleStoreTestCase):
context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from context")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
ms = modulestore('direct')
did_load_item = False
try:
ms.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True
except ItemNotFoundError:
pass
# make sure we found the item (e.g. it didn't error while loading)
self.assertTrue(did_load_item)
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct')
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
# let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals:
self.assertIn('xqa_key', vertical.metadata)
self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
self.assertGreater(len(verticals), 0)
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
# crate a new module and add it as a child to a vertical
ms.clone_item(source_template_location, new_component_location)
parent = verticals[0]
ms.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
# flush the cache
ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location)
# check for grace period definition which should be defined at the course level
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual(parent.metadata['graceperiod'], new_module.metadata['graceperiod'])
self.assertEqual(course.metadata['xqa_key'], new_module.metadata['xqa_key'])
#
# now let's define an override at the leaf node level
#
new_module.metadata['graceperiod'] = '1 day'
ms.update_metadata(new_module.location, new_module.metadata)
# flush the cache and refetch
ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location)
self.assertIn('graceperiod', new_module.metadata)
self.assertEqual('1 day', new_module.metadata['graceperiod'])
class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self):
ms = modulestore('direct')
# insert a bogus template in the store
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
ms.clone_item(source_template_location, bogus_template_location)
verify_create = ms.get_item(bogus_template_location)
self.assertIsNotNone(verify_create)
# now run cleanup
update_templates()
# now try to find dangling template, it should not be in DB any longer
asserted = False
try:
verify_create = ms.get_item(bogus_template_location)
except ItemNotFoundError:
asserted = True
self.assertTrue(asserted)
import datetime
import time
import json
import calendar
import copy
from util import converters
from util.converters import jsdate_to_time
......@@ -11,7 +9,6 @@ from django.test.client import Client
from django.core.urlresolvers import reverse
from django.utils.timezone import UTC
import xmodule
from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import (CourseDetails,
CourseSettingsEncoder)
......@@ -22,6 +19,10 @@ from django.test import TestCase
from utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
class ConvertersTestCase(TestCase):
......@@ -143,10 +144,6 @@ class CourseDetailsViewTest(CourseTestCase):
def test_update_and_fetch(self):
details = CourseDetails.fetch(self.course_location)
resp = self.client.get(reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'name': self.course_location.name}))
self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True)
# resp s/b json from here on
url = reverse('course_settings', kwargs={'org': self.course_location.org, 'course': self.course_location.course,
'name': self.course_location.name, 'section': 'details'})
......@@ -266,3 +263,64 @@ class CourseGradingTest(CourseTestCase):
test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
class CourseMetadataEditingTest(CourseTestCase):
def setUp(self):
CourseTestCase.setUp(self)
# add in the full class too
import_from_xml(modulestore(), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x','edX','full','course','6.002_Spring_2012', None])
def test_fetch_initial_fields(self):
test_model = CourseMetadata.fetch(self.course_location)
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
test_model = CourseMetadata.fetch(self.fullcourse_location)
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
self.assertIn('showanswer', test_model, 'showanswer field ')
self.assertIn('xqa_key', test_model, 'xqa_key field ')
def test_update_from_json(self):
test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 1,
"b_a_c_h" : { "c" : "test" },
"test_text" : "a text string"})
self.update_check(test_model)
# try fresh fetch to ensure persistence
test_model = CourseMetadata.fetch(self.course_location)
self.update_check(test_model)
# now change some of the existing metadata
test_model = CourseMetadata.update_from_json(self.course_location,
{ "a" : 2,
"display_name" : "jolly roger"})
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
self.assertIn('a', test_model, 'Missing revised a metadata field')
self.assertEqual(test_model['a'], 2, "a not expected value")
def update_check(self, test_model):
self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('a', test_model, 'Missing new a metadata field')
self.assertEqual(test_model['a'], 1, "a not expected value")
self.assertIn('b_a_c_h', test_model, 'Missing b_a_c_h metadata field')
self.assertDictEqual(test_model['b_a_c_h'], { "c" : "test" }, "b_a_c_h not expected value")
self.assertIn('test_text', test_model, 'Missing test_text metadata field')
self.assertEqual(test_model['test_text'], "a text string", "test_text not expected value")
def test_delete_key(self):
test_model = CourseMetadata.delete_key(self.fullcourse_location, { 'deleteKeys' : ['doesnt_exist', 'showanswer', 'xqa_key']})
# ensure no harm
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Testing', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
self.assertNotIn('showanswer', test_model, 'showanswer field still in')
self.assertNotIn('xqa_key', test_model, 'xqa_key field still in')
......@@ -4,7 +4,6 @@ from django.test.client import Client
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
from tempfile import mkdtemp
import json
from fs.osfs import OSFS
import copy
......@@ -85,7 +84,6 @@ class ContentStoreTestCase(ModuleStoreTestCase):
# Now make sure that the user is now actually activated
self.assertTrue(user(email).is_active)
class AuthTestCase(ContentStoreTestCase):
"""Check that various permissions-related things work"""
......
import json
import copy
from time import time
from uuid import uuid4
from django.test import TestCase
from django.conf import settings
......@@ -20,13 +20,12 @@ class ModuleStoreTestCase(TestCase):
def _pre_setup(self):
super(ModuleStoreTestCase, self)._pre_setup()
# Use the current seconds since epoch to differentiate
# Use a uuid to differentiate
# the mongo collections on jenkins.
sec_since_epoch = '%s' % int(time() * 100)
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
self.test_MODULESTORE = self.orig_MODULESTORE
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
settings.MODULESTORE = self.test_MODULESTORE
# Flush and initialize the module store
......
......@@ -75,12 +75,20 @@ def get_course_for_item(location):
return courses[0]
def get_lms_link_for_item(location, preview=False):
def get_lms_link_for_item(location, preview=False, course_id=None):
if course_id is None:
course_id = get_course_id(location)
if settings.LMS_BASE is not None:
lms_link = "//{preview}{lms_base}/courses/{course_id}/jump_to/{location}".format(
preview='preview.' if preview else '',
lms_base=settings.LMS_BASE,
course_id=get_course_id(location),
if preview:
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
'preview.' + settings.LMS_BASE)
else:
lms_base = settings.LMS_BASE
lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format(
lms_base=lms_base,
course_id=course_id,
location=Location(location)
)
else:
......
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
from xmodule.x_module import XModuleDescriptor
class CourseMetadata(object):
'''
For CRUD operations on metadata fields which do not have specific editors on the other pages including any user generated ones.
The objects have no predefined attrs but instead are obj encodings of the editable metadata.
'''
# __new_advanced_key__ is used by client not server; so, could argue against it being here
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end', 'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', '__new_advanced_key__']
@classmethod
def fetch(cls, course_location):
"""
Fetch the key:value editable course details for the given course from persistence and return a CourseMetadata model.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
course = {}
descriptor = get_modulestore(course_location).get_item(course_location)
for k, v in descriptor.metadata.iteritems():
if k not in cls.FILTERED_LIST:
course[k] = v
return course
@classmethod
def update_from_json(cls, course_location, jsondict):
"""
Decode the json into CourseMetadata and save any changed attrs to the db.
Ensures none of the fields are in the blacklist.
"""
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False
for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload?
if k not in cls.FILTERED_LIST and (k not in descriptor.metadata or descriptor.metadata[k] != v):
dirty = True
descriptor.metadata[k] = v
if dirty:
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly
return cls.fetch(course_location)
@classmethod
def delete_key(cls, course_location, payload):
'''
Remove the given metadata key(s) from the course. payload can be a single key or [key..]
'''
descriptor = get_modulestore(course_location).get_item(course_location)
for key in payload['deleteKeys']:
if key in descriptor.metadata:
del descriptor.metadata[key]
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
return cls.fetch(course_location)
\ No newline at end of file
......@@ -20,7 +20,6 @@ Longer TODO:
"""
import sys
import tempfile
import os.path
import os
import lms.envs.common
......@@ -59,7 +58,8 @@ sys.path.append(COMMON_ROOT / 'lib')
############################# WEB CONFIGURATION #############################
# This is where we stick our compiled template files.
MAKO_MODULE_DIR = tempfile.mkdtemp('mako')
from tempdir import mkdtemp_clean
MAKO_MODULE_DIR = mkdtemp_clean('mako')
MAKO_TEMPLATES = {}
MAKO_TEMPLATES['main'] = [
PROJECT_ROOT / 'templates',
......@@ -74,8 +74,8 @@ TEMPLATE_DIRS = MAKO_TEMPLATES['main']
MITX_ROOT_URL = ''
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/login'
LOGIN_URL = MITX_ROOT_URL + '/login'
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/signin'
LOGIN_URL = MITX_ROOT_URL + '/signin'
TEMPLATE_CONTEXT_PROCESSORS = (
......@@ -172,6 +172,9 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identi
USE_I18N = True
USE_L10N = True
# Tracking
TRACK_MAX_EVENT = 10000
# Messages
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
......@@ -275,6 +278,10 @@ INSTALLED_APPS = (
'auth',
'student', # misleading name due to sharing with lms
'course_groups', # not used in cms (yet), but tests run
# tracking
'track',
# For asset pipelining
'pipeline',
'staticfiles',
......
<li class="field-group course-advanced-policy-list-item">
<div class="field text key" id="<%= (_.isEmpty(key) ? '__new_advanced_key__' : key) %>">
<label for="<%= keyUniqueId %>">Policy Key:</label>
<input type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
<span class="tip tip-stacked">Keys are case sensitive and cannot contain spaces or start with a number</span>
</div>
<div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
</div>
<div class="actions">
<a href="#" class="button delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
</div>
</li>
\ No newline at end of file
<li class="input input-existing multi course-grading-assignment-list-item">
<div class="row row-col2">
<label for="course-grading-assignment-name">Assignment Type Name:</label>
<div class="field">
<div class="input course-grading-assignment-name">
<input type="text" class="long"
id="course-grading-assignment-name" value="<%= model.get('type') %>">
<span class="tip tip-stacked">e.g. Homework, Labs, Midterm Exams, Final Exam</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-shortname">Abbreviation:</label>
<div class="field">
<div class="input course-grading-shortname">
<input type="text" class="short"
id="course-grading-assignment-shortname"
value="<%= model.get('short_label') %>">
<span class="tip tip-inline">e.g. HW, Midterm, Final</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-gradeweight">Weight of Total
Grade:</label>
<div class="field">
<div class="input course-grading-gradeweight">
<input type="text" class="short"
id="course-grading-assignment-gradeweight"
value = "<%= model.get('weight') %>">
<span class="tip tip-inline">e.g. 25%</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-assignment-totalassignments">Total
Number:</label>
<div class="field">
<div class="input course-grading-totalassignments">
<input type="text" class="short"
id="course-grading-assignment-totalassignments"
value = "<%= model.get('min_count') %>">
<span class="tip tip-inline">total exercises assigned</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-assignment-droppable">Number of
Droppable:</label>
<div class="field">
<div class="input course-grading-droppable">
<input type="text" class="short"
id="course-grading-assignment-droppable"
value = "<%= model.get('drop_count') %>">
<span class="tip tip-inline">total exercises that won't be graded</span>
</div>
</div>
</div>
<a href="#" class="delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
<li class="field-group course-grading-assignment-list-item">
<div class="field text" id="field-course-grading-assignment-name">
<label for="course-grading-assignment-name">Assignment Type Name</label>
<input type="text" class="long" id="course-grading-assignment-name" value="<%= model.get('type') %>" />
<span class="tip tip-stacked">e.g. Homework, Midterm Exams</span>
</div>
<div class="field text" id="field-course-grading-assignment-shortname">
<label for="course-grading-assignment-shortname">Abbreviation:</label>
<input type="text" class="short" id="course-grading-assignment-shortname" value="<%= model.get('short_label') %>" />
<span class="tip tip-inline">e.g. HW, Midterm</span>
</div>
<div class="field text" id="field-course-grading-assignment-gradeweight">
<label for="course-grading-assignment-gradeweight">Weight of Total Grade</label>
<input type="text" class="short" id="course-grading-assignment-gradeweight" value = "<%= model.get('weight') %>" />
<span class="tip tip-inline">e.g. 25%</span>
</div>
<div class="field text" id="field-course-grading-assignment-totalassignments">
<label for="course-grading-assignment-totalassignments">Total
Number</label>
<input type="text" class="short" id="course-grading-assignment-totalassignments" value = "<%= model.get('min_count') %>" />
<span class="tip tip-inline">total exercises assigned</span>
</div>
<div class="field text" id="field-course-grading-assignment-droppable">
<label for="course-grading-assignment-droppable">Number of
Droppable</label>
<input type="text" class="short" id="course-grading-assignment-droppable" value = "<%= model.get('drop_count') %>" />
<span class="tip tip-inline">total exercises that won't be graded</span>
</div>
<div class="actions">
<a href="#" class="button delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span>Delete</a>
</div>
</li>
class CMS.Views.TabsEdit extends Backbone.View
events:
'click .new-tab': 'addNewTab'
initialize: =>
@$('.component').each((idx, element) =>
......@@ -13,6 +11,7 @@ class CMS.Views.TabsEdit extends Backbone.View
)
)
@options.mast.find('.new-tab').on('click', @addNewTab)
@$('.components').sortable(
handle: '.drag-handle'
update: @tabMoved
......
cms/static/img/html-icon.png

1.37 KB | W: | H:

cms/static/img/html-icon.png

581 Bytes | W: | H:

cms/static/img/html-icon.png
cms/static/img/html-icon.png
cms/static/img/html-icon.png
cms/static/img/html-icon.png
  • 2-up
  • Swipe
  • Onion skin
cms/static/img/large-discussion-icon.png

1.46 KB | W: | H:

cms/static/img/large-discussion-icon.png

737 Bytes | W: | H:

cms/static/img/large-discussion-icon.png
cms/static/img/large-discussion-icon.png
cms/static/img/large-discussion-icon.png
cms/static/img/large-discussion-icon.png
  • 2-up
  • Swipe
  • Onion skin
cms/static/img/large-freeform-icon.png

1.17 KB | W: | H:

cms/static/img/large-freeform-icon.png

412 Bytes | W: | H:

cms/static/img/large-freeform-icon.png
cms/static/img/large-freeform-icon.png
cms/static/img/large-freeform-icon.png
cms/static/img/large-freeform-icon.png
  • 2-up
  • Swipe
  • Onion skin
cms/static/img/large-problem-icon.png

1.49 KB | W: | H:

cms/static/img/large-problem-icon.png

797 Bytes | W: | H:

cms/static/img/large-problem-icon.png
cms/static/img/large-problem-icon.png
cms/static/img/large-problem-icon.png
cms/static/img/large-problem-icon.png
  • 2-up
  • Swipe
  • Onion skin
cms/static/img/large-video-icon.png

994 Bytes | W: | H:

cms/static/img/large-video-icon.png

234 Bytes | W: | H:

cms/static/img/large-video-icon.png
cms/static/img/large-video-icon.png
cms/static/img/large-video-icon.png
cms/static/img/large-video-icon.png
  • 2-up
  • Swipe
  • Onion skin
if (!CMS.Models['Settings']) CMS.Models.Settings = {};
CMS.Models.Settings.Advanced = Backbone.Model.extend({
// the key for a newly added policy-- before the user has entered a key value
new_key : "__new_advanced_key__",
defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server)
},
// which keys to send as the deleted keys on next save
deleteKeys : [],
blacklistKeys : [], // an array which the controller should populate directly for now [static not instance based]
validate: function (attrs) {
var errors = {};
for (var key in attrs) {
if (key === this.new_key || _.isEmpty(key)) {
errors[key] = "A key must be entered.";
}
else if (_.contains(this.blacklistKeys, key)) {
errors[key] = key + " is a reserved keyword or can be edited on another screen";
}
}
if (!_.isEmpty(errors)) return errors;
},
save : function (attrs, options) {
// wraps the save call w/ the deletion of the removed keys after we know the saved ones worked
options = options ? _.clone(options) : {};
// add saveSuccess to the success
var success = options.success;
options.success = function(model, resp, options) {
model.afterSave(model);
if (success) success(model, resp, options);
};
Backbone.Model.prototype.save.call(this, attrs, options);
},
afterSave : function(self) {
// remove deleted attrs
if (!_.isEmpty(self.deleteKeys)) {
// remove the to be deleted keys from the returned model
_.each(self.deleteKeys, function(key) { self.unset(key); });
// not able to do via backbone since we're not destroying the model
$.ajax({
url : self.url,
// json to and fro
contentType : "application/json",
dataType : "json",
// delete
type : 'DELETE',
// data
data : JSON.stringify({ deleteKeys : self.deleteKeys})
})
.fail(function(hdr, status, error) { CMS.ServerError(self, "Deleting keys:" + status); })
.done(function(data, status, error) {
// clear deleteKeys on success
self.deleteKeys = [];
});
}
}
});
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
defaults: {
location : null, // the course's Location model, required
start_date: null, // maps to 'start'
end_date: null, // maps to 'end'
enrollment_start: null,
enrollment_end: null,
syllabus: null,
overview: "",
intro_video: null,
effort: null // an int or null
},
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
parse: function(attributes) {
if (attributes['course_location']) {
attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
}
if (attributes['start_date']) {
attributes.start_date = new Date(attributes.start_date);
}
if (attributes['end_date']) {
attributes.end_date = new Date(attributes.end_date);
}
if (attributes['enrollment_start']) {
attributes.enrollment_start = new Date(attributes.enrollment_start);
}
if (attributes['enrollment_end']) {
attributes.enrollment_end = new Date(attributes.enrollment_end);
}
return attributes;
},
validate: function(newattrs) {
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
var errors = {};
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
errors.end_date = "The course end date cannot be before the course start date.";
}
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
}
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
}
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
}
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
errors.intro_video = "Key should only contain letters, numbers, _, or -";
}
// TODO check if key points to a real video using google's youtube api
}
if (!_.isEmpty(errors)) return errors;
// NOTE don't return empty errors as that will be interpreted as an error state
},
url: function() {
var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
},
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
save_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null},
{ error : CMS.ServerError});
// TODO remove all whitespace w/in string
else {
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource,
{ error : CMS.ServerError});
}
return this.videosourceSample();
},
videosourceSample : function() {
if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
else return "";
}
defaults: {
location : null, // the course's Location model, required
start_date: null, // maps to 'start'
end_date: null, // maps to 'end'
enrollment_start: null,
enrollment_end: null,
syllabus: null,
overview: "",
intro_video: null,
effort: null // an int or null
},
// When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
parse: function(attributes) {
if (attributes['course_location']) {
attributes.location = new CMS.Models.Location(attributes.course_location, {parse:true});
}
if (attributes['start_date']) {
attributes.start_date = new Date(attributes.start_date);
}
if (attributes['end_date']) {
attributes.end_date = new Date(attributes.end_date);
}
if (attributes['enrollment_start']) {
attributes.enrollment_start = new Date(attributes.enrollment_start);
}
if (attributes['enrollment_end']) {
attributes.enrollment_end = new Date(attributes.enrollment_end);
}
return attributes;
},
validate: function(newattrs) {
// Returns either nothing (no return call) so that validate works or an object of {field: errorstring} pairs
// A bit funny in that the video key validation is asynchronous; so, it won't stop the validation.
var errors = {};
if (newattrs.start_date && newattrs.end_date && newattrs.start_date >= newattrs.end_date) {
errors.end_date = "The course end date cannot be before the course start date.";
}
if (newattrs.start_date && newattrs.enrollment_start && newattrs.start_date < newattrs.enrollment_start) {
errors.enrollment_start = "The course start date cannot be before the enrollment start date.";
}
if (newattrs.enrollment_start && newattrs.enrollment_end && newattrs.enrollment_start >= newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment start date cannot be after the enrollment end date.";
}
if (newattrs.end_date && newattrs.enrollment_end && newattrs.end_date < newattrs.enrollment_end) {
errors.enrollment_end = "The enrollment end date cannot be after the course end date.";
}
if (newattrs.intro_video && newattrs.intro_video !== this.get('intro_video')) {
if (this._videokey_illegal_chars.exec(newattrs.intro_video)) {
errors.intro_video = "Key should only contain letters, numbers, _, or -";
}
// TODO check if key points to a real video using google's youtube api
}
if (!_.isEmpty(errors)) return errors;
// NOTE don't return empty errors as that will be interpreted as an error state
},
url: function() {
var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/details';
},
_videokey_illegal_chars : /[^a-zA-Z0-9_-]/g,
save_videosource: function(newsource) {
// newsource either is <video youtube="speed:key, *"/> or just the "speed:key, *" string
// returns the videosource for the preview which iss the key whose speed is closest to 1
if (_.isEmpty(newsource) && !_.isEmpty(this.get('intro_video'))) this.save({'intro_video': null});
// TODO remove all whitespace w/in string
else {
if (this.get('intro_video') !== newsource) this.save('intro_video', newsource);
}
return this.videosourceSample();
},
videosourceSample : function() {
if (this.has('intro_video')) return "http://www.youtube.com/embed/" + this.get('intro_video');
else return "";
}
});
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseSettings = Backbone.Model.extend({
// a container for the models representing the n possible tabbed states
defaults: {
courseLocation: null,
// NOTE: keep these sync'd w/ the data-section names in settings-page-menu
details: null,
faculty: null,
grading: null,
problems: null,
discussions: null
},
// a container for the models representing the n possible tabbed states
defaults: {
courseLocation: null,
details: null,
faculty: null,
grading: null,
problems: null,
discussions: null
},
retrieve: function(submodel, callback) {
if (this.get(submodel)) callback();
else {
var cachethis = this;
switch (submodel) {
case 'details':
var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
details.fetch( {
success : function(model) {
cachethis.set('details', model);
callback(model);
}
});
break;
case 'grading':
var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
grading.fetch( {
success : function(model) {
cachethis.set('grading', model);
callback(model);
}
});
break;
retrieve: function(submodel, callback) {
if (this.get(submodel)) callback();
else {
var cachethis = this;
switch (submodel) {
case 'details':
var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
details.fetch( {
success : function(model) {
cachethis.set('details', model);
callback(model);
}
});
break;
case 'grading':
var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
grading.fetch( {
success : function(model) {
cachethis.set('grading', model);
callback(model);
}
});
break;
default:
break;
}
}
}
default:
break;
}
}
}
})
\ No newline at end of file
......@@ -5,7 +5,7 @@
if (typeof window.templateLoader == 'function') return;
var templateLoader = {
templateVersion: "0.0.12",
templateVersion: "0.0.15",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
......
......@@ -10,7 +10,7 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
render: function() {
// instantiate the ClassInfoUpdateView and delegate the proper dom to it
new CMS.Views.ClassInfoUpdateView({
el: this.$('#course-update-view'),
el: $('body.updates'),
collection: this.model.get('updates')
});
......@@ -27,10 +27,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// collection is CourseUpdateCollection
events: {
"click .new-update-button" : "onNew",
"click .save-button" : "onSave",
"click .cancel-button" : "onCancel",
"click .edit-button" : "onEdit",
"click .delete-button" : "onDelete"
"click #course-update-view .save-button" : "onSave",
"click #course-update-view .cancel-button" : "onCancel",
"click .post-actions > .edit-button" : "onEdit",
"click .post-actions > .delete-button" : "onDelete"
},
initialize: function() {
......
CMS.Views.ValidatingView = Backbone.View.extend({
// Intended as an abstract class which catches validation errors on the model and
// decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
initialize : function() {
this.model.on('error', this.handleValidationError, this);
this.selectorToField = _.invert(this.fieldToSelectorMap);
},
errorTemplate : _.template('<span class="message-error"><%= message %></span>'),
events : {
"change input" : "clearValidationErrors",
"change textarea" : "clearValidationErrors"
},
fieldToSelectorMap : {
// Your subclass must populate this w/ all of the model keys and dom selectors
// which may be the subjects of validation errors
},
_cacheValidationErrors : [],
handleValidationError : function(model, error) {
// error triggered either by validation or server error
// error is object w/ fields and error strings
for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
if (ele.length === 0) {
// check if it might a server error: note a typo in the field name
// or failure to put in a map may cause this to muffle validation errors
if (_.has(error, 'error') && _.has(error, 'responseText')) {
CMS.ServerError(model, error);
return;
}
else continue;
}
this._cacheValidationErrors.push(ele);
if ($(ele).is('div')) {
// put error on the contained inputs
$(ele).find('input, textarea').addClass('error');
}
else $(ele).addClass('error');
$(ele).parent().append(this.errorTemplate({message : error[field]}));
}
},
clearValidationErrors : function() {
// error is object w/ fields and error strings
while (this._cacheValidationErrors.length > 0) {
var ele = this._cacheValidationErrors.pop();
if ($(ele).is('div')) {
// put error on the contained inputs
$(ele).find('input, textarea').removeClass('error');
}
else $(ele).removeClass('error');
$(ele).nextAll('.message-error').remove();
}
},
saveIfChanged : function(event) {
// returns true if the value changed and was thus sent to server
var field = this.selectorToField[event.currentTarget.id];
var currentVal = this.model.get(field);
var newVal = $(event.currentTarget).val();
this.clearValidationErrors(); // curr = new if user reverts manually
if (currentVal != newVal) {
this.model.save(field, newVal);
return true;
}
else return false;
},
// these should perhaps go into a superclass but lack of event hash inheritance demotivates me
inputFocus : function(event) {
$("label[for='" + event.currentTarget.id + "']").addClass("is-focused");
},
inputUnfocus : function(event) {
$("label[for='" + event.currentTarget.id + "']").removeClass("is-focused");
}
});
// Studio - Sign In/Up
// ====================
body.signup, body.signin {
.wrapper-content {
margin: 0;
padding: 0 $baseline;
position: relative;
width: 100%;
}
.content {
@include clearfix();
@include font-size(16);
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto;
color: $gray-d2;
header {
position: relative;
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l4;
padding-bottom: ($baseline/2);
h1 {
@include font-size(32);
margin: 0;
padding: 0;
font-weight: 600;
}
.action {
@include font-size(13);
position: absolute;
right: 0;
top: 40%;
}
}
.introduction {
@include font-size(14);
margin: 0 0 $baseline 0;
}
}
.content-primary, .content-supplementary {
@include box-sizing(border-box);
float: left;
}
.content-primary {
width: flex-grid(8, 12);
margin-right: flex-gutter();
form {
@include box-sizing(border-box);
@include box-shadow(0 1px 2px $shadow-l1);
@include border-radius(2px);
width: 100%;
border: 1px solid $gray-l2;
padding: $baseline ($baseline*1.5);
background: $white;
.form-actions {
margin-top: $baseline;
.action-primary {
@include blue-button;
@include transition(all .15s);
@include font-size(15);
display:block;
width: 100%;
padding: ($baseline*0.75) ($baseline/2);
font-weight: 600;
text-transform: uppercase;
}
}
.list-input {
margin: 0;
padding: 0;
list-style: none;
.field {
margin: 0 0 ($baseline*0.75) 0;
&:last-child {
margin-bottom: 0;
}
&.required {
label {
font-weight: 600;
}
label:after {
margin-left: ($baseline/4);
content: "*";
}
}
label, input, textarea {
display: block;
}
label {
@include font-size(14);
@include transition(color, 0.15s, ease-in-out);
margin: 0 0 ($baseline/4) 0;
&.is-focused {
color: $blue;
}
}
input, textarea {
@include font-size(16);
height: 100%;
width: 100%;
padding: ($baseline/2);
&.long {
width: 100%;
}
&.short {
width: 25%;
}
::-webkit-input-placeholder {
color: $gray-l4;
}
:-moz-placeholder {
color: $gray-l3;
}
::-moz-placeholder {
color: $gray-l3;
}
:-ms-input-placeholder {
color: $gray-l3;
}
&:focus {
+ .tip {
color: $gray;
}
}
}
textarea.long {
height: ($baseline*5);
}
input[type="checkbox"] {
display: inline-block;
margin-right: ($baseline/4);
width: auto;
height: auto;
& + label {
display: inline-block;
}
}
.tip {
@include transition(color, 0.15s, ease-in-out);
@include font-size(13);
display: block;
margin-top: ($baseline/4);
color: $gray-l3;
}
}
.field-group {
@include clearfix();
margin: 0 0 ($baseline/2) 0;
.field {
display: block;
width: 47%;
border-bottom: none;
margin: 0 $baseline 0 0;
padding-bottom: 0;
&:nth-child(odd) {
float: left;
}
&:nth-child(even) {
float: right;
margin-right: 0;
}
input, textarea {
width: 100%;
}
}
}
}
}
}
.content-supplementary {
width: flex-grid(4, 12);
.bit {
@include font-size(13);
margin: 0 0 $baseline 0;
border-bottom: 1px solid $gray-l4;
padding: 0 0 $baseline 0;
color: $gray-l1;
&:last-child {
margin-bottom: 0;
border: none;
padding-bottom: 0;
}
h3 {
@include font-size(14);
margin: 0 0 ($baseline/4) 0;
color: $gray-d2;
font-weight: 600;
}
}
}
}
.signup {
}
.signin {
#field-password {
position: relative;
.action-forgotpassword {
@include font-size(13);
position: absolute;
top: 0;
right: 0;
}
}
}
// ====================
// messages
.message {
@include font-size(14);
display: block;
}
.message-status {
display: none;
@include border-top-radius(2px);
@include box-sizing(border-box);
border-bottom: 2px solid $yellow-d2;
margin: 0 0 $baseline 0;
padding: ($baseline/2) $baseline;
font-weight: 500;
background: $yellow-d1;
color: $white;
.ss-icon {
position: relative;
top: 3px;
@include font-size(16);
display: inline-block;
margin-right: ($baseline/2);
}
.text {
display: inline-block;
}
&.error {
border-color: shade($red, 50%);
background: tint($red, 20%);
}
&.is-shown {
display: block;
}
}
// notifications
.wrapper-notification {
@include clearfix();
@include box-sizing(border-box);
@include transition (bottom 2.0s ease-in-out 5s);
@include box-shadow(0 -1px 2px rgba(0,0,0,0.1));
position: fixed;
bottom: -100px;
z-index: 1000;
width: 100%;
overflow: hidden;
opacity: 0;
border-top: 1px solid $darkGrey;
padding: 20px 40px;
&.is-shown {
bottom: 0;
opacity: 1.0;
}
&.wrapper-notification-warning {
border-color: shade($yellow, 25%);
background: tint($yellow, 25%);
}
&.wrapper-notification-error {
border-color: shade($red, 50%);
background: tint($red, 20%);
color: $white;
}
&.wrapper-notification-confirm {
border-color: shade($green, 30%);
background: tint($green, 40%);
color: shade($green, 30%);
}
}
.notification {
@include box-sizing(border-box);
margin: 0 auto;
width: flex-grid(12);
max-width: $fg-max-width;
min-width: $fg-min-width;
.copy {
float: left;
width: flex-grid(9, 12);
margin-right: flex-gutter();
margin-top: 5px;
font-size: 14px;
.icon {
display: inline-block;
vertical-align: top;
margin-right: 5px;
font-size: 20px;
}
p {
width: flex-grid(8, 9);
display: inline-block;
vertical-align: top;
}
}
.actions {
float: right;
width: flex-grid(3, 12);
margin-top: ($baseline/2);
text-align: right;
li {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.save-button {
@include blue-button;
}
.cancel-button {
@include white-button;
}
}
strong {
font-weight: 700;
}
}
// adopted alerts
.alert {
padding: 15px 20px;
margin-bottom: 30px;
......
.assets {
.uploads {
input.asset-search-input {
float: left;
width: 260px;
......
......@@ -28,7 +28,7 @@
}
}
&:hover {
&:hover, &.active {
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15));
}
}
......@@ -41,7 +41,7 @@
background-color: $blue;
color: #fff;
&:hover {
&:hover, &.active {
background-color: #62aaf5;
color: #fff;
}
......@@ -285,4 +285,11 @@
padding: 0;
position: absolute;
width: 1px;
}
@mixin active {
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: rgba(255, 255, 255, .3);
@include box-shadow(0 -1px 0 rgba(0, 0, 0, .2) inset, 0 1px 0 #fff inset);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
\ No newline at end of file
......@@ -37,6 +37,11 @@
padding: 34px 0 42px;
border-top: 1px solid #cbd1db;
&:first-child {
padding-top: 0;
border: none;
}
&.editing {
position: relative;
z-index: 1001;
......
......@@ -498,6 +498,7 @@ input.courseware-unit-search-input {
}
&.new-section {
header {
height: auto;
@include clearfix();
......@@ -506,6 +507,15 @@ input.courseware-unit-search-input {
.expand-collapse-icon {
visibility: hidden;
}
.item-details {
padding: 25px 0 0 0;
.section-name {
float: none;
width: 100%;
}
}
}
}
......
......@@ -6,19 +6,27 @@
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
li {
position: relative;
border-bottom: 1px solid $mediumGrey;
&:last-child {
border-bottom: none;
}
}
a {
padding: 20px 25px;
line-height: 1.3;
&:hover {
background: $paleYellow;
.class-link {
z-index: 100;
display: block;
padding: 20px 25px;
line-height: 1.3;
&:hover {
background: $paleYellow;
+ .view-live-button {
opacity: 1.0;
pointer-events: auto;
}
}
}
}
......@@ -34,6 +42,22 @@
margin-right: 20px;
color: #3c3c3c;
}
// view live button
.view-live-button {
z-index: 10000;
position: absolute;
top: 15px;
right: $baseline;
padding: ($baseline/4) ($baseline/2);
opacity: 0;
pointer-events: none;
&:hover {
opacity: 1.0;
pointer-events: auto;
}
}
}
.new-course {
......
.faded-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1) 50%,
rgba(200,200,200, 0)));
height: 1px;
width: 100%;
}
.faded-hr-divider-medium {
@include background-image(linear-gradient(180deg, rgba(240,240,240, 0) 0%,
rgba(240,240,240, 1) 50%,
rgba(240,240,240, 0)));
height: 1px;
width: 100%;
}
.faded-hr-divider-light {
@include background-image(linear-gradient(180deg, rgba(255,255,255, 0) 0%,
rgba(255,255,255, 0.8) 50%,
rgba(255,255,255, 0)));
height: 1px;
width: 100%;
}
.faded-vertical-divider {
@include background-image(linear-gradient(90deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1) 50%,
rgba(200,200,200, 0)));
height: 100%;
width: 1px;
}
.faded-vertical-divider-light {
@include background-image(linear-gradient(90deg, rgba(255,255,255, 0) 0%,
rgba(255,255,255, 0.6) 50%,
rgba(255,255,255, 0)));
height: 100%;
width: 1px;
}
.vertical-divider {
@extend .faded-vertical-divider;
position: relative;
&::after {
@extend .faded-vertical-divider-light;
content: "";
display: block;
position: absolute;
left: 1px;
}
}
.horizontal-divider {
border: none;
@extend .faded-hr-divider;
position: relative;
&::after {
@extend .faded-hr-divider-light;
content: "";
display: block;
position: absolute;
top: 1px;
}
}
.fade-right-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 0) 0%,
rgba(200,200,200, 1)));
border: none;
}
.fade-left-hr-divider {
@include background-image(linear-gradient(180deg, rgba(200,200,200, 1) 0%,
rgba(200,200,200, 0)));
border: none;
}
\ No newline at end of file
//studio global footer
.wrapper-footer {
margin: ($baseline*1.5) 0 $baseline 0;
padding: $baseline;
position: relative;
width: 100%;
footer.primary {
@include clearfix();
@include font-size(13);
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto;
padding-top: $baseline;
border-top: 1px solid $gray-l4;
color: $gray-l2;
.colophon {
width: flex-grid(4, 12);
float: left;
margin-right: flex-gutter(2);
}
.nav-peripheral {
width: flex-grid(6, 12);
float: right;
text-align: right;
.nav-item {
display: inline-block;
margin-right: ($baseline/2);
&:last-child {
margin-right: 0;
}
}
}
a {
color: $gray-l1;
&:hover, &:active {
color: $blue;
}
}
}
}
\ No newline at end of file
body.index {
> header {
display: none;
}
// how it works/not signed in index
.index {
> h1 {
font-weight: 300;
color: lighten($dark-blue, 40%);
text-shadow: 0 1px 0 #fff;
-webkit-font-smoothing: antialiased;
max-width: 600px;
text-align: center;
margin: 80px auto 30px;
}
&.not-signedin {
.wrapper-header {
margin-bottom: 0;
}
.wrapper-footer {
margin: 0;
border-top: 2px solid $gray-l3;
footer.primary {
border: none;
margin-top: 0;
padding-top: 0;
}
}
.wrapper-content-header, .wrapper-content-features, .wrapper-content-cta {
@include box-sizing(border-box);
margin: 0;
padding: 0 $baseline;
position: relative;
width: 100%;
}
section.main-container {
border-right: 3px;
background: #FFF;
max-width: 600px;
margin: 0 auto;
display: block;
@include box-sizing(border-box);
border: 1px solid lighten( $dark-blue , 30% );
@include border-radius(3px);
overflow: hidden;
@include bounce-in-animation(.8s);
header {
border-bottom: 1px solid lighten($dark-blue, 50%);
@include linear-gradient(#fff, lighten($dark-blue, 62%));
.content {
@include clearfix();
@include box-shadow( 0 2px 0 $light-blue, inset 0 -1px 0 #fff);
text-shadow: 0 1px 0 #fff;
@include font-size(16);
max-width: $fg-max-width;
min-width: $fg-min-width;
width: flex-grid(12);
margin: 0 auto;
color: $gray-d2;
header {
border: none;
padding-bottom: 0;
margin-bottom: 0;
}
h1, h2, h3, h4, h5, h6 {
color: $gray-d3;
}
h2 {
}
h3 {
}
h4 {
}
}
// welcome content
.wrapper-content-header {
@include linear-gradient($blue-l1,$blue,$blue-d1);
padding-bottom: ($baseline*4);
padding-top: ($baseline*4);
}
.content-header {
position: relative;
text-align: center;
color: $white;
h1 {
font-size: 14px;
padding: 8px 20px;
float: left;
color: $dark-blue;
margin: 0;
@include font-size(52);
float: none;
margin: 0 0 ($baseline/2) 0;
border-bottom: 1px solid $blue-l1;
padding: 0;
font-weight: 500;
color: $white;
}
.logo {
@include text-hide();
position: relative;
top: 3px;
display: inline-block;
vertical-align: baseline;
width: 282px;
height: 57px;
background: transparent url('../img/logo-edx-studio-white.png') 0 0 no-repeat;
}
a {
float: right;
padding: 8px 20px;
border-left: 1px solid lighten($dark-blue, 50%);
@include box-shadow( inset -1px 0 0 #fff);
font-weight: bold;
font-size: 22px;
line-height: 1;
color: $dark-blue;
.tagline {
@include font-size(24);
margin: 0;
color: $blue-l3;
}
}
ol {
list-style: none;
margin: 0;
padding: 0;
.arrow_box {
position: relative;
background: #fff;
border: 4px solid #000;
}
.arrow_box:after, .arrow_box:before {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.arrow_box:after {
border-color: rgba(255, 255, 255, 0);
border-top-color: #fff;
border-width: 30px;
left: 50%;
margin-left: -30px;
}
.arrow_box:before {
border-color: rgba(0, 0, 0, 0);
border-top-color: #000;
border-width: 36px;
left: 50%;
margin-left: -36px;
}
// feature content
.wrapper-content-features {
@include box-shadow(0 -1px ($baseline/4) $shadow);
padding-bottom: ($baseline*2);
padding-top: ($baseline*3);
background: $white;
}
li {
border-bottom: 1px solid lighten($dark-blue, 50%);
.content-features {
a {
display: block;
padding: 10px 20px;
.list-features {
}
// indiv features
.feature {
@include clearfix();
margin: 0 0 ($baseline*2) 0;
border-bottom: 1px solid $gray-l4;
padding: 0 0 ($baseline*2) 0;
.img {
@include box-sizing(border-box);
float: left;
width: flex-grid(3, 12);
margin-right: flex-gutter();
a {
@include box-sizing(border-box);
@include box-shadow(0 1px ($baseline/10) $shadow-l1);
position: relative;
top: 0;
display: block;
overflow: hidden;
border: 1px solid $gray-l3;
padding: ($baseline/4);
background: $white;
.action-zoom {
@include transition(bottom .50s ease-in-out);
position: absolute;
bottom: -30px;
right: ($baseline/2);
opacity: 0;
.ss-icon {
@include font-size(18);
@include border-top-radius(3px);
display: inline-block;
padding: ($baseline/4) ($baseline/2);
background: $blue;
color: $white;
text-align: center;
}
}
&:hover {
color: $dark-blue;
background: lighten($yellow, 10%);
text-shadow: 0 1px 0 #fff;
&:hover {
border-color: $blue;
.action-zoom {
opacity: 1.0;
bottom: -2px;
}
}
}
img {
display: block;
width: 100%;
height: 100%;
}
}
.copy {
float: left;
width: flex-grid(9, 12);
margin-top: -($baseline/4);
h3 {
margin: 0 0 ($baseline/2) 0;
@include font-size(24);
font-weight: 600;
}
> p {
@include font-size(18);
color: $gray-d1;
}
strong {
color: $gray-d2;
font-weight: 500;
}
.list-proofpoints {
@include clearfix();
@include font-size(14);
width: flex-grid(9, 9);
margin: ($baseline*1.5) 0 0 0;
.proofpoint {
@include box-sizing(border-box);
@include border-radius(($baseline/4));
@include transition(color .50s ease-in-out);
position: relative;
top: 0;
float: left;
width: flex-grid(3, 9);
min-height: ($baseline*8);
margin-right: flex-gutter();
padding: ($baseline*0.75) $baseline;
color: $gray-l1;
.title {
@include font-size(16);
margin: 0 0 ($baseline/4) 0;
font-weight: 500;
color: $gray-d3;
}
&:hover {
@include box-shadow(0 1px ($baseline/10) $shadow-l1);
background: $blue-l5;
top: -($baseline/5);
.title {
color: $blue;
}
}
&:last-child {
margin-right: 0;
}
}
}
}
&:last-child {
border-bottom: none;
margin-bottom: 0;
border: none;
padding-bottom: 0;
}
&:nth-child(even) {
.img {
float: right;
margin-right: 0;
margin-left: flex-gutter();
}
.copy {
float: right;
text-align: right;
}
.list-proofpoints {
.proofpoint {
float: right;
width: flex-grid(3, 9);
margin-left: flex-gutter();
margin-right: 0;
&:last-child {
margin-left: 0;
}
}
}
}
}
}
// call to action content
.wrapper-content-cta {
padding-bottom: ($baseline*2);
padding-top: ($baseline*2);
background: $white;
}
.content-cta {
border-top: 1px solid $gray-l4;
header {
border: none;
margin: 0;
padding: 0;
}
.list-actions {
position: relative;
margin-top: -($baseline*1.5);
li {
width: flex-grid(6, 12);
margin: 0 auto;
}
.action {
display: block;
width: 100%;
text-align: center;
}
.action-primary {
@include blue-button;
@include transition(all .15s);
@include font-size(18);
padding: ($baseline*0.75) ($baseline/2);
font-weight: 600;
text-align: center;
text-transform: uppercase;
}
.action-secondary {
@include font-size(14);
margin-top: ($baseline/2);
}
}
}
}
}
}
\ No newline at end of file
......@@ -54,4 +54,16 @@
@include white-button;
margin-top: 13px;
}
}
// lean modal alternative
#lean_overlay {
position: fixed;
z-index: 10000;
top: 0px;
left: 0px;
display: none;
height: 100%;
width: 100%;
background: $black;
}
\ No newline at end of file
......@@ -54,4 +54,118 @@ del {
table {
border-collapse: collapse;
border-spacing: 0;
}
/* Reset styles to remove ui-lightness jquery ui theme
from the tabs component (used in the add component problem tab menu)
*/
.ui-tabs {
padding: 0;
white-space: normal;
}
.ui-corner-all, .ui-corner-bottom, .ui-corner-left, ui-corner-top, .ui-corner-br, .ui-corner-right {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 0;
border-top-left-radius: 0;
}
.ui-widget-content {
border: 0;
background: none;
}
.ui-widget {
font-family: 'Open Sans', sans-serif;
font-size: 16px;
}
.ui-widget-header {
border:none;
background: none;
}
.ui-tabs .ui-tabs-nav {
padding: 0;
}
.ui-tabs .ui-tabs-nav li {
margin: 0;
padding: 0;
border: none;
top: 0;
margin: 0;
float: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.ui-tabs-nav {
li {
top: 0;
margin: 0;
}
a {
float: none;
font-weight: normal;
}
}
.ui-tabs .ui-tabs-panel {
padding: 0;
}
/* reapplying the tab styles from unit.scss after
removing jquery ui ui-lightness styling
*/
.problem-type-tabs {
border:none;
list-style-type: none;
width: 100%;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
//background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
li:first-child {
margin-left: 20px;
}
li {
opacity: .8;
&:ui-state-active {
background-color: rgba(255, 255, 255, .3);
opacity: 1;
font-weight: 400;
}
a:focus {
outline: none;
border: 0px;
}
}
/*
li {
float:left;
display:inline-block;
text-align:center;
width: auto;
//@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
//background-color: tint($lightBluishGrey, 20%);
//@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
opacity:.8;
&:hover {
opacity:1;
}
&.current {
border: 0px;
//@include active;
opacity:1;
}
}
*/
}
\ No newline at end of file
......@@ -28,7 +28,9 @@
border-radius: 0;
&.new-component-item {
margin-top: 20px;
background: transparent;
border: none;
@include box-shadow(none);
}
}
......
.subsection .main-wrapper {
margin: 40px;
}
.subsection .inner-wrapper {
@include clearfix();
}
.subsection-body {
padding: 32px 40px;
@include clearfix;
......
.unit .main-wrapper,
.subsection .main-wrapper {
.unit .main-wrapper {
@include clearfix();
margin: 40px;
}
//Problem Selector tab menu requirements
.js .tabs .tab {
display: none;
}
//end problem selector reqs
.main-column {
clear: both;
float: left;
......@@ -58,6 +64,7 @@
margin: 20px 40px;
.title {
margin: 0 0 15px 0;
color: $mediumGrey;
......@@ -67,22 +74,25 @@
}
&.new-component-item {
padding: 20px;
border: none;
border-radius: 3px;
background: $lightGrey;
margin: 20px 0px;
border-top: 1px solid $mediumGrey;
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
background-color: $lightGrey;
margin-bottom: 0px;
padding-bottom: 20px;
.new-component-button {
display: block;
padding: 20px;
text-align: center;
color: #6d788b;
color: #edf1f5;
}
h5 {
margin-bottom: 8px;
margin: 20px 0px;
color: #fff;
font-weight: 700;
font-weight: 600;
font-size: 18px;
}
.rendered-component {
......@@ -92,18 +102,21 @@
}
.new-component-type {
a,
li {
display: inline-block;
}
a {
border: 1px solid $mediumGrey;
width: 100px;
height: 100px;
margin-right: 10px;
margin-bottom: 10px;
color: #fff;
margin-right: 15px;
margin-bottom: 20px;
border-radius: 8px;
font-size: 13px;
font-size: 15px;
line-height: 14px;
text-align: center;
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
......@@ -115,25 +128,40 @@
width: 100%;
padding: 10px;
@include box-sizing(border-box);
color: #fff;
}
}
}
.new-component-templates {
display: none;
padding: 20px;
margin: 20px 40px 20px 40px;
border-radius: 3px;
border: 1px solid $mediumGrey;
background-color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, .2), 0 1px 0 rgba(255, 255, 255, .4) inset);
@include clearfix;
.cancel-button {
margin: 20px 0px 10px 10px;
@include white-button;
}
.problem-type-tabs {
display: none;
}
// specific menu types
&.new-component-problem {
padding-bottom:10px;
.ss-icon, .editor-indicator {
display: inline-block;
}
.problem-type-tabs {
display: inline-block;
}
}
}
......@@ -146,7 +174,6 @@
border: 1px solid $darkGreen;
background: tint($green,20%);
color: #fff;
@include transition(background-color .15s);
&:hover {
background: $brightGreen;
......@@ -154,19 +181,81 @@
}
}
.problem-type-tabs {
list-style-type: none;
border-radius: 0;
width: 100%;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: $lightBluishGrey;
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
li:first-child {
margin-left: 20px;
}
li {
float:left;
display:inline-block;
text-align:center;
width: auto;
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
background-color: tint($lightBluishGrey, 10%);
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
opacity:.8;
&:hover {
opacity:1;
background-color: tint($lightBluishGrey, 20%);
}
&.ui-state-active {
border: 0px;
@include active;
opacity:1;
}
}
a{
display: block;
padding: 15px 25px;
font-size: 15px;
line-height: 16px;
text-align: center;
color: #3c3c3c;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3);
}
}
.new-component-template {
margin-bottom: 20px;
li:last-child {
a {
background: #fff;
border: 0px;
color: #3c3c3c;
@include transition (none);
&:hover {
background: tint($green,30%);
color: #fff;
@include transition(background-color .15s);
}
}
li {
border:none;
border-bottom: 1px dashed $lightGrey;
color: #fff;
}
li:first-child {
a {
border-radius: 0 0 3px 3px;
border-bottom: 1px solid $darkGreen;
border-top: 0px;
}
}
li:nth-child(2) {
a {
border-radius: 3px 3px 0 0;
border-radius: 0px;
}
}
......@@ -175,18 +264,20 @@
display: block;
padding: 7px 20px;
border-bottom: none;
font-weight: 300;
font-weight: 500;
.name {
float: left;
.ss-icon {
@include transition(opacity .15s);
position: relative;
display: inline-block;
top: 1px;
font-size: 13px;
margin-right: 5px;
opacity: 0.5;
width: 17;
height: 21px;
vertical-align: middle;
}
}
......@@ -204,6 +295,7 @@
}
&:hover {
color: #fff;
.ss-icon {
opacity: 1.0;
......@@ -217,14 +309,18 @@
// specific editor types
.empty {
@include box-shadow(0 1px 3px rgba(0,0,0,0.2));
margin-bottom: 10px;
a {
border-bottom: 1px solid $darkGreen;
border-radius: 3px;
font-weight: 500;
background: $green;
line-height: 1.4;
font-weight: 400;
background: #fff;
color: #3c3c3c;
&:hover {
background: tint($green,30%);
color: #fff;
}
}
}
}
......@@ -233,7 +329,7 @@
text-align: center;
h5 {
color: $green;
color: $darkGreen;
}
}
......@@ -507,6 +603,7 @@
.edit-state-draft {
.visibility,
.edit-draft-message,
.view-button {
display: none;
......
$gw-column: 80px;
$gw-gutter: 20px;
$baseline: 20px;
// grid
$gw-column: ($baseline*3);
$gw-gutter: $baseline;
$fg-column: $gw-column;
$fg-gutter: $gw-gutter;
$fg-max-columns: 12;
$fg-max-width: 1400px;
$fg-min-width: 810px;
$fg-max-width: 1280px;
$fg-min-width: 900px;
// type
$sans-serif: 'Open Sans', $verdana;
$body-line-height: golden-ratio(.875em, 1);
$error-red: rgb(253, 87, 87);
$white: rgb(255,255,255);
// colors - new for re-org
$black: rgb(0,0,0);
$pink: rgb(182,37,104);
$error-red: rgb(253, 87, 87);
$white: rgb(255,255,255);
$gray: rgb(127,127,127);
$gray-l1: tint($gray,20%);
$gray-l2: tint($gray,40%);
$gray-l3: tint($gray,60%);
$gray-l4: tint($gray,80%);
$gray-l5: tint($gray,90%);
$gray-d1: shade($gray,20%);
$gray-d2: shade($gray,40%);
$gray-d3: shade($gray,60%);
$gray-d4: shade($gray,80%);
$blue: rgb(85, 151, 221);
$blue-l1: tint($blue,20%);
$blue-l2: tint($blue,40%);
$blue-l3: tint($blue,60%);
$blue-l4: tint($blue,80%);
$blue-l5: tint($blue,90%);
$blue-d1: shade($blue,20%);
$blue-d2: shade($blue,40%);
$blue-d3: shade($blue,60%);
$blue-d4: shade($blue,80%);
$pink: rgb(183, 37, 103);
$pink-l1: tint($pink,20%);
$pink-l2: tint($pink,40%);
$pink-l3: tint($pink,60%);
$pink-l4: tint($pink,80%);
$pink-l5: tint($pink,90%);
$pink-d1: shade($pink,20%);
$pink-d2: shade($pink,40%);
$pink-d3: shade($pink,60%);
$pink-d4: shade($pink,80%);
$green: rgb(37, 184, 90);
$green-l1: tint($green,20%);
$green-l2: tint($green,40%);
$green-l3: tint($green,60%);
$green-l4: tint($green,80%);
$green-l5: tint($green,90%);
$green-d1: shade($green,20%);
$green-d2: shade($green,40%);
$green-d3: shade($green,60%);
$green-d4: shade($green,80%);
$yellow: rgb(231, 214, 143);
$yellow-l1: tint($yellow,20%);
$yellow-l2: tint($yellow,40%);
$yellow-l3: tint($yellow,60%);
$yellow-l4: tint($yellow,80%);
$yellow-l5: tint($yellow,90%);
$yellow-d1: shade($yellow,20%);
$yellow-d2: shade($yellow,40%);
$yellow-d3: shade($yellow,60%);
$yellow-d4: shade($yellow,80%);
$shadow: rgba(0,0,0,0.2);
$shadow-l1: rgba(0,0,0,0.1);
$shadow-d1: rgba(0,0,0,0.4);
// colors - inherited
$baseFontColor: #3c3c3c;
$offBlack: #3c3c3c;
$black: rgb(0,0,0);
$white: rgb(255,255,255);
$blue: #5597dd;
$orange: #edbd3c;
$red: #b20610;
$green: #108614;
......@@ -34,4 +94,4 @@ $brightGreen: rgb(22, 202, 87);
$disabledGreen: rgb(124, 206, 153);
$darkGreen: rgb(52, 133, 76);
$lightBluishGrey: rgb(197, 207, 223);
$lightBluishGrey2: rgb(213, 220, 228);
$lightBluishGrey2: rgb(213, 220, 228);
\ No newline at end of file
@import 'bourbon/bourbon';
@import 'bourbon/addons/button';
@import 'vendor/normalize';
@import 'keyframes';
......@@ -8,8 +9,10 @@
@import "fonts";
@import "variables";
@import "cms_mixins";
@import "extends";
@import "base";
@import "header";
@import "footer";
@import "dashboard";
@import "courseware";
@import "subsection";
......@@ -26,6 +29,8 @@
@import "modal";
@import "alerts";
@import "login";
@import "account";
@import "index";
@import 'jquery-ui-calendar';
@import 'content-types';
......
......@@ -7,7 +7,7 @@
<section class="activation">
<h1>Account already active!</h1>
<p> This account has already been activated. <a href="/login">Log in here</a>.</p>
<p> This account has already been activated. <a href="/signin">Log in here</a>.</p>
</div>
</section>
......
......@@ -5,7 +5,7 @@
<section class="tos">
<div>
<h1>Activation Complete!</h1>
<p>Thanks for activating your account. <a href="/login">Log in here</a>.</p>
<p>Thanks for activating your account. <a href="/signin">Log in here</a>.</p>
</div>
</section>
......
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">assets</%block>
<%block name="title">Courseware Assets</%block>
<%block name="bodyclass">is-signedin course uploads</%block>
<%block name="title">Uploads &amp; Files</%block>
<%namespace name='static' file='static_content.html'/>
......@@ -33,12 +33,27 @@
</tr>
</script>
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Content</span>
<h1 class="title-1">Files &amp; Uploads</h1>
</div>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button upload-button new-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#xEB40;</i> Upload New File</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="main-wrapper">
<div class="inner-wrapper">
<div class="page-actions">
<a href="#" class="upload-button new-button">
<span class="upload-icon"></span>Upload New Asset
</a>
<input type="text" class="asset-search-input search wip-box" placeholder="search assets" style="display:none"/>
</div>
<article class="asset-library">
......
......@@ -5,23 +5,29 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>
<%block name="title"></%block> |
% if context_course:
<% ctx_loc = context_course.location %>
${context_course.display_name} |
% endif
edX Studio
</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="path_prefix" content="${MITX_ROOT_URL}">
<%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')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<title><%block name="title"></%block></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="path_prefix" content="${MITX_ROOT_URL}">
<%block name="header_extras"></%block>
</head>
<body class="<%block name='bodyclass'></%block> hide-wip">
<%include file="widgets/header.html" args="active_tab=active_tab"/>
<%include file="widgets/header.html" />
<%include file="courseware_vendor_js.html"/>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
......@@ -47,9 +53,9 @@
</script>
<%block name="content"></%block>
<%include file="widgets/footer.html" />
<%block name="jsextra"></%block>
</body>
</html>
......
<%inherit file="base.html" />
<%block name="title">Course Manager</%block>
<%include file="widgets/header.html"/>
<%block name="content">
......
......@@ -2,8 +2,9 @@
<%namespace name='static' file='static_content.html'/>
<!-- TODO decode course # from context_course into title -->
<%block name="title">Course Info</%block>
<%block name="bodyclass">course-info</%block>
<%block name="title">Updates</%block>
<%block name="bodyclass">is-signedin course course-info updates</%block>
<%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
......@@ -41,16 +42,38 @@
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Content</span>
<h1 class="title-1">Course Updates</h1>
</div>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class=" button new-button new-update-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Update</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<div class="introduction">
<p clas="copy">Course updates are announcements or notifications you want to share with your class. Other course authors have used them for important exam/date reminders, change in schedules, and to call out any important steps students need to be aware of.</p>
</div>
</section>
</div>
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Course Info</h1>
<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 class="sidebar window course-handouts" id="course-handouts-view"></div>
......
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Edit Static Page</%block>
<%block name="bodyclass">edit-static-page</%block>
<%block name="title">Editing Static Page</%block>
<%block name="bodyclass">is-signedin course pages edit-static-page</%block>
<%block name="content">
<div class="main-wrapper">
......
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Tabs</%block>
<%block name="bodyclass">static-pages</%block>
<%block name="title">Static Pages</%block>
<%block name="bodyclass">is-signedin course pages static-pages</%block>
<%block name="jsextra">
<script type='text/javascript'>
......@@ -9,25 +9,49 @@
el: $('.main-wrapper'),
model: new CMS.Models.Module({
id: '${context_course.location}'
})
}),
mast: $('.wrapper-mast')
});
</script>
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Content</span>
<h1 class="title-1">Static Pages</h1>
</div>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button new-tab"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Page</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<div class="introduction has-links">
<p class="copy">Static Pages are additional pages that supplement your Courseware. Other course authors have used them to share a syllabus, calendar, handouts, and more.</p>
<nav class="nav-introduction-supplementary">
<ul>
<li class="nav-item">
<a rel="modal" href="#preview-lms-staticpages"><i class="ss-icon ss-symbolicons-block icon icon-information">&#x2753;</i>How do Static Pages look to students in my course?</a>
</li>
</ul>
</nav>
</div>
</section>
</div>
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="unit-body">
<div class="details">
<h2>Here you can add and manage additional pages for your course</h2>
<p>These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.</p>
</div>
<div class="page-actions">
<a href="#" class="new-button new-tab">
<span class="plus-icon white"></span>New Page
</a>
</div>
<div class="tab-list">
<ol class='components'>
......@@ -43,4 +67,17 @@
</article>
</div>
</div>
<div class="content-modal" id="preview-lms-staticpages">
<h3 class="title">How Static Pages are Used in Your Course</h3>
<figure>
<img src="/static/img/preview-lms-staticpages.png" alt="Preview of how Static Pages are used in your course" />
<figcaption class="description">These pages will be presented in your course's main navigation alongside Courseware, Course Info, Discussion, etc.</figcaption>
</figure>
<a href="#" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon-close icon">&#x2421;</i>
<span class="label">close modal</span>
</a>
</div>
</%block>
\ No newline at end of file
......@@ -7,8 +7,9 @@
%>
<%! from django.core.urlresolvers import reverse %>
<%block name="bodyclass">subsection</%block>
<%block name="title">CMS Subsection</%block>
<%block name="bodyclass">is-signedin course subsection</%block>
<%namespace name="units" file="widgets/units.html" />
<%namespace name='static' file='static_content.html'/>
......@@ -21,7 +22,7 @@
<article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input">
<label>Display Name:</label>
<input type="text" value="${subsection.lms.display_name}" class="subsection-display-name-input" data-metadata-name="display_name"/>
<input type="text" value="${subsection.lms.display_name | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div class="sortable-unit-list">
<label>Units:</label>
......@@ -97,6 +98,7 @@
</div>
</div>
</div>
</div>
</%block>
<%block name="jsextra">
......@@ -108,7 +110,7 @@
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
<script type="text/javascript">
$(document).ready(function() {
......
......@@ -2,10 +2,19 @@
<%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Export</%block>
<%block name="bodyclass">export</%block>
<%block name="title">Export Course</%block>
<%block name="bodyclass">is-signedin course tools export</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Tools</span>
<h1 class="title-1">Course Export</h1>
</div>
</header>
</div>
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="export-overview">
......
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Welcome</%block>
<%block name="bodyclass">not-signedin index howitworks</%block>
<%block name="content">
<div class="wrapper-content-header wrapper">
<section class="content content-header">
<header>
<h1>Welcome to <span class="logo">edX Studio</span></h1>
<p class="tagline">Studio helps manage your courses online, so you can focus on teaching them</p>
</header>
</section>
</div>
<div class="wrapper-content-features wrapper">
<section class="content content-features">
<header>
<h2 class="sr">Studio's Many Features</h2>
</header>
<ol class="list-features">
<li class="feature">
<figure class="img zoom">
<a rel="modal" href="#hiw-feature1">
<img src="/static/img/thumb-hiw-feature1.png" alt="Studio Helps You Keep Your Courses Organized" />
<figcaption class="sr">Studio Helps You Keep Your Courses Organized</figcaption>
<span class="action-zoom">
<i class="ss-icon ss-symbolicons-block icon icon-zoom">&#xE002;</i>
</span>
</a>
</figure>
<div class="copy">
<h3>Keeping Your Course Organized</h3>
<p>The backbone of your course is how it is organized. Studio offers an <strong>Outline</strong> editor, providing a simple hierarchy and easy drag and drop to help you and your students stay organized.</p>
<ul class="list-proofpoints">
<li class="proofpoint">
<h4 class="title">Simple Organization For Content</h4>
<p>Studio uses a simple hierarchy of <strong>sections</strong> and <strong>subsections</strong> to organize your content.</p>
</li>
<li class="proofpoint">
<h4 class="title">Change Your Mind Anytime</h4>
<p>Draft your outline and build content anywhere. Simple drag and drop tools let your reorganize quickly.</p>
</li>
<li class="proofpoint">
<h4 class="title">Go A Week Or A Semester At A Time</h4>
<p>Build and release <strong>sections</strong> to your students incrementally. You don't have to have it all done at once.</p>
</li>
</ul>
</div>
</li>
<li class="feature">
<figure class="img zoom">
<a rel="modal" href="#hiw-feature2">
<img src="/static/img/thumb-hiw-feature2.png" alt="Learning is More than Just Lectures" />
<figcaption class="sr">Learning is More than Just Lectures</figcaption>
<span class="action-zoom">
<i class="ss-icon ss-symbolicons-block icon icon-zoom">&#xE002;</i>
</span>
</a>
</figure>
<div class="copy">
<h3>Learning is More than Just Lectures</h3>
<p>Studio lets you weave your content together in a way that reinforces learning &mdash; short video lectures interleaved with exercises and more. Insert videos and author a wide variety of exercise types with just a few clicks. </p>
<ul class="list-proofpoints">
<li class="proofpoint">
<h4 class="title">Create Learning Pathways</h4>
<p>Help your students understand a small interactive piece at a time with multimedia, HTML, and exercises.</p>
</li>
<li class="proofpoint">
<h4 class="title">Work Visually, Organize Quickly</h4>
<p>Work visually and see exactly what your students will see. Reorganize all your content with drag and drop.</p>
</li>
<li class="proofpoint">
<h4 class="title">A Broad Library of Problem Types</h4>
<p>It's more than just multiple choice. Studio has nearly a dozen types of problems to challenge your learners.</p>
</li>
</ul>
</div>
</li>
<li class="feature">
<figure class="img zoom">
<a rel="modal" href="#hiw-feature3">
<img src="/static/img/thumb-hiw-feature3.png" alt="Studio Gives You Simple, Fast, and Incremental Publishing. With Friends." />
<figcaption class="sr">Studio Gives You Simple, Fast, and Incremental Publishing. With Friends.</figcaption>
<span class="action-zoom">
<i class="ss-icon ss-symbolicons-block icon icon-zoom">&#xE002;</i>
</span>
</a>
</figure>
<div class="copy">
<h3>Simple, Fast, and Incremental Publishing. With Friends.</h3>
<p>Studio works like web applications you already know, yet understands how you build curriculum. Instant publishing to the web when you want it, incremental release when it makes sense. And with co-authors, you can have a whole team building a course, together.</p>
<ul class="list-proofpoints">
<li class="proofpoint">
<h4 class="title">Instant Changes</h4>
<p>Caught a bug? No problem. When you want, your changes to live when you hit Save.</p>
</li>
<li class="proofpoint">
<h4 class="title">Release-On Date Publishing</h4>
<p>When you've finished a <strong>section</strong>, pick when you want it to go live and Studio takes care of the rest. Build your course incrementally.</p>
</li>
<li class="proofpoint">
<h4 class="title">Work in Teams</h4>
<p>Co-authors have full access to all the same authoring tools. Make your course better through a team effort.</p>
</li>
</ul>
</div>
</li>
</ol>
</section>
</div>
<div class="wrapper-content-cta wrapper">
<section class="content content-cta">
<header>
<h2 class="sr">Sign Up for Studio Today!</h2>
</header>
<ul class="list-actions">
<li>
<a href="${reverse('signup')}" class="action action-primary">Sign Up &amp; Start Making an edX Course</a>
</li>
<li>
<a href="${reverse('login')}" class="action action-secondary">Already have a Studio Account? Sign In</a>
</li>
</ul>
</section>
</div>
<div class="content-modal" id="hiw-feature1">
<h3 class="title">Outlining Your Course</h3>
<figure>
<img src="/static/img/hiw-feature1.png" alt="" />
<figcaption class="description">Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.</figcaption>
</figure>
<a href="#" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close modal</span>
</a>
</div>
<div class="content-modal" id="hiw-feature2">
<h3 class="title">More than Just Lectures</h3>
<figure>
<img src="/static/img/hiw-feature2.png" alt="" />
<figcaption class="description">Quickly create videos, text snippets, inline discussions, and a variety of problem types.</figcaption>
</figure>
<a href="#" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close modal</span>
</a>
</div>
<div class="content-modal" id="hiw-feature3">
<h3 class="title">Publishing on Date</h3>
<figure>
<img src="/static/img/hiw-feature3.png" alt="" />
<figcaption class="description">Simply set the date of a section or subsection, and Studio will publish it to your students for you.</figcaption>
</figure>
<a href="#" rel="view" class="action action-modal-close">
<i class="ss-icon ss-symbolicons-block icon icon-close">&#x2421;</i>
<span class="label">close modal</span>
</a>
</div>
</%block>
\ No newline at end of file
......@@ -2,15 +2,23 @@
<%namespace name='static' file='static_content.html'/>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Import</%block>
<%block name="bodyclass">import</%block>
<%block name="title">Import Course</%block>
<%block name="bodyclass">is-signedin course tools import</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Tools</span>
<h1 class="title-1">Course Import</h1>
</div>
</header>
</div>
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="import-overview">
<div class="description">
<h2>Please <a href="https://edge.edx.org/courses/edX/edx101/edX_Studio_Reference/about" target="_blank">read the documentation</a> before attempting an import!</h2>
<p><strong>Importing a new course will delete all content currently associated with your course
and replace it with the contents of the uploaded file.</strong></p>
<p>File uploads must be gzipped tar files (.tar.gz or .tgz) containing, at a minimum, a <code>course.xml</code> file.</p>
......
<%inherit file="base.html" />
<%block name="bodyclass">index</%block>
<%block name="title">Courses</%block>
<%block name="bodyclass">is-signedin index dashboard</%block>
<%block name="header_extras">
<script type="text/template" id="new-course-template">
......@@ -32,35 +33,57 @@
</%block>
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>My Courses</h1>
<article class="my-classes">
% if user.is_active:
% if not disable_course_creation:
<a href="#" class="new-button new-course-button"><span class="plus-icon white"></span> New Course</a>
%endif
<ul class="class-list">
%for course, url in courses:
<li>
<a href="${url}" class="class-name">
<span class="class-name">${course}</span>
<!--
<span class="detail">Started: 9/21/2012</span>
<span class="detail">Ends: 10/21/2012</span>
-->
</a>
</li>
%endfor
</ul>
% else:
<div class='warn-msg'>
<p>
In order to start authoring courses using edX studio, please click on the activation link in your email.
</p>
</div>
% endif
</article>
</div>
</div>
</%block>
<div class="wrapper-mast wrapper">
<header class="mast has-actions">
<div class="title">
<h1 class="title-1">My Courses</h1>
</div>
% if user.is_active:
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
% if not disable_course_creation:
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Course</a>
% endif
</li>
</ul>
</nav>
% endif
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<div class="introduction">
<p class="copy"><strong>Welcome, ${ user.username }</strong>. Here are all of the courses you are currently authoring in Studio:</p>
</div>
</section>
</div>
<div class="main-wrapper">
<div class="inner-wrapper">
<article class="my-classes">
% if user.is_active:
<ul class="class-list">
%for course, url, lms_link in courses:
<li>
<a class="class-link" href="${url}" class="class-name">
<span class="class-name">${course}</span>
</a>
<a href="${lms_link}" rel="external" class="button view-button view-live-button"><i class="ss-icon ss-symbolicons-block icon icon-view">&#xE010;</i>View Live</a>
</li>
%endfor
</ul>
% else:
<div class='warn-msg'>
<p>
In order to start authoring courses using edX Studio, please click on the activation link in your email.
</p>
</div>
% endif
</article>
</div>
</div>
</%block>
\ No newline at end of file
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Log in</%block>
<%block name="bodyclass">no-header</%block>
<%block name="title">Sign In</%block>
<%block name="bodyclass">not-signedin signin</%block>
<%block name="content">
<div class="edx-studio-logo-large"></div>
<article class="log-in-box">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1>Log in to edX studio</h1>
<h1 class="title title-1">Sign In to edX Studio</h1>
<a href="${reverse('signup')}" class="action action-signin">Don't have a Studio Account? Sign up!</a>
</header>
<form class="log-in-form" id="login_form" action="login_post" method="post">
<div class="row">
<label>Email</label>
<input name="email" type="email" class="email-field" tabindex="1">
</div>
<div class="row">
<label>Password <a href="${forgot_password_link}" class="forgot-button">Forgot password?</a></label>
<input name="password" type="password" class="password-field" tabindex="2">
</div>
<div class="row form-actions">
<input name="submit" type="submit" value="Log In" class="log-in-button" tabindex="3">
<span class="or">or</span>
<a href="${reverse('signup')}" class="sign-up-button" tabindex="4">Sign up</a>
<article class="content-primary" role="main">
<form id="login_form" method="post" action="login_post">
<fieldset>
<legend class="sr">Required Information to Sign In to edX Studio</legend>
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">Email Address</label>
<input id="email" type="email" name="email" placeholder="e.g. jane.doe@gmail.com" />
</li>
<li class="field text required" id="field-password">
<a href="${forgot_password_link}" class="action action-forgotpassword" tabindex="-1">Forgot password?</a>
<label for="password">Password</label>
<input id="password" type="password" name="password" />
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">Sign In to edX Studio</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
<aside class="content-supplementary" role="complimentary">
<h2 class="sr">Studio Support</h2>
<div class="bit">
<h3 class="title-3">Need Help?</h3>
<p>Having trouble with your account? Use <a href="http://help.edge.edx.org" rel="external">our support center</a> to look over self help steps, find solutions others have found to the same problem, or let us know of your issue.</p>
</div>
</form>
</article>
</aside>
</section>
</div>
</%block>
<%block name="jsextra">
<script type="text/javascript">
(function() {
function getCookie(name) {
......@@ -51,12 +77,16 @@
submit_data,
function(json) {
if(json.success) {
location.href = "${reverse('index')}";
var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search));
if (next && next.length > 1) {
location.href = next[1];
}
else location.href = "${reverse('homepage')}";
} else if($('#login_error').length == 0) {
$('#login_form').prepend('<div id="login_error">' + json.value + '</div>');
$('#login_error').slideDown(150);
$('#login_form').prepend('<div id="login_error" class="message message-status error">' + json.value + '</span></div>');
$('#login_error').addClass('is-shown');
} else {
$('#login_error').stop().slideDown(150);
$('#login_error').stop().addClass('is-shown');
$('#login_error').html(json.value);
}
}
......@@ -64,5 +94,4 @@
});
})(this)
</script>
</%block>
</%block>
\ No newline at end of file
<%inherit file="base.html" />
<%block name="title">Course Staff Manager</%block>
<%block name="bodyclass">users</%block>
<%block name="bodyclass">is-signedin course users settings team</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Settings</span>
<h1 class="title-1">Course Team</h1>
</div>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
%if allow_actions:
<li class="nav-item">
<a href="#" class="button new-button new-user-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New User</a>
</li>
%endif
</ul>
</nav>
</header>
</div>
<div class="main-wrapper">
<div class="inner-wrapper">
<div class="page-actions">
%if allow_actions:
<a href="#" class="new-button new-user-button">
<span class="plus-icon white"></span>New User
</a>
%endif
</div>
<div class="details">
<p>The following list of users have been designated as course staff. This means that these users will have permissions to modify course content. You may add additional course staff below, if you are the course instructor. Please note that they must have already registered and verified their account.</p>
......
......@@ -6,7 +6,8 @@
from datetime import datetime
%>
<%! from django.core.urlresolvers import reverse %>
<%block name="title">CMS Courseware Overview</%block>
<%block name="title">Course Outline</%block>
<%block name="bodyclass">is-signedin course outline</%block>
<%namespace name='static' file='static_content.html'/>
<%namespace name="units" file="widgets/units.html" />
......@@ -31,7 +32,7 @@
window.graderTypes.course_location = new CMS.Models.Location('${parent_location}');
window.graderTypes.reset(${course_graders|n});
}
$(".gradable-status").each(function(index, ele) {
var gradeView = new CMS.Views.OverviewAssignmentGrader({
el : ele,
......@@ -39,7 +40,7 @@
});
});
});
</script>
</%block>
......@@ -119,13 +120,33 @@
</div>
</div>
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<div class="title">
<span class="title-sub">Course Content</span>
<h1 class="title-1">Course Outline</h1>
</div>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="toggle-button toggle-button-sections"><i class="ss-icon ss-symbolicons-block icon">up</i> <span class="label">Collapse All Sections</span></a>
</li>
<li class="nav-item">
<a href="#" class="button new-button new-courseware-section-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Section</a>
</li>
<li class="nav-item">
<a href="${lms_link}" rel="external" class="button view-button view-live-button"><i class="ss-icon ss-symbolicons-block icon icon-view">&#xE010;</i>View Live</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="main-wrapper">
<div class="inner-wrapper">
<div class="page-actions">
<a href="#" class="new-button new-courseware-section-button"><span class="plus-icon white"></span> New Section</a>
<a href="#" class="toggle-button toggle-button-sections"><i class="ss-icon ss-symbolicons-block">up</i> <span class="label">Collapse All Sections</span></a>
</div>
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
<article class="courseware-overview" data-course-id="${context_course.location.url()}">
% for section in sections:
<section class="courseware-section branch" data-id="${section.location}">
<header>
......@@ -135,7 +156,7 @@
<h3 class="section-name">
<span data-tooltip="Edit this section's name" class="section-name-span">${section.lms.display_name}</span>
<form class="section-name-edit" style="display:none">
<input type="text" value="${section.lms.display_name}" class="edit-section-name" autocomplete="off"/>
<input type="text" value="${section.lms.display_name | h}" class="edit-section-name" autocomplete="off"/>
<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>
......@@ -153,9 +174,9 @@
<span class="published-status"><strong>Will Release:</strong> ${start_date_str} at ${start_time_str}</span>
<a href="#" class="edit-button" data-date="${start_date_str}" data-time="${start_time_str}" data-id="${section.location}">Edit</a>
%endif
</div>
</div>
</div>
<div class="item-actions">
<a href="#" data-tooltip="Delete this section" class="delete-button delete-section-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
......@@ -178,12 +199,12 @@
<span class="subsection-name"><span class="subsection-name-value">${subsection.lms.display_name}</span></span>
</a>
</div>
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}">
</div>
<div class="item-actions">
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Delete this subsection" class="delete-button delete-subsection-button"><span class="delete-icon"></span></a>
<a href="#" data-tooltip="Drag to reorder" class="drag-handle"></a>
</div>
</div>
......
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Advanced Settings</%block>
<%block name="bodyclass">is-signedin course advanced settings</%block>
<%namespace name='static' file='static_content.html'/>
<%!
from contentstore import utils
%>
<%block name="jsextra">
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/advanced.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/settings/advanced_view.js')}"></script>
<script type="text/javascript">
$(document).ready(function () {
// proactively populate advanced b/c it has the filtered list and doesn't really follow the model pattern
var advancedModel = new CMS.Models.Settings.Advanced(${advanced_dict | n}, {parse: true});
advancedModel.blacklistKeys = ${advanced_blacklist | n};
advancedModel.url = "${reverse('course_advanced_settings_updates', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}";
var editor = new CMS.Views.Settings.Advanced({
el: $('.settings-advanced'),
model: advancedModel
});
editor.render();
});
</script>
</%block>
<%block name="content">
<div class="wrapper-content wrapper">
<section class="content">
<header class="page">
<span class="title-sub">Settings</span>
<h1 class="title-1">Advanced Settings</h1>
</header>
<article class="content-primary" role="main">
<form id="settings_advanced" class="settings-advanced" method="post" action="">
<div class="message message-status confirm">
Your policy changes have been saved.
</div>
<div class="message message-status error">
There was an error saving your information. Please see below.
</div>
<section class="group-settings advanced-policies">
<header>
<h2 class="title-2">Manual Policy Definition</h2>
<span class="tip">Manually Edit Course Policy Values (JSON Key / Value pairs)</span>
</header>
<p class="instructions"><strong>Warning</strong>: Add only manual policy data that you are familiar
with.</p>
<ul class="list-input course-advanced-policy-list enum">
</ul>
<div class="actions">
<a href="#" class="button new-button new-advanced-policy-item add-policy-data">
<span class="plus-icon white"></span>New Manual Policy
</a>
</div>
</section>
</form>
</article>
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title-3">How will these settings be used?</h3>
<p>Manual policies are JSON-based key and value pairs that allow you add additional settings which edX Studio will use when generating your course.</p>
<p>Any policies you define here will override any other information you've defined elsewhere in Studio. With this in mind, please be very careful and do not add policies that you are unfamiliar with (both their purpose and their syntax).</p>
</div>
<div class="bit">
% if context_course:
<% ctx_loc = context_course.location %>
<%! from django.core.urlresolvers import reverse %>
<h3 class="title-3">Other Course Settings</h3>
<nav class="nav-related">
<ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Details &amp; Schedule</a></li>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
</ul>
</nav>
% endif
</div>
</aside>
</section>
</div>
<!-- notification: change has been made and a save is needed -->
<div class="wrapper wrapper-notification wrapper-notification-warning">
<div class="notification warning">
<div class="copy">
<i class="ss-icon ss-symbolicons-block icon icon-warning">&#x26A0;</i>
<p><strong>Note: </strong>Your changes will not take effect until you <strong>save your
progress</strong>. Take care with key and value formatting, as validation is <strong>not implemented</strong>.</p>
</div>
<div class="actions">
<ul>
<li><a href="#" class="save-button">Save</a></li>
<li><a href="#" class="cancel-button">Cancel</a></li>
</ul>
</div>
</div>
</div>
</%block>
\ No newline at end of file
<%inherit file="base.html" />
<%block name="title">Grading</%block>
<%block name="bodyclass">is-signedin course grading settings</%block>
<%namespace name='static' file='static_content.html'/>
<%!
from contentstore import utils
%>
<%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 type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/settings/settings_grading_view.js')}"></script>
<script type="text/javascript">
$(document).ready(function(){
$("form :input").focus(function() {
$("label[for='" + this.id + "']").addClass("is-focused");
}).blur(function() {
$("label").removeClass("is-focused");
});
var editor = new CMS.Views.Settings.Grading({
el: $('.settings-grading'),
model : new CMS.Models.Settings.CourseGradingPolicy(${course_details|n},{parse:true})
});
editor.render();
});
</script>
</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-subtitle">
<div class="title">
<span class="title-sub">Settings</span>
<h1 class="title-1">Grading</h1>
</div>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<form id="settings_details" class="settings-grading" method="post" action="">
<section class="group-settings grade-range">
<header>
<h2 class="title-2">Overall Grade Range</h2>
<span class="tip">Your overall grading scale for student final grades</span>
</header>
<ol class="list-input">
<li class="field" id="field-course-grading-range">
<div class="grade-controls course-grading-range well">
<a href="#" class="new-grade-button"><span class="plus-icon"></span></a>
<div class="grade-slider">
<div class="grade-bar">
<ol class="increments">
<li class="increment-0">0</li>
<li class="increment-10">10</li>
<li class="increment-20">20</li>
<li class="increment-30">30</li>
<li class="increment-40">40</li>
<li class="increment-50">50</li>
<li class="increment-60">60</li>
<li class="increment-70">70</li>
<li class="increment-80">80</li>
<li class="increment-90">90</li>
<li class="increment-100">100</li>
</ol>
<ol class="grades">
</ol>
</div>
</div>
</div>
</li>
</ol>
</section>
<hr class="divide" />
<section class="group-settings grade-rules">
<header>
<h2 class="title-2">Grading Rules &amp; Policies</h2>
<span class="tip">Deadlines, requirements, and logistics around grading student work</span>
</header>
<ol class="list-input">
<li class="field text" id="field-course-grading-graceperiod">
<label for="course-grading-graceperiod">Grace Period on Deadline:</label>
<input type="text" class="short time" id="course-grading-graceperiod" value="0:00" placeholder="e.g. 10 minutes">
<span class="tip tip-inline">Leeway on due dates</span>
</li>
</ol>
</section>
<hr class="divide" />
<section class="group-settings assignment-types">
<header>
<h2 class="title-2">Assignment Types</h2>
<span class="tip">Categories and labels for any exercises that are gradable</span>
</header>
<ol class="list-input course-grading-assignment-list enum">
</ol>
<div class="actions">
<a href="#" class="new-button new-course-grading-item add-grading-data">
<span class="plus-icon white"></span>New Assignment Type
</a>
</div>
</section>
</form>
</article>
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title-3">How will these settings be used?</h3>
<p>Your grading settings will be used to calculate students grades and performance.</p>
<p>Overall grade range will be used in students' final grades, which are calculated by the weighting you determine for each custom assignment type.</p>
</div>
<div class="bit">
% if context_course:
<% ctx_loc = context_course.location %>
<%! from django.core.urlresolvers import reverse %>
<h3 class="title-3">Other Course Settings</h3>
<nav class="nav-related">
<ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">Details &amp; Schedule</a></li>
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li>
</ul>
</nav>
% endif
</div>
</aside>
</section>
</div>
</%block>
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Sign up</%block>
<%block name="bodyclass">no-header</%block>
<%block name="title">Sign Up</%block>
<%block name="bodyclass">not-signedin signup</%block>
<%block name="content">
<div class="edx-studio-logo-large"></div>
<article class="sign-up-box">
<header>
<h1>Register for edX studio</h1>
</header>
<form id="register_form" method="post">
<div id="register_error" name="register_error"></div>
<div class="row">
<label>Email</label>
<input name="email" type="email">
</div>
<div class="row">
<label>Password</label>
<input name="password" type="password">
</div>
<div class="row">
<label>Public Username</label>
<input name="username" type="text">
</div>
<div class="row">
<label>Full Name</label>
<input name="name" type="text">
</div>
<div class="row">
<div class="split">
<label>Your Location</label>
<input name="location" type="text">
<div class="wrapper-content wrapper">
<section class="content">
<header>
<h1 class="title title-1">Sign Up for edX Studio</h1>
<a href="${reverse('login')}" class="action action-signin">Already have a Studio Account? Sign in</a>
</header>
<p class="introduction">Ready to start creating online courses? Sign up below and start creating your first edX course today.</p>
<article class="content-primary" role="main">
<form id="register_form" method="post" action="register_post">
<div id="register_error" name="register_error" class="message message-status message-status error">
</div>
<fieldset>
<legend class="sr">Required Information to Sign Up for edX Studio</legend>
<ol class="list-input">
<li class="field text required" id="field-email">
<label for="email">Email Address</label>
<input id="email" type="email" name="email" placeholder="e.g. jane.doe@gmail.com" />
</li>
<li class="field text required" id="field-password">
<label for="password">Password</label>
<input id="password" type="password" name="password" />
</li>
<li class="field text required" id="field-username">
<label for="username">Public Username</label>
<input id="username" type="text" name="username" placeholder="e.g. janedoe" />
<span class="tip tip-stacked">This will be used in public discussions with your courses and in our edX101 support forums</span>
</li>
<li class="field text required" id="field-name">
<label for="name">Full Name</label>
<input id="name" type="text" name="name" placeholder="e.g. Jane Doe" />
</li>
<li class="field-group">
<div class="field text" id="field-location">
<label for="location">Your Location</label>
<input class="short" id="location" type="text" name="location" />
</div>
<div class="field text" id="field-language">
<label for="language">Preferred Language</label>
<input class="short" id="language" type="text" name="language" />
</div>
</li>
<li class="field checkbox required" id="field-tos">
<input id="tos" name="terms_of_service" type="checkbox" value="true" />
<label for="tos">I agree to the Terms of Service</label>
</li>
</ol>
</fieldset>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">Create My Account & Start Authoring Courses</button>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</article>
<aside class="content-supplementary" role="complimentary">
<h2 class="sr">Common Studio Questions</h2>
<div class="bit">
<h3 class="title-3">Who is Studio for?</h3>
<p>Studio is for anyone that wants to create online courses that leverage the global edX platform. Our users are often faculty members, teaching assistants and course staff, and members of instructional technology groups.</p>
</div>
<div class="bit">
<h3 class="title-3">How technically savvy do I need to be to create courses in Studio?</h3>
<p>Studio is designed to be easy to use by almost anyone familiar with common web-based authoring environments (Wordpress, Moodle, etc.). No programming knowledge is required, but for some of the more advanced features, a technical background would be helpful. As always, we are here to help, so don't hesitate to dive right in.</p>
</div>
<div class="split">
<label>Preferred Language</label>
<input name="language" type="text">
<div class="bit">
<h3 class="title-3">I've never authored a course online before. Is there help?</h3>
<p>Absolutely. We have created an online course, edX101, that describes some best practices: from filming video, creating exercises, to the basics of running an online course. Additionally, we're always here to help, just drop us a note.</p>
</div>
</div>
<div class="row">
<label class="terms-of-service">
<input name="terms_of_service" type="checkbox" value="true">
I agree to the
<a href="#">Terms of Service</a>
</label>
</div>
<!-- no honor code for CMS, but need it because we're using the lms student object -->
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
<div class="row form-actions submit">
<input name="submit" type="submit" value="Create My Account" class="create-account-button">
<p class="enrolled">Already enrolled? <a href="/">Log In.</a></p>
</div>
</form>
</article>
<script type="text/javascript">
(function() {
function getCookie(name) {
return $.cookie(name);
}
function postJSON(url, data, callback) {
$.ajax({type:'POST',
url: url,
dataType: 'json',
data: data,
success: callback,
headers : {'X-CSRFToken':getCookie('csrftoken')}
</aside>
</section>
</div>
</%block>
<%block name="jsextra">
<script type="text/javascript">
(function() {
$("form :input").focus(function() {
$("label[for='" + this.id + "']").addClass("is-focused");
}).blur(function() {
$("label").removeClass("is-focused");
});
}
$('form#register_form').submit(function(e) {
e.preventDefault();
var submit_data = $('#register_form').serialize();
postJSON('/create_account',
submit_data,
function(json) {
if(json.success) {
location.href = "${reverse('index')}";
} else {
$('#register_error').html(json.value).stop().slideDown(150);
function getCookie(name) {
return $.cookie(name);
}
// form validation
function postJSON(url, data, callback) {
$.ajax({type:'POST',
url: url,
dataType: 'json',
data: data,
success: callback,
headers : {'X-CSRFToken':getCookie('csrftoken')}
});
}
$('form#register_form').submit(function(e) {
e.preventDefault();
var submit_data = $('#register_form').serialize();
postJSON('/create_account',
submit_data,
function(json) {
if(json.success) {
location.href = "${reverse('index')}";
} else {
$('#register_error').html(json.value).stop().addClass('is-shown');
}
}
}
);
});
})(this)
</script>
);
});
})(this)
</script>
</%block>
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment