diff --git a/apt-packages.txt b/apt-packages.txt index b783ccb67e..0560dfcbc2 100644 --- a/apt-packages.txt +++ b/apt-packages.txt @@ -9,6 +9,7 @@ gfortran liblapack-dev libfreetype6-dev libpng12-dev +libjpeg-dev libxml2-dev libxslt-dev yui-compressor diff --git a/cms/.coveragerc b/cms/.coveragerc index b7ae181e99..4f0dbebe79 100644 --- a/cms/.coveragerc +++ b/cms/.coveragerc @@ -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 diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index f868b598a8..925bb101f3 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -20,7 +20,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$') @@ -113,7 +114,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) @@ -127,16 +132,19 @@ 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)) + 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'): diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index d2d038a928..e394165f08 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -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') diff --git a/cms/djangoapps/contentstore/features/signup.feature b/cms/djangoapps/contentstore/features/signup.feature index 8a6f93d33b..03a1c9524a 100644 --- a/cms/djangoapps/contentstore/features/signup.feature +++ b/cms/djangoapps/contentstore/features/signup.feature @@ -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." diff --git a/cms/djangoapps/contentstore/features/signup.py b/cms/djangoapps/contentstore/features/signup.py index e105b674f7..a786225ead 100644 --- a/cms/djangoapps/contentstore/features/signup.py +++ b/cms/djangoapps/contentstore/features/signup.py @@ -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$') diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py index 796184baa0..7ed4505c94 100644 --- a/cms/djangoapps/contentstore/module_info_model.py +++ b/cms/djangoapps/contentstore/module_info_model.py @@ -1,37 +1,35 @@ -import logging from static_replace import replace_static_urls from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore import Location -from xmodule.modulestore.django import modulestore -from lxml import etree -import re -from django.http import HttpResponseBadRequest, Http404 +from django.http import Http404 def get_module_info(store, location, parent_location=None, rewrite_static_links=False): - try: - if location.revision is None: - module = store.get_item(location) - else: - module = store.get_item(location) - except ItemNotFoundError: - raise Http404 + try: + if location.revision is None: + module = store.get_item(location) + else: + module = store.get_item(location) + except ItemNotFoundError: + # create a new one + template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) + module = store.clone_item(template_location, location) - data = module.definition['data'] - if rewrite_static_links: - data = replace_static_urls( - module.definition['data'], - None, - course_namespace=Location([ - module.location.tag, - module.location.org, - module.location.course, + data = module.definition['data'] + if rewrite_static_links: + data = replace_static_urls( + module.definition['data'], None, - None - ]) - ) + course_namespace=Location([ + module.location.tag, + module.location.org, + module.location.course, + None, + None + ]) + ) - return { + return { 'id': module.location.url(), 'data': data, 'metadata': module.metadata @@ -39,58 +37,56 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links= def set_module_info(store, location, post_data): - module = None - isNew = False - try: - if location.revision is None: - module = store.get_item(location) - else: - module = store.get_item(location) - except: - pass + module = None + try: + if location.revision is None: + module = store.get_item(location) + else: + module = store.get_item(location) + except: + pass - if module is None: - # new module at this location - # presume that we have an 'Empty' template - template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) - module = store.clone_item(template_location, location) - isNew = True + if module is None: + # new module at this location + # presume that we have an 'Empty' template + template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) + module = store.clone_item(template_location, location) - if post_data.get('data') is not None: - data = post_data['data'] - store.update_item(location, data) + if post_data.get('data') is not None: + data = post_data['data'] + store.update_item(location, data) - # cdodge: note calling request.POST.get('children') will return None if children is an empty array - # so it lead to a bug whereby the last component to be deleted in the UI was not actually - # deleting the children object from the children collection - if 'children' in post_data and post_data['children'] is not None: - children = post_data['children'] - store.update_children(location, children) + # cdodge: note calling request.POST.get('children') will return None if children is an empty array + # so it lead to a bug whereby the last component to be deleted in the UI was not actually + # deleting the children object from the children collection + if 'children' in post_data and post_data['children'] is not None: + children = post_data['children'] + store.update_children(location, children) - # cdodge: also commit any metadata which might have been passed along in the - # POST from the client, if it is there - # NOTE, that the postback is not the complete metadata, as there's system metadata which is - # not presented to the end-user for editing. So let's fetch the original and - # 'apply' the submitted metadata, so we don't end up deleting system metadata - if post_data.get('metadata') is not None: - posted_metadata = post_data['metadata'] - - # update existing metadata with submitted metadata (which can be partial) - # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' - for metadata_key in posted_metadata.keys(): - - # let's strip out any metadata fields from the postback which have been identified as system metadata - # and therefore should not be user-editable, so we should accept them back from the client - if metadata_key in module.system_metadata_fields: - del posted_metadata[metadata_key] - elif posted_metadata[metadata_key] is None: - # remove both from passed in collection as well as the collection read in from the modulestore - if metadata_key in module.metadata: - del module.metadata[metadata_key] - del posted_metadata[metadata_key] - - # overlay the new metadata over the modulestore sourced collection to support partial updates - module.metadata.update(posted_metadata) - - # commit to datastore - store.update_metadata(location, module.metadata) + # cdodge: also commit any metadata which might have been passed along in the + # POST from the client, if it is there + # NOTE, that the postback is not the complete metadata, as there's system metadata which is + # not presented to the end-user for editing. So let's fetch the original and + # 'apply' the submitted metadata, so we don't end up deleting system metadata + if post_data.get('metadata') is not None: + posted_metadata = post_data['metadata'] + + # update existing metadata with submitted metadata (which can be partial) + # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' + for metadata_key in posted_metadata.keys(): + + # let's strip out any metadata fields from the postback which have been identified as system metadata + # and therefore should not be user-editable, so we should accept them back from the client + if metadata_key in module.system_metadata_fields: + del posted_metadata[metadata_key] + elif posted_metadata[metadata_key] is None: + # remove both from passed in collection as well as the collection read in from the modulestore + if metadata_key in module.metadata: + del module.metadata[metadata_key] + del posted_metadata[metadata_key] + + # overlay the new metadata over the modulestore sourced collection to support partial updates + module.metadata.update(posted_metadata) + + # commit to datastore + store.update_metadata(location, module.metadata) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 72ae3821cc..b79d86b52f 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1,7 +1,7 @@ import json import shutil from django.test.client import Client -from override_settings import override_settings +from django.test.utils import override_settings from django.conf import settings from django.core.urlresolvers import reverse from path import path @@ -10,6 +10,7 @@ import json from fs.osfs import OSFS import copy from mock import Mock +from json import dumps, loads from student.models import Registration from django.contrib.auth.models import User @@ -26,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') @@ -207,6 +210,24 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): # check for custom_tags self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template') + # check for graiding_policy.json + 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()) + 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) @@ -321,7 +342,7 @@ class ContentStoreTest(ModuleStoreTestCase): # Create a course so there is something to view resp = self.client.get(reverse('index')) self.assertContains(resp, - '

My Courses

', + '

My Courses

', status_code=200, html=True) @@ -357,7 +378,7 @@ class ContentStoreTest(ModuleStoreTestCase): resp = self.client.get(reverse('course_index', kwargs=data)) self.assertContains(resp, - 'Robot Super Course', + '
', status_code=200, html=True) @@ -380,11 +401,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) @@ -399,3 +420,32 @@ class ContentStoreTest(ModuleStoreTestCase): self.assertIn('markdown', context, "markdown is missing from context") self.assertIn('markdown', problem.metadata, "markdown is missing from metadata") self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields") + + +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) + + diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 84e79b9670..86503d2136 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -143,10 +143,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, '
  • Course Details
  • ', 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'}) @@ -249,7 +245,7 @@ class CourseGradingTest(CourseTestCase): altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D") - test_grader.grace_period = {'hours' : '4'} + test_grader.grace_period = {'hours' : 4, 'minutes' : 5, 'seconds': 0} altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period") diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 9af5b09276..166982e35f 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -1,7 +1,6 @@ import json import shutil from django.test.client import Client -from override_settings import override_settings from django.conf import settings from django.core.urlresolvers import reverse from path import path @@ -86,7 +85,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""" diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 4e3510463f..be028b2836 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -2,7 +2,6 @@ import json import copy from time import time from django.test import TestCase -from override_settings import override_settings from django.conf import settings from student.models import Registration diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 137e71b24a..6d5905afe7 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -59,6 +59,7 @@ from cms.djangoapps.models.settings.course_details import CourseDetails,\ from cms.djangoapps.models.settings.course_grading import CourseGradingModel from cms.djangoapps.contentstore.utils import get_modulestore from lxml import etree +from django.shortcuts import redirect # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' @@ -81,6 +82,11 @@ def signup(request): csrf_token = csrf(request)['csrf_token'] return render_to_response('signup.html', {'csrf': csrf_token}) +def old_login_redirect(request): + ''' + Redirect to the active login url. + ''' + return redirect('login', permanent=True) @ssl_login_shortcut @ensure_csrf_cookie @@ -94,6 +100,11 @@ def login_page(request): 'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE), }) +def howitworks(request): + if request.user.is_authenticated(): + return index(request) + else: + return render_to_response('howitworks.html', {}) # ==== Views for any logged-in user ================================== @@ -120,9 +131,11 @@ def index(request): reverse('course_index', args=[ course.location.org, course.location.course, - course.location.name])) + course.location.name]), + get_lms_link_for_item(course.location)) for course in courses], - 'user': request.user + 'user': request.user, + 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff }) @@ -160,6 +173,8 @@ def course_index(request, org, course, name): if not has_access(request.user, location): raise PermissionDenied() + lms_link = get_lms_link_for_item(location) + upload_asset_callback_url = reverse('upload_asset', kwargs={ 'org': org, 'course': course, @@ -172,6 +187,7 @@ def course_index(request, org, course, name): return render_to_response('overview.html', { 'active_tab': 'courseware', 'context_course': course, + 'lms_link': lms_link, 'sections': sections, 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), 'parent_location': course.location, @@ -272,7 +288,7 @@ def edit_unit(request, location): template.display_name, template.location.url(), 'markdown' in template.metadata, - template.location.name == 'Empty' + 'empty' in template.metadata )) components = [ @@ -729,8 +745,6 @@ def clone_item(request): #@login_required #@ensure_csrf_cookie - - def upload_asset(request, org, course, coursename): ''' cdodge: this method allows for POST uploading of files into the course asset library, which will @@ -795,8 +809,6 @@ def upload_asset(request, org, course, coursename): ''' This view will return all CMS users who are editors for the specified course ''' - - @login_required @ensure_csrf_cookie def manage_users(request, location): @@ -818,7 +830,7 @@ def manage_users(request, location): }) -def create_json_response(errmsg=None): +def create_json_response(errmsg = None): if errmsg is not None: resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg})) else: @@ -830,8 +842,6 @@ def create_json_response(errmsg=None): This POST-back view will add a user - specified by email - to the list of editors for the specified course ''' - - @expect_json @login_required @ensure_csrf_cookie @@ -864,8 +874,6 @@ def add_user(request, location): This POST-back view will remove a user - specified by email - from the list of editors for the specified course ''' - - @expect_json @login_required @ensure_csrf_cookie @@ -1123,8 +1131,31 @@ def get_course_settings(request, org, course, name): course_details = CourseDetails.fetch(location) return render_to_response('settings.html', { - 'active_tab': 'settings', 'context_course': course_module, + 'course_location' : location, + 'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder) + }) + +@login_required +@ensure_csrf_cookie +def course_config_graders_page(request, org, course, name): + """ + Send models and views as well as html for editing the course settings to the client. + + org, course, name: Attributes of the Location for the item to edit + """ + location = ['i4x', org, course, 'course', name] + + # check that logged in user has permissions to this item + if not has_access(request.user, location): + raise PermissionDenied() + + course_module = modulestore().get_item(location) + course_details = CourseGradingModel.fetch(location) + + return render_to_response('settings_graders.html', { + 'context_course': course_module, + 'course_location' : location, 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder) }) @@ -1259,6 +1290,10 @@ def edge(request): @login_required @expect_json def create_new_course(request): + + if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff: + raise PermissionDenied() + # This logic is repeated in xmodule/modulestore/tests/factories.py # so if you change anything here, you need to also change it there. # TODO: write a test that creates two courses, one with the factory and diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index f4c6fd3d7c..3d0b8f78af 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -155,7 +155,8 @@ class CourseGradingModel(object): if 'grace_period' in graceperiodjson: graceperiodjson = graceperiodjson['grace_period'] - grace_rep = " ".join(["%s %s" % (value, key) for (key, value) in graceperiodjson.iteritems()]) + # lms requires these to be in a fixed order + grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson) descriptor = get_modulestore(course_location).get_item(course_location) descriptor.metadata['graceperiod'] = grace_rep @@ -234,10 +235,10 @@ class CourseGradingModel(object): @staticmethod def convert_set_grace_period(descriptor): - # 5 hours 59 minutes 59 seconds => converted to iso format + # 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59} rawgrace = descriptor.metadata.get('graceperiod', None) if rawgrace: - parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)} + parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)} return parsedgrace else: return None diff --git a/cms/envs/common.py b/cms/envs/common.py index ef7a4f43fa..281dd97f20 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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 = ( @@ -165,13 +165,6 @@ STATICFILES_DIRS = [ # This is how you would use the textbook images locally # ("book", ENV_ROOT / "book_images") ] -if os.path.isdir(GITHUB_REPO_ROOT): - STATICFILES_DIRS += [ - # TODO (cpennington): When courses aren't loaded from github, remove this - (course_dir, GITHUB_REPO_ROOT / course_dir) - for course_dir in os.listdir(GITHUB_REPO_ROOT) - if os.path.isdir(GITHUB_REPO_ROOT / course_dir) - ] # Locale/Internationalization TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name diff --git a/cms/static/client_templates/course_grade_policy.html b/cms/static/client_templates/course_grade_policy.html index c9a21280dd..db129614f6 100644 --- a/cms/static/client_templates/course_grade_policy.html +++ b/cms/static/client_templates/course_grade_policy.html @@ -1,69 +1,37 @@ -
  • -
    - +
  • +
    + + + e.g. Homework, Midterm Exams +
    -
    -
    - - e.g. Homework, Labs, Midterm Exams, Final Exam -
    -
    - - -
    - - -
    -
    - - e.g. HW, Midterm, Final -
    -
    -
    - -
    - - -
    -
    - - e.g. 25% -
    -
    -
    - -
    - - -
    -
    - - total exercises assigned -
    -
    -
    - -
    - - -
    -
    - - total exercises that won't be graded -
    -
    -
    - Delete +
    + + + e.g. HW, Midterm +
    + +
    + + + e.g. 25% +
    + +
    + + + total exercises assigned +
    + +
    + + + total exercises that won't be graded +
    + +
    + Delete +
  • diff --git a/cms/static/coffee/src/views/tabs.coffee b/cms/static/coffee/src/views/tabs.coffee index 5a826c1794..9fbe4e5789 100644 --- a/cms/static/coffee/src/views/tabs.coffee +++ b/cms/static/coffee/src/views/tabs.coffee @@ -1,6 +1,4 @@ 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 diff --git a/cms/static/img/hiw-feature1.png b/cms/static/img/hiw-feature1.png new file mode 100644 index 0000000000..3cfd48d066 Binary files /dev/null and b/cms/static/img/hiw-feature1.png differ diff --git a/cms/static/img/hiw-feature2.png b/cms/static/img/hiw-feature2.png new file mode 100644 index 0000000000..9442325dd5 Binary files /dev/null and b/cms/static/img/hiw-feature2.png differ diff --git a/cms/static/img/hiw-feature3.png b/cms/static/img/hiw-feature3.png new file mode 100644 index 0000000000..fa6b81ae89 Binary files /dev/null and b/cms/static/img/hiw-feature3.png differ diff --git a/cms/static/img/html-icon.png b/cms/static/img/html-icon.png index e739f2fc11..8f576178b2 100644 Binary files a/cms/static/img/html-icon.png and b/cms/static/img/html-icon.png differ diff --git a/cms/static/img/large-discussion-icon.png b/cms/static/img/large-discussion-icon.png index 2f0bfea98f..cebf332769 100644 Binary files a/cms/static/img/large-discussion-icon.png and b/cms/static/img/large-discussion-icon.png differ diff --git a/cms/static/img/large-freeform-icon.png b/cms/static/img/large-freeform-icon.png index b1d195a7ca..0d5e454f58 100644 Binary files a/cms/static/img/large-freeform-icon.png and b/cms/static/img/large-freeform-icon.png differ diff --git a/cms/static/img/large-problem-icon.png b/cms/static/img/large-problem-icon.png index b962d42b14..a30ab8eac8 100644 Binary files a/cms/static/img/large-problem-icon.png and b/cms/static/img/large-problem-icon.png differ diff --git a/cms/static/img/large-video-icon.png b/cms/static/img/large-video-icon.png index 392851324c..f1ab048b4c 100644 Binary files a/cms/static/img/large-video-icon.png and b/cms/static/img/large-video-icon.png differ diff --git a/cms/static/img/logo-edx-studio-white.png b/cms/static/img/logo-edx-studio-white.png new file mode 100644 index 0000000000..3e3ee63622 Binary files /dev/null and b/cms/static/img/logo-edx-studio-white.png differ diff --git a/cms/static/img/logo-edx-studio.png b/cms/static/img/logo-edx-studio.png new file mode 100644 index 0000000000..006194a195 Binary files /dev/null and b/cms/static/img/logo-edx-studio.png differ diff --git a/cms/static/img/pl-1x1-000.png b/cms/static/img/pl-1x1-000.png new file mode 100644 index 0000000000..b94b7a9746 Binary files /dev/null and b/cms/static/img/pl-1x1-000.png differ diff --git a/cms/static/img/pl-1x1-fff.png b/cms/static/img/pl-1x1-fff.png new file mode 100644 index 0000000000..7081c75d36 Binary files /dev/null and b/cms/static/img/pl-1x1-fff.png differ diff --git a/cms/static/img/preview-lms-staticpages.png b/cms/static/img/preview-lms-staticpages.png new file mode 100644 index 0000000000..05a62f7c7f Binary files /dev/null and b/cms/static/img/preview-lms-staticpages.png differ diff --git a/cms/static/img/thumb-hiw-feature1.png b/cms/static/img/thumb-hiw-feature1.png new file mode 100644 index 0000000000..b2dc0c00ee Binary files /dev/null and b/cms/static/img/thumb-hiw-feature1.png differ diff --git a/cms/static/img/thumb-hiw-feature2.png b/cms/static/img/thumb-hiw-feature2.png new file mode 100644 index 0000000000..e96bcad1aa Binary files /dev/null and b/cms/static/img/thumb-hiw-feature2.png differ diff --git a/cms/static/img/thumb-hiw-feature3.png b/cms/static/img/thumb-hiw-feature3.png new file mode 100644 index 0000000000..f694fca516 Binary files /dev/null and b/cms/static/img/thumb-hiw-feature3.png differ diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 7e55d2b8d8..d8b32cb0e8 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -5,7 +5,7 @@ var $newComponentItem; var $changedInput; var $spinner; -$(document).ready(function() { +$(document).ready(function () { $body = $('body'); $modal = $('.history-modal'); $modalCover = $(' diff --git a/cms/templates/activation_complete.html b/cms/templates/activation_complete.html index 5d9437ccb3..1e195a632c 100644 --- a/cms/templates/activation_complete.html +++ b/cms/templates/activation_complete.html @@ -5,7 +5,7 @@

    Activation Complete!

    -

    Thanks for activating your account. Log in here.

    +

    Thanks for activating your account. Log in here.

    diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html index 01766e2dac..5ace98df56 100644 --- a/cms/templates/asset_index.html +++ b/cms/templates/asset_index.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> -<%block name="bodyclass">assets -<%block name="title">Courseware Assets +<%block name="bodyclass">is-signedin course uploads +<%block name="title">Uploads & Files <%namespace name='static' file='static_content.html'/> @@ -33,12 +33,27 @@ +
    +
    +
    + Course Content +

    Files & Uploads

    +
    + + +
    +
    +
    diff --git a/cms/templates/base.html b/cms/templates/base.html index 84f10fc2d1..498897bd11 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -5,23 +5,29 @@ + + <%block name="title"></%block> | + % if context_course: + <% ctx_loc = context_course.location %> + ${context_course.display_name} | + % endif + edX Studio + + + + <%static:css group='base-style'/> - + - <%block name="title"></%block> - - - - <%block name="header_extras"> - <%include file="widgets/header.html" args="active_tab=active_tab"/> + <%include file="widgets/header.html" /> <%include file="courseware_vendor_js.html"/> @@ -47,9 +53,9 @@ <%block name="content"> + <%include file="widgets/footer.html" /> <%block name="jsextra"> - diff --git a/cms/templates/course_index.html b/cms/templates/course_index.html index e490ad7817..5c8772c1ed 100644 --- a/cms/templates/course_index.html +++ b/cms/templates/course_index.html @@ -1,5 +1,5 @@ <%inherit file="base.html" /> -<%block name="title">Course Manager + <%include file="widgets/header.html"/> <%block name="content"> diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index 83d829efa0..a68a0da76a 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -2,8 +2,9 @@ <%namespace name='static' file='static_content.html'/> -<%block name="title">Course Info -<%block name="bodyclass">course-info +<%block name="title">Updates +<%block name="bodyclass">is-signedin course course-info updates + <%block name="jsextra"> @@ -41,16 +42,38 @@ <%block name="content"> +
    +
    +
    + Course Content +

    Course Updates

    +
    + + +
    +
    + +
    +
    +
    +

    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.

    +
    +
    +
    +
    -

    Course Info

    diff --git a/cms/templates/edit-static-page.html b/cms/templates/edit-static-page.html index 02fe2308fa..f1b2374b46 100644 --- a/cms/templates/edit-static-page.html +++ b/cms/templates/edit-static-page.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> -<%block name="title">Edit Static Page -<%block name="bodyclass">edit-static-page +<%block name="title">Editing Static Page +<%block name="bodyclass">is-signedin course pages edit-static-page <%block name="content">
    diff --git a/cms/templates/edit-tabs.html b/cms/templates/edit-tabs.html index c6ffb14124..1a44de60c1 100644 --- a/cms/templates/edit-tabs.html +++ b/cms/templates/edit-tabs.html @@ -1,7 +1,7 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> -<%block name="title">Tabs -<%block name="bodyclass">static-pages +<%block name="title">Static Pages +<%block name="bodyclass">is-signedin course pages static-pages <%block name="jsextra"> <%block name="content"> +
    +
    +
    + Course Content +

    Static Pages

    +
    + + +
    +
    + +
    +
    + +
    +
    +
    -
    -

    Here you can add and manage additional pages for your course

    -

    These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.

    -
    - -
      @@ -43,4 +67,17 @@
    + +
    +

    How Static Pages are Used in Your Course

    +
    + Preview of how Static Pages are used in your course +
    These pages will be presented in your course's main navigation alongside Courseware, Course Info, Discussion, etc.
    +
    + + + + close modal + +
    \ No newline at end of file diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index de5e14e0a9..00780eab3b 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -7,8 +7,9 @@ %> <%! from django.core.urlresolvers import reverse %> -<%block name="bodyclass">subsection <%block name="title">CMS Subsection +<%block name="bodyclass">is-signedin course subsection + <%namespace name="units" file="widgets/units.html" /> <%namespace name='static' file='static_content.html'/> @@ -97,6 +98,7 @@
    +
    <%block name="jsextra"> @@ -107,6 +109,8 @@ + + - - + \ No newline at end of file diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 36930f5386..722e756203 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -1,17 +1,31 @@ <%inherit file="base.html" /> <%block name="title">Course Staff Manager -<%block name="bodyclass">users +<%block name="bodyclass">is-signedin course users settings team + <%block name="content"> +
    +
    +
    + Course Settings +

    Course Team

    +
    + + +
    +
    +
    -
    - %if allow_actions: - - New User - - %endif -

    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.

    @@ -97,7 +111,7 @@ $cancelButton.bind('click', hideNewUserForm); $('.new-user-button').bind('click', showNewUserForm); - $body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel); + $('body').bind('keyup', { $cancelButton: $cancelButton }, checkForCancel); $('.remove-user').click(function() { $.ajax({ diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 20ddcead01..91a1107726 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -6,7 +6,8 @@ from datetime import datetime %> <%! from django.core.urlresolvers import reverse %> -<%block name="title">CMS Courseware Overview +<%block name="title">Course Outline +<%block name="bodyclass">is-signedin course outline <%namespace name='static' file='static_content.html'/> <%namespace name="units" file="widgets/units.html" /> @@ -119,12 +120,32 @@
    +
    +
    +
    + Course Content +

    Course Outline

    +
    + + +
    +
    +
    -
    % for section in sections:
    diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 8cd4246da9..32d24b77e6 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -1,6 +1,6 @@ <%inherit file="base.html" /> -<%block name="bodyclass">settings -<%block name="title">Settings +<%block name="title">Schedule & Details +<%block name="bodyclass">is-signedin course schedule settings <%namespace name='static' file='static_content.html'/> <%! @@ -15,24 +15,24 @@ from contentstore import utils - - - - - + + + + + + + + + + + +<%block name="content"> + +
    +
    +

    Settings

    +
    +
    + +
    +

    Faculty

    + +
    +
    +

    Faculty Members

    + Individuals instructing and help with this course +
    + +
    +
    +
      +
    • +
      + +
      + +
      +
      + +
      + +
      + +
      +
      + +
      + + +
      + +
      + +
      + + A brief description of your education, experience, and expertise +
      +
      + + Delete Faculty Member +
    • + +
    • +
      + +
      + +
      +
      + +
      + +
      + +
      +
      + +
      + +
      +
      + + Upload Faculty Photo + + Max size: 30KB +
      +
      +
      + +
      + +
      +
      + + A brief description of your education, experience, and expertise +
      +
      +
      +
    • +
    + + + New Faculty Member + +
    +
    +
    + +
    + +
    +

    Problems

    + +
    +
    +

    General Settings

    + Course-wide settings for all problems +
    + +
    +

    Problem Randomization:

    + +
    +
    + + +
    + + randomize all problems +
    +
    + +
    + + +
    + + do not randomize problems +
    +
    + +
    + + +
    + + randomize problems per student +
    +
    +
    +
    + +
    +

    Show Answers:

    + +
    +
    + + +
    + + Answers will be shown after the number of attempts has been met +
    +
    + +
    + + +
    + + Answers will never be shown, regardless of attempts +
    +
    +
    +
    + +
    + + +
    +
    + + Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0" +
    +
    +
    +
    + +
    +
    +

    [Assignment Type Name]

    +
    + +
    +

    Problem Randomization:

    + +
    +
    + + +
    + + randomize all problems +
    +
    + +
    + + +
    + + do not randomize problems +
    +
    + +
    + + +
    + + randomize problems per student +
    +
    +
    +
    + +
    +

    Show Answers:

    + +
    +
    + + +
    + + Answers will be shown after the number of attempts has been met +
    +
    + +
    + + +
    + + Answers will never be shown, regardless of attempts +
    +
    +
    +
    + +
    + + +
    +
    + + Students will this have this number of chances to answer a problem. To set infinite atttempts, use "0" +
    +
    +
    +
    +
    + +
    +

    Discussions

    + +
    +
    +

    General Settings

    + Course-wide settings for online discussion +
    + +
    +

    Anonymous Discussions:

    + +
    +
    + + +
    + + Students and faculty will be able to post anonymously +
    +
    + +
    + + +
    + + Posting anonymously is not allowed. Any previous anonymous posts will be reverted to non-anonymous +
    +
    +
    +
    + +
    +

    Anonymous Discussions:

    + +
    +
    + + +
    + + Students and faculty will be able to post anonymously +
    +
    + +
    + + +
    + + This option is disabled since there are previous discussions that are anonymous. +
    +
    +
    +
    + +
    +

    Discussion Categories

    + +
    + + + + New Discussion Category + +
    +
    +
    +
    +
    +
    +
    +
    +
    + diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html new file mode 100644 index 0000000000..61cb59e995 --- /dev/null +++ b/cms/templates/settings_graders.html @@ -0,0 +1,151 @@ +<%inherit file="base.html" /> +<%block name="title">Grading +<%block name="bodyclass">is-signedin course grading settings + +<%namespace name='static' file='static_content.html'/> +<%! +from contentstore import utils +%> + +<%block name="jsextra"> + + + + + + + + + + + + + +<%block name="content"> +
    +
    +
    + Settings +

    Grading

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +

    Overall Grade Range

    + Your overall grading scale for student final grades +
    + +
      +
    1. +
      + +
      +
      +
        +
      1. 0
      2. +
      3. 10
      4. +
      5. 20
      6. +
      7. 30
      8. +
      9. 40
      10. +
      11. 50
      12. +
      13. 60
      14. +
      15. 70
      16. +
      17. 80
      18. +
      19. 90
      20. +
      21. 100
      22. +
      +
        +
      +
      +
      +
      +
    2. +
    +
    + +
    + +
    +
    +

    Grading Rules & Policies

    + Deadlines, requirements, and logistics around grading student work +
    + +
      +
    1. + + + Leeway on due dates +
    2. +
    +
    + +
    + +
    +
    +

    Assignment Types

    + Categories and labels for any exercises that are gradable +
    + +
      + +
    + + +
    +
    +
    + + +
    +
    + diff --git a/cms/templates/signup.html b/cms/templates/signup.html index 2c60b758e6..30c5c1cf2b 100644 --- a/cms/templates/signup.html +++ b/cms/templates/signup.html @@ -1,94 +1,141 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> -<%block name="title">Sign up -<%block name="bodyclass">no-header +<%block name="title">Sign Up +<%block name="bodyclass">not-signedin signup <%block name="content"> -
    +
    +
    +
    +

    Sign Up for edX Studio

    + +
    - +
    +

    I've never authored a course online before. Is there help?

    +

    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.

    +
    + +
    +
    + - + ); + }); + })(this) + \ No newline at end of file diff --git a/cms/templates/unit.html b/cms/templates/unit.html index f3a779604e..c529f5863a 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -1,8 +1,9 @@ <%inherit file="base.html" /> <%! from django.core.urlresolvers import reverse %> <%namespace name="units" file="widgets/units.html" /> -<%block name="bodyclass">unit -<%block name="title">CMS Unit +<%block name="title">Individual Unit +<%block name="bodyclass">is-signedin course unit + <%block name="jsextra"> @@ -56,38 +65,66 @@
    % for type, templates in sorted(component_templates.items()):
    -

    Select ${type} component type:

    - - + % if type == "problem": +
    + + % endif +
    +
      + % for name, location, has_markdown, is_empty in templates: + % if has_markdown or type != "problem": + % if is_empty: +
    • + + ${name} + +
    • + + % else: +
    • + + ${name} + +
    • + % endif + % endif + + %endfor +
    +
    + % if type == "problem": +
    +
      + % for name, location, has_markdown, is_empty in templates: + % if not has_markdown: + % if is_empty: +
    • + + ${name} + +
    • + + % else: +
    • + + ${name} + + +
    • + % endif + % endif + % endfor +
    +
    +
    + % endif Cancel
    % endfor diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html new file mode 100644 index 0000000000..0f265dfc2c --- /dev/null +++ b/cms/templates/widgets/footer.html @@ -0,0 +1,30 @@ +<%! from django.core.urlresolvers import reverse %> + + \ No newline at end of file diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 5f41452339..7b516ececd 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -1,40 +1,117 @@ <%! from django.core.urlresolvers import reverse %> -<% active_tab_class = 'active-tab-' + active_tab if active_tab else '' %> -
    -
    -
    -
    - % if context_course: - <% ctx_loc = context_course.location %> - › - ${context_course.display_name} › - % endif -
    +
    + + +
    + % if user.is_authenticated(): + + % else: + + % endif +
    +
    +
    \ No newline at end of file diff --git a/cms/templates/widgets/problem-edit.html b/cms/templates/widgets/problem-edit.html index 4ff9d299ab..8ca07a7928 100644 --- a/cms/templates/widgets/problem-edit.html +++ b/cms/templates/widgets/problem-edit.html @@ -1,20 +1,20 @@ <%include file="metadata-edit.html" />
    - %if markdown != '' or data == '\n\n': + %if enable_markdown:
    • -
    • -
    • -
    • -
    • @@ -56,7 +56,7 @@
    -
    Check Multiple
    +
    Checkboxes
    @@ -67,7 +67,7 @@
    -
    String Response
    +
    Text Input
    @@ -76,7 +76,7 @@
    -
    Numerical Response
    +
    Numerical Input
    @@ -85,7 +85,7 @@
    -
    Option Response
    +
    Dropdown
    diff --git a/cms/urls.py b/cms/urls.py index ad4dd87d74..35b2707241 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -6,7 +6,8 @@ from django.conf.urls import patterns, include, url # admin.autodiscover() urlpatterns = ('', - url(r'^$', 'contentstore.views.index', name='index'), + url(r'^$', 'contentstore.views.howitworks', name='homepage'), + url(r'^listing', 'contentstore.views.index', name='index'), url(r'^edit/(?P.*?)$', 'contentstore.views.edit_unit', name='edit_unit'), url(r'^subsection/(?P.*?)$', 'contentstore.views.edit_subsection', name='edit_subsection'), url(r'^preview_component/(?P.*?)$', 'contentstore.views.preview_component', name='preview_component'), @@ -42,9 +43,10 @@ urlpatterns = ('', 'contentstore.views.remove_user', name='remove_user'), url(r'^(?P[^/]+)/(?P[^/]+)/info/(?P[^/]+)$', 'contentstore.views.course_info', name='course_info'), url(r'^(?P[^/]+)/(?P[^/]+)/course_info/updates/(?P.*)$', 'contentstore.views.course_info_updates', name='course_info'), - url(r'^(?P[^/]+)/(?P[^/]+)/settings/(?P[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'), - url(r'^(?P[^/]+)/(?P[^/]+)/settings/(?P[^/]+)/section/(?P
    [^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'), - url(r'^(?P[^/]+)/(?P[^/]+)/grades/(?P[^/]+)/(?P.*)$', 'contentstore.views.course_grader_updates', name='course_settings'), + url(r'^(?P[^/]+)/(?P[^/]+)/settings-details/(?P[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'), + url(r'^(?P[^/]+)/(?P[^/]+)/settings-grading/(?P[^/]+)$', 'contentstore.views.course_config_graders_page', name='course_settings'), + url(r'^(?P[^/]+)/(?P[^/]+)/settings-details/(?P[^/]+)/section/(?P
    [^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'), + url(r'^(?P[^/]+)/(?P[^/]+)/settings-grading/(?P[^/]+)/(?P.*)$', 'contentstore.views.course_grader_updates', name='course_settings'), url(r'^(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/gradeas.*$', 'contentstore.views.assignment_type_update', name='assignment_type_update'), @@ -76,13 +78,15 @@ urlpatterns = ('', # User creation and updating views urlpatterns += ( + url(r'^howitworks$', 'contentstore.views.howitworks', name='howitworks'), url(r'^signup$', 'contentstore.views.signup', name='signup'), url(r'^create_account$', 'student.views.create_account'), url(r'^activate/(?P[^/]*)$', 'student.views.activate_account', name='activate'), # form page - url(r'^login$', 'contentstore.views.login_page', name='login'), + url(r'^login$', 'contentstore.views.old_login_redirect', name='old_login'), + url(r'^signin$', 'contentstore.views.login_page', name='login'), # ajax view that actually does the work url(r'^login_post$', 'student.views.login_user', name='login_post'), diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py index 0fbf863fee..b3ad928b39 100644 --- a/common/djangoapps/course_groups/tests/tests.py +++ b/common/djangoapps/course_groups/tests/tests.py @@ -2,7 +2,7 @@ import django.test from django.contrib.auth.models import User from django.conf import settings -from override_settings import override_settings +from django.test.utils import override_settings from course_groups.models import CourseUserGroup from course_groups.cohorts import (get_cohort, get_course_cohorts, diff --git a/common/djangoapps/static_replace/__init__.py b/common/djangoapps/static_replace/__init__.py index bf27f5b38d..fb1f48d143 100644 --- a/common/djangoapps/static_replace/__init__.py +++ b/common/djangoapps/static_replace/__init__.py @@ -13,12 +13,18 @@ log = logging.getLogger(__name__) def _url_replace_regex(prefix): + """ + Match static urls in quotes that don't end in '?raw'. + + To anyone contemplating making this more complicated: + http://xkcd.com/1171/ + """ return r""" - (?x) # flags=re.VERBOSE - (?P\\?['"]) # the opening quotes - (?P{prefix}) # theeprefix - (?P.*?) # everything else in the url - (?P=quote) # the first matching closing quote + (?x) # flags=re.VERBOSE + (?P\\?['"]) # the opening quotes + (?P{prefix}) # the prefix + (?P.*?) # everything else in the url + (?P=quote) # the first matching closing quote """.format(prefix=prefix) @@ -74,12 +80,20 @@ def replace_static_urls(text, data_directory, course_namespace=None): quote = match.group('quote') rest = match.group('rest') + # Don't mess with things that end in '?raw' + if rest.endswith('?raw'): + return original + # course_namespace is not None, then use studio style urls if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): url = StaticContent.convert_legacy_static_url(rest, course_namespace) + # In debug mode, if we can find the url as is, + elif settings.DEBUG and finders.find(rest, True): + return original # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed else: course_path = "/".join((data_directory, rest)) + try: if staticfiles_storage.exists(rest): url = staticfiles_storage.url(rest) diff --git a/common/djangoapps/static_replace/test/test_static_replace.py b/common/djangoapps/static_replace/test/test_static_replace.py index 98c29ca2f9..f23610e1bd 100644 --- a/common/djangoapps/static_replace/test/test_static_replace.py +++ b/common/djangoapps/static_replace/test/test_static_replace.py @@ -1,5 +1,8 @@ -from nose.tools import assert_equals -from static_replace import replace_static_urls, replace_course_urls +import re + +from nose.tools import assert_equals, assert_true, assert_false +from static_replace import (replace_static_urls, replace_course_urls, + _url_replace_regex) from mock import patch, Mock from xmodule.modulestore import Location from xmodule.modulestore.mongo import MongoModuleStore @@ -75,3 +78,34 @@ def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings): mock_storage.exists.return_value = False assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY)) + + +def test_raw_static_check(): + """ + Make sure replace_static_urls leaves alone things that end in '.raw' + """ + path = '"/static/foo.png?raw"' + assert_equals(path, replace_static_urls(path, DATA_DIRECTORY)) + + text = 'text
    diff --git a/common/lib/capa/capa/templates/designprotein2dinput.html b/common/lib/capa/capa/templates/designprotein2dinput.html index ff845f8713..6733566ab9 100644 --- a/common/lib/capa/capa/templates/designprotein2dinput.html +++ b/common/lib/capa/capa/templates/designprotein2dinput.html @@ -1,5 +1,5 @@
    -
    +
    % if status == 'unsubmitted': diff --git a/common/lib/capa/capa/tests/test_files/js/.gitignore b/common/lib/capa/capa/tests/test_files/js/.gitignore new file mode 100644 index 0000000000..d2910668f2 --- /dev/null +++ b/common/lib/capa/capa/tests/test_files/js/.gitignore @@ -0,0 +1,4 @@ +test_problem_display.js +test_problem_generator.js +test_problem_grader.js +xproblem.js \ No newline at end of file diff --git a/common/lib/capa/capa/tests/test_files/js/test_problem_display.js b/common/lib/capa/capa/tests/test_files/js/test_problem_display.js deleted file mode 100644 index b61569acea..0000000000 --- a/common/lib/capa/capa/tests/test_files/js/test_problem_display.js +++ /dev/null @@ -1,49 +0,0 @@ -// Generated by CoffeeScript 1.4.0 -(function() { - var MinimaxProblemDisplay, root, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - MinimaxProblemDisplay = (function(_super) { - - __extends(MinimaxProblemDisplay, _super); - - function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) { - this.state = state; - this.submission = submission; - this.evaluation = evaluation; - this.container = container; - this.submissionField = submissionField; - this.parameters = parameters != null ? parameters : {}; - MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters); - } - - MinimaxProblemDisplay.prototype.render = function() {}; - - MinimaxProblemDisplay.prototype.createSubmission = function() { - var id, value, _ref, _results; - this.newSubmission = {}; - if (this.submission != null) { - _ref = this.submission; - _results = []; - for (id in _ref) { - value = _ref[id]; - _results.push(this.newSubmission[id] = value); - } - return _results; - } - }; - - MinimaxProblemDisplay.prototype.getCurrentSubmission = function() { - return this.newSubmission; - }; - - return MinimaxProblemDisplay; - - })(XProblemDisplay); - - root = typeof exports !== "undefined" && exports !== null ? exports : this; - - root.TestProblemDisplay = TestProblemDisplay; - -}).call(this); diff --git a/common/lib/capa/capa/tests/test_files/js/test_problem_generator.js b/common/lib/capa/capa/tests/test_files/js/test_problem_generator.js deleted file mode 100644 index 4b1d133723..0000000000 --- a/common/lib/capa/capa/tests/test_files/js/test_problem_generator.js +++ /dev/null @@ -1,29 +0,0 @@ -// Generated by CoffeeScript 1.4.0 -(function() { - var TestProblemGenerator, root, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - TestProblemGenerator = (function(_super) { - - __extends(TestProblemGenerator, _super); - - function TestProblemGenerator(seed, parameters) { - this.parameters = parameters != null ? parameters : {}; - TestProblemGenerator.__super__.constructor.call(this, seed, this.parameters); - } - - TestProblemGenerator.prototype.generate = function() { - this.problemState.value = this.parameters.value; - return this.problemState; - }; - - return TestProblemGenerator; - - })(XProblemGenerator); - - root = typeof exports !== "undefined" && exports !== null ? exports : this; - - root.generatorClass = TestProblemGenerator; - -}).call(this); diff --git a/common/lib/capa/capa/tests/test_files/js/test_problem_grader.js b/common/lib/capa/capa/tests/test_files/js/test_problem_grader.js deleted file mode 100644 index 80d7ad1690..0000000000 --- a/common/lib/capa/capa/tests/test_files/js/test_problem_grader.js +++ /dev/null @@ -1,50 +0,0 @@ -// Generated by CoffeeScript 1.4.0 -(function() { - var TestProblemGrader, root, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - TestProblemGrader = (function(_super) { - - __extends(TestProblemGrader, _super); - - function TestProblemGrader(submission, problemState, parameters) { - this.submission = submission; - this.problemState = problemState; - this.parameters = parameters != null ? parameters : {}; - TestProblemGrader.__super__.constructor.call(this, this.submission, this.problemState, this.parameters); - } - - TestProblemGrader.prototype.solve = function() { - return this.solution = { - 0: this.problemState.value - }; - }; - - TestProblemGrader.prototype.grade = function() { - var allCorrect, id, value, valueCorrect, _ref; - if (!(this.solution != null)) { - this.solve(); - } - allCorrect = true; - _ref = this.solution; - for (id in _ref) { - value = _ref[id]; - valueCorrect = this.submission != null ? value === this.submission[id] : false; - this.evaluation[id] = valueCorrect; - if (!valueCorrect) { - allCorrect = false; - } - } - return allCorrect; - }; - - return TestProblemGrader; - - })(XProblemGrader); - - root = typeof exports !== "undefined" && exports !== null ? exports : this; - - root.graderClass = TestProblemGrader; - -}).call(this); diff --git a/common/lib/capa/capa/tests/test_files/js/xproblem.js b/common/lib/capa/capa/tests/test_files/js/xproblem.js deleted file mode 100644 index 55a469f7c1..0000000000 --- a/common/lib/capa/capa/tests/test_files/js/xproblem.js +++ /dev/null @@ -1,78 +0,0 @@ -// Generated by CoffeeScript 1.4.0 -(function() { - var XProblemDisplay, XProblemGenerator, XProblemGrader, root; - - XProblemGenerator = (function() { - - function XProblemGenerator(seed, parameters) { - this.parameters = parameters != null ? parameters : {}; - this.random = new MersenneTwister(seed); - this.problemState = {}; - } - - XProblemGenerator.prototype.generate = function() { - return console.error("Abstract method called: XProblemGenerator.generate"); - }; - - return XProblemGenerator; - - })(); - - XProblemDisplay = (function() { - - function XProblemDisplay(state, submission, evaluation, container, submissionField, parameters) { - this.state = state; - this.submission = submission; - this.evaluation = evaluation; - this.container = container; - this.submissionField = submissionField; - this.parameters = parameters != null ? parameters : {}; - } - - XProblemDisplay.prototype.render = function() { - return console.error("Abstract method called: XProblemDisplay.render"); - }; - - XProblemDisplay.prototype.updateSubmission = function() { - return this.submissionField.val(JSON.stringify(this.getCurrentSubmission())); - }; - - XProblemDisplay.prototype.getCurrentSubmission = function() { - return console.error("Abstract method called: XProblemDisplay.getCurrentSubmission"); - }; - - return XProblemDisplay; - - })(); - - XProblemGrader = (function() { - - function XProblemGrader(submission, problemState, parameters) { - this.submission = submission; - this.problemState = problemState; - this.parameters = parameters != null ? parameters : {}; - this.solution = null; - this.evaluation = {}; - } - - XProblemGrader.prototype.solve = function() { - return console.error("Abstract method called: XProblemGrader.solve"); - }; - - XProblemGrader.prototype.grade = function() { - return console.error("Abstract method called: XProblemGrader.grade"); - }; - - return XProblemGrader; - - })(); - - root = typeof exports !== "undefined" && exports !== null ? exports : this; - - root.XProblemGenerator = XProblemGenerator; - - root.XProblemDisplay = XProblemDisplay; - - root.XProblemGrader = XProblemGrader; - -}).call(this); diff --git a/common/lib/logsettings.py b/common/lib/logsettings.py index 2ed20a0bad..8fc2bb9db1 100644 --- a/common/lib/logsettings.py +++ b/common/lib/logsettings.py @@ -53,7 +53,7 @@ def get_logger_config(log_dir, logging_env=logging_env, hostname=hostname) - handlers = ['console', 'local', 'null'] if debug else ['console', + handlers = ['console', 'local'] if debug else ['console', 'syslogger-remote', 'local'] logger_config = { @@ -84,12 +84,6 @@ def get_logger_config(log_dir, 'level': 'ERROR', 'class': 'newrelic_logging.NewRelicHandler', 'formatter': 'raw', - }, - 'null' : { - 'level': 'CRITICAL', - 'class': 'logging.handlers.SysLogHandler', - 'address': syslog_addr, - 'formatter': 'syslog_format', } }, 'loggers': { @@ -98,26 +92,11 @@ def get_logger_config(log_dir, 'level': 'DEBUG', 'propagate': False, }, - 'django.db.backends': { - 'handlers': ['null'], - 'propagate': False, - 'level':'DEBUG', - }, - 'django_comment_client.utils' : { - 'handlers': ['null'], - 'propagate': False, - 'level':'DEBUG', - }, - 'pipeline.compilers' : { - 'handlers': ['null'], - 'propagate': False, - 'level':'DEBUG', - }, '': { 'handlers': handlers, 'level': 'DEBUG', 'propagate': False - } + }, } } diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 3bc8bc5143..ec369420cd 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -34,8 +34,10 @@ setup( "section = xmodule.backcompat_module:SemanticSectionDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor", "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor", + "timelimit = xmodule.timelimit_module:TimeLimitDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor", "video = xmodule.video_module:VideoDescriptor", + "videoalpha = xmodule.videoalpha_module:VideoAlphaDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", @@ -43,7 +45,8 @@ setup( "static_tab = xmodule.html_module:StaticTabDescriptor", "custom_tag_template = xmodule.raw_module:RawDescriptor", "about = xmodule.html_module:AboutDescriptor", - "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor" - ] + "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", + "foldit = xmodule.foldit_module:FolditDescriptor", + ] } ) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index d806ec7913..4635cc6871 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -703,15 +703,15 @@ class CapaDescriptor(RawDescriptor): def get_context(self): _context = RawDescriptor.get_context(self) - _context.update({'markdown': self.metadata.get('markdown', '')}) + _context.update({'markdown': self.metadata.get('markdown', ''), + 'enable_markdown' : 'markdown' in self.metadata}) return _context @property def editable_metadata_fields(self): - """Remove metadata from the editable fields since it has its own editor""" - subset = super(CapaDescriptor, self).editable_metadata_fields - if 'markdown' in subset: - subset.remove('markdown') + """Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user.""" + subset = [field for field in super(CapaDescriptor,self).editable_metadata_fields + if field not in ['markdown', 'empty']] return subset diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 750c8615a0..2c69c449ba 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -446,7 +446,7 @@ class CourseDescriptor(SequenceDescriptor): # utility function to get datetime objects for dates used to # compute the is_new flag and the sorting_score def to_datetime(timestamp): - return datetime.fromtimestamp(time.mktime(timestamp)) + return datetime(*timestamp[:6]) def get_date(field): timetuple = self._try_parse_time(field) @@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor): raise ValueError("First appointment date must be before last appointment date") if self.registration_end_date > self.last_eligible_appointment_date: raise ValueError("Registration end date must be before last appointment date") - + self.exam_url = exam_info.get('Exam_URL') def _try_parse_time(self, key): """ @@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor): else: return None + def get_test_center_exam(self, exam_series_code): + exams = [exam for exam in self.test_center_exams if exam.exam_series_code == exam_series_code] + return exams[0] if len(exams) == 1 else None + @property def title(self): return self.display_name diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 4cff477127..20700ab092 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -120,7 +120,7 @@ div.combined-rubric-container { } } - b.rubric-category { + span.rubric-category { font-size: .9em; } padding-bottom: 5px; diff --git a/common/lib/xmodule/xmodule/css/html/display.scss b/common/lib/xmodule/xmodule/css/html/display.scss index 956923c6d0..93138ac5a9 100644 --- a/common/lib/xmodule/xmodule/css/html/display.scss +++ b/common/lib/xmodule/xmodule/css/html/display.scss @@ -49,10 +49,18 @@ p { em, i { font-style: italic; + + span { + font-style: italic; + } } strong, b { font-weight: bold; + + span { + font-weight: bold; + } } p + p, ul + p, ol + p { diff --git a/common/lib/xmodule/xmodule/css/videoalpha/display.scss b/common/lib/xmodule/xmodule/css/videoalpha/display.scss new file mode 100644 index 0000000000..bf575e74a3 --- /dev/null +++ b/common/lib/xmodule/xmodule/css/videoalpha/display.scss @@ -0,0 +1,559 @@ +& { + margin-bottom: 30px; +} + +div.video { + @include clearfix(); + background: #f3f3f3; + display: block; + margin: 0 -12px; + padding: 12px; + border-radius: 5px; + + article.video-wrapper { + float: left; + margin-right: flex-gutter(9); + width: flex-grid(6, 9); + + section.video-player { + height: 0; + overflow: hidden; + padding-bottom: 56.25%; + position: relative; + + object, iframe { + border: none; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + } + } + + section.video-controls { + @include clearfix(); + background: #333; + border: 1px solid #000; + border-top: 0; + color: #ccc; + position: relative; + + &:hover { + ul, div { + opacity: 1; + } + } + + div.slider { + @include clearfix(); + background: #c2c2c2; + border: 1px solid #000; + @include border-radius(0); + border-top: 1px solid #000; + @include box-shadow(inset 0 1px 0 #eee, 0 1px 0 #555); + height: 7px; + margin-left: -1px; + margin-right: -1px; + @include transition(height 2.0s ease-in-out); + + div.ui-widget-header { + background: #777; + @include box-shadow(inset 0 1px 0 #999); + } + + a.ui-slider-handle { + background: $pink url(../images/slider-handle.png) center center no-repeat; + @include background-size(50%); + border: 1px solid darken($pink, 20%); + @include border-radius(15px); + @include box-shadow(inset 0 1px 0 lighten($pink, 10%)); + cursor: pointer; + height: 15px; + margin-left: -7px; + top: -4px; + @include transition(height 2.0s ease-in-out, width 2.0s ease-in-out); + width: 15px; + + &:focus, &:hover { + background-color: lighten($pink, 10%); + outline: none; + } + } + } + + ul.vcr { + @extend .dullify; + float: left; + list-style: none; + margin: 0 lh() 0 0; + padding: 0; + + li { + float: left; + margin-bottom: 0; + + a { + border-bottom: none; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555); + cursor: pointer; + display: block; + line-height: 46px; + padding: 0 lh(.75); + text-indent: -9999px; + @include transition(background-color, opacity); + width: 14px; + background: url('../images/vcr.png') 15px 15px no-repeat; + outline: 0; + + &:focus { + outline: 0; + } + + &:empty { + height: 46px; + background: url('../images/vcr.png') 15px 15px no-repeat; + } + + &.play { + background-position: 17px -114px; + + &:hover { + background-color: #444; + } + } + + &.pause { + background-position: 16px -50px; + + &:hover { + background-color: #444; + } + } + } + + div.vidtime { + padding-left: lh(.75); + font-weight: bold; + line-height: 46px; //height of play pause buttons + padding-left: lh(.75); + -webkit-font-smoothing: antialiased; + } + } + } + + div.secondary-controls { + @extend .dullify; + float: right; + + div.speeds { + float: left; + position: relative; + + &.open { + &>a { + background: url('../images/open-arrow.png') 10px center no-repeat; + } + + ol.video_speeds { + display: block; + opacity: 1; + padding: 0; + margin: 0; + list-style: none; + } + } + + &>a { + background: url('../images/closed-arrow.png') 10px center no-repeat; + border-left: 1px solid #000; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + @include clearfix(); + color: #fff; + cursor: pointer; + display: block; + line-height: 46px; //height of play pause buttons + margin-right: 0; + padding-left: 15px; + position: relative; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 116px; + outline: 0; + + &:focus { + outline: 0; + } + + h3 { + color: #999; + float: left; + font-size: em(14); + font-weight: normal; + letter-spacing: 1px; + padding: 0 lh(.25) 0 lh(.5); + line-height: 46px; + text-transform: uppercase; + } + + p.active { + float: left; + font-weight: bold; + margin-bottom: 0; + padding: 0 lh(.5) 0 0; + line-height: 46px; + color: #fff; + } + + &:hover, &:active, &:focus { + opacity: 1; + background-color: #444; + } + } + + // fix for now + ol.video_speeds { + @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); + @include transition(); + background-color: #444; + border: 1px solid #000; + bottom: 46px; + display: none; + opacity: 0; + position: absolute; + width: 133px; + z-index: 10; + + li { + @include box-shadow( 0 1px 0 #555); + border-bottom: 1px solid #000; + color: #fff; + cursor: pointer; + + a { + border: 0; + color: #fff; + display: block; + padding: lh(.5); + + &:hover { + background-color: #666; + color: #aaa; + } + } + + &.active { + font-weight: bold; + } + + &:last-child { + @include box-shadow(none); + border-bottom: 0; + margin-top: 0; + } + } + } + } + + div.volume { + float: left; + position: relative; + + &.open { + .volume-slider-container { + display: block; + opacity: 1; + } + } + + &.muted { + &>a { + background: url('../images/mute.png') 10px center no-repeat; + } + } + + > a { + background: url('../images/volume.png') 10px center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + @include clearfix(); + color: #fff; + cursor: pointer; + display: block; + height: 46px; + margin-right: 0; + padding-left: 15px; + position: relative; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 30px; + + &:hover, &:active, &:focus { + background-color: #444; + } + } + + .volume-slider-container { + @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); + @include transition(); + background-color: #444; + border: 1px solid #000; + bottom: 46px; + display: none; + opacity: 0; + position: absolute; + width: 45px; + height: 125px; + margin-left: -1px; + z-index: 10; + + .volume-slider { + height: 100px; + border: 0; + width: 5px; + margin: 14px auto; + background: #666; + border: 1px solid #000; + @include box-shadow(0 1px 0 #333); + + a.ui-slider-handle { + background: $pink url(../images/slider-handle.png) center center no-repeat; + @include background-size(50%); + border: 1px solid darken($pink, 20%); + @include border-radius(15px); + @include box-shadow(inset 0 1px 0 lighten($pink, 10%)); + cursor: pointer; + height: 15px; + left: -6px; + @include transition(height 2.0s ease-in-out, width 2.0s ease-in-out); + width: 15px; + } + + .ui-slider-range { + background: #ddd; + } + } + } + } + + a.add-fullscreen { + background: url(../images/fullscreen.png) center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + color: #797979; + display: block; + float: left; + line-height: 46px; //height of play pause buttons + margin-left: 0; + padding: 0 lh(.5); + text-indent: -9999px; + @include transition(); + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + } + + a.quality_control { + background: url(../images/hd.png) center no-repeat; + border-right: 1px solid #000; + @include box-shadow(1px 0 0 #555, inset 1px 0 0 #555); + color: #797979; + display: block; + float: left; + line-height: 46px; //height of play pause buttons + margin-left: 0; + padding: 0 lh(.5); + text-indent: -9999px; + @include transition(); + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + + &.active { + background-color: #F44; + color: #0ff; + text-decoration: none; + } + } + + + a.hide-subtitles { + background: url('../images/cc.png') center no-repeat; + color: #797979; + display: block; + float: left; + font-weight: 800; + line-height: 46px; //height of play pause buttons + margin-left: 0; + opacity: 1; + padding: 0 lh(.5); + position: relative; + text-indent: -9999px; + @include transition(); + -webkit-font-smoothing: antialiased; + width: 30px; + + &:hover { + background-color: #444; + color: #fff; + text-decoration: none; + } + + &.off { + opacity: .7; + } + } + } + } + + &:hover section.video-controls { + ul, div { + opacity: 1; + } + + div.slider { + height: 14px; + margin-top: -7px; + + a.ui-slider-handle { + @include border-radius(20px); + height: 20px; + margin-left: -10px; + top: -4px; + width: 20px; + } + } + } + } + + ol.subtitles { + padding-left: 0; + float: left; + max-height: 460px; + overflow: auto; + width: flex-grid(3, 9); + margin: 0; + font-size: 14px; + list-style: none; + + li { + border: 0; + color: #666; + cursor: pointer; + margin-bottom: 8px; + padding: 0; + line-height: lh(); + + &.current { + color: #333; + font-weight: 700; + } + + &:hover { + color: $blue; + } + + &:empty { + margin-bottom: 0px; + } + } + } + + &.closed { + @extend .trans; + + article.video-wrapper { + width: flex-grid(9,9); + } + + ol.subtitles { + width: 0; + height: 0; + } + } + + &.fullscreen { + background: rgba(#000, .95); + border: 0; + bottom: 0; + height: 100%; + left: 0; + margin: 0; + overflow: hidden; + padding: 0; + position: fixed; + top: 0; + width: 100%; + z-index: 999; + vertical-align: middle; + + &.closed { + ol.subtitles { + right: -(flex-grid(4)); + width: auto; + } + } + + div.tc-wrapper { + @include clearfix; + display: table; + width: 100%; + height: 100%; + + article.video-wrapper { + width: 100%; + display: table-cell; + vertical-align: middle; + float: none; + } + + object, iframe { + bottom: 0; + height: 100%; + left: 0; + overflow: hidden; + position: fixed; + top: 0; + } + + section.video-controls { + bottom: 0; + left: 0; + position: absolute; + width: 100%; + z-index: 9999; + } + } + + ol.subtitles { + background: rgba(#000, .8); + bottom: 0; + height: 100%; + max-height: 100%; + max-width: flex-grid(3); + padding: lh(); + position: fixed; + right: 0; + top: 0; + @include transition(); + + li { + color: #aaa; + + &.current { + color: #fff; + } + } + } + } +} diff --git a/common/lib/xmodule/xmodule/foldit_module.py b/common/lib/xmodule/xmodule/foldit_module.py new file mode 100644 index 0000000000..ea16fee7f1 --- /dev/null +++ b/common/lib/xmodule/xmodule/foldit_module.py @@ -0,0 +1,124 @@ +import logging +from lxml import etree +from dateutil import parser + +from pkg_resources import resource_string + +from xmodule.editing_module import EditingDescriptor +from xmodule.x_module import XModule +from xmodule.xml_module import XmlDescriptor + +log = logging.getLogger(__name__) + +class FolditModule(XModule): + def __init__(self, system, location, definition, descriptor, + instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, descriptor, + instance_state, shared_state, **kwargs) + # ooh look--I'm lazy, so hardcoding the 7.00x required level. + # If we need it generalized, can pull from the xml later + self.required_level = 4 + self.required_sublevel = 5 + + def parse_due_date(): + """ + Pull out the date, or None + """ + s = self.metadata.get("due") + if s: + return parser.parse(s) + else: + return None + + self.due_str = self.metadata.get("due", "None") + self.due = parse_due_date() + + def is_complete(self): + """ + Did the user get to the required level before the due date? + """ + # We normally don't want django dependencies in xmodule. foldit is + # special. Import this late to avoid errors with things not yet being + # initialized. + from foldit.models import PuzzleComplete + + complete = PuzzleComplete.is_level_complete( + self.system.anonymous_student_id, + self.required_level, + self.required_sublevel, + self.due) + return complete + + def completed_puzzles(self): + """ + Return a list of puzzles that this user has completed, as an array of + dicts: + + [ {'set': int, + 'subset': int, + 'created': datetime} ] + + The list is sorted by set, then subset + """ + from foldit.models import PuzzleComplete + + return sorted( + PuzzleComplete.completed_puzzles(self.system.anonymous_student_id), + key=lambda d: (d['set'], d['subset'])) + + + def get_html(self): + """ + Render the html for the module. + """ + goal_level = '{0}-{1}'.format( + self.required_level, + self.required_sublevel) + + context = { + 'due': self.due_str, + 'success': self.is_complete(), + 'goal_level': goal_level, + 'completed': self.completed_puzzles(), + } + + return self.system.render_template('foldit.html', context) + + + def get_score(self): + """ + 0 / 1 based on whether student has gotten far enough. + """ + score = 1 if self.is_complete() else 0 + return {'score': score, + 'total': self.max_score()} + + def max_score(self): + return 1 + + +class FolditDescriptor(XmlDescriptor, EditingDescriptor): + """ + Module for adding open ended response questions to courses + """ + mako_template = "widgets/html-edit.html" + module_class = FolditModule + filename_extension = "xml" + + stores_state = True + has_score = True + template_dir_name = "foldit" + + js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} + js_module_name = "HTMLEditingDescriptor" + + # The grade changes without any student interaction with the edx website, + # so always need to actually check. + always_recalculate_grades = True + + @classmethod + def definition_from_xml(cls, xml_object, system): + """ + For now, don't need anything from the xml + """ + return {} diff --git a/common/lib/xmodule/xmodule/hidden_module.py b/common/lib/xmodule/xmodule/hidden_module.py index d4f2a0fa33..e7639e63c8 100644 --- a/common/lib/xmodule/xmodule/hidden_module.py +++ b/common/lib/xmodule/xmodule/hidden_module.py @@ -3,7 +3,11 @@ from xmodule.raw_module import RawDescriptor class HiddenModule(XModule): - pass + def get_html(self): + if self.system.user_is_staff: + return "ERROR: This module is unknown--students will not see it at all" + else: + return "" class HiddenDescriptor(RawDescriptor): diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index af1ce0ad80..456ea3cf10 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -172,6 +172,13 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): elt.set("filename", relname) return elt + @property + def editable_metadata_fields(self): + """Remove any metadata from the editable fields which have their own editor or shouldn't be edited by user.""" + subset = [field for field in super(HtmlDescriptor,self).editable_metadata_fields + if field not in ['empty']] + return subset + class AboutDescriptor(HtmlDescriptor): """ diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore index 03534687ca..bbd93c90e3 100644 --- a/common/lib/xmodule/xmodule/js/src/.gitignore +++ b/common/lib/xmodule/xmodule/js/src/.gitignore @@ -1,2 +1 @@ -*.js - +# Please do not ignore *.js files. Some xmodules are written in JS. diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee index 1b14d67016..fd0391450b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -4,7 +4,7 @@ class @Rubric # finds the scores for each rubric category @get_score_list: () => # find the number of categories: - num_categories = $('b.rubric-category').length + num_categories = $('.rubric-category').length score_lst = [] # get the score for each one @@ -23,7 +23,7 @@ class @Rubric @check_complete: () -> # check to see whether or not any categories have not been scored - num_categories = $('b.rubric-category').length + num_categories = $('.rubric-category').length for i in [0..(num_categories-1)] score = $("input[name='score-selection-#{i}']:checked").val() if score == undefined diff --git a/common/lib/xmodule/xmodule/js/src/html/edit.coffee b/common/lib/xmodule/xmodule/js/src/html/edit.coffee index 238182f3d9..eae9df0f20 100644 --- a/common/lib/xmodule/xmodule/js/src/html/edit.coffee +++ b/common/lib/xmodule/xmodule/js/src/html/edit.coffee @@ -107,12 +107,13 @@ class @HTMLEditingDescriptor # In order for isDirty() to return true ONLY if edits have been made after setting the text, # both the startContent must be sync'ed up and the dirty flag set to false. visualEditor.startContent = visualEditor.getContent({format: "raw", no_events: 1}); - visualEditor.isNotDirty = true @focusVisualEditor(visualEditor) @showingVisualEditor = true focusVisualEditor: (visualEditor) => visualEditor.focus() + # Need to mark editor as not dirty both when it is initially created and when we switch back to it. + visualEditor.isNotDirty = true if not @$mceToolbar? @$mceToolbar = $(@element).find('table.mceToolbar') diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee new file mode 100644 index 0000000000..a27362b094 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display.coffee @@ -0,0 +1,103 @@ +class @VideoAlpha + constructor: (element) -> + @el = $(element).find('.video') + @id = @el.attr('id').replace(/video_/, '') + @start = @el.data('start') + @end = @el.data('end') + @caption_data_dir = @el.data('caption-data-dir') + @caption_asset_path = @el.data('caption-asset-path') + @show_captions = @el.data('show-captions').toString() == "true" + @el = $("#video_#{@id}") + if @parseYoutubeId(@el.data("streams")) is true + @videoType = "youtube" + @fetchMetadata() + @parseSpeed() + else + @videoType = "html5" + @parseHtml5Sources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source') + @speeds = ['0.75', '1.0', '1.25', '1.50'] + sub = @el.data('sub') + if (typeof sub isnt "string") or (sub.length is 0) + sub = "" + @show_captions = false + @videos = + "0.75": sub + "1.0": sub + "1.25": sub + "1.5": sub + @setSpeed $.cookie('video_speed') + $("#video_#{@id}").data('video', this).addClass('video-load-complete') + if @show_captions is true + @hide_captions = $.cookie('hide_captions') == 'true' + else + @hide_captions = true + $.cookie('hide_captions', @hide_captions, expires: 3650, path: '/') + @el.addClass 'closed' + if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player)) + @embed() + else + if @videoType is "youtube" + window.onYouTubePlayerAPIReady = => + @embed() + else if @videoType is "html5" + window.onHTML5PlayerAPIReady = => + @embed() + + youtubeId: (speed)-> + @videos[speed || @speed] + + parseYoutubeId: (videos)-> + return false if (typeof videos isnt "string") or (videos.length is 0) + @videos = {} + $.each videos.split(/,/), (index, video) => + speed = undefined + video = video.split(/:/) + speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0") + @videos[speed] = video[1] + true + + parseHtml5Sources: (mp4Source, webmSource, oggSource)-> + @html5Sources = + mp4: null + webm: null + ogg: null + @html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0) + @html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0) + @html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0) + + parseSpeed: -> + @speeds = ($.map @videos, (url, speed) -> speed).sort() + @setSpeed $.cookie('video_speed') + + setSpeed: (newSpeed, updateCookie)-> + if @speeds.indexOf(newSpeed) isnt -1 + @speed = newSpeed + + if updateCookie isnt false + $.cookie "video_speed", "" + newSpeed, + expires: 3650 + path: "/" + else + @speed = "1.0" + + embed: -> + @player = new VideoPlayerAlpha video: this + + fetchMetadata: (url) -> + @metadata = {} + $.each @videos, (speed, url) => + $.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp' + + getDuration: -> + @metadata[@youtubeId()].duration + + log: (eventName)-> + logInfo = + id: @id + code: @youtubeId() + currentTime: @player.currentTime + speed: @speed + if @videoType is "youtube" + logInfo.code = @youtubeId() + else logInfo.code = "html5" if @videoType is "html5" + Logger.log eventName, logInfo diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee new file mode 100644 index 0000000000..6b86296dfa --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/_subview.coffee @@ -0,0 +1,14 @@ +class @SubviewAlpha + constructor: (options) -> + $.each options, (key, value) => + @[key] = value + @initialize() + @render() + @bind() + + $: (selector) -> + $(selector, @el) + + initialize: -> + render: -> + bind: -> diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js new file mode 100644 index 0000000000..c3cc462ab8 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/html5_video.js @@ -0,0 +1,294 @@ +this.HTML5Video = (function () { + var HTML5Video; + + HTML5Video = {}; + + HTML5Video.Player = (function () { + Player.prototype.callStateChangeCallback = function () { + if ($.isFunction(this.config.events.onStateChange) === true) { + this.config.events.onStateChange({ + 'data': this.playerState + }); + } + }; + + Player.prototype.pauseVideo = function () { + this.video.pause(); + }; + + Player.prototype.seekTo = function (value) { + if ((typeof value === 'number') && (value <= this.video.duration) && (value >= 0)) { + this.start = 0; + this.end = this.video.duration; + + this.video.currentTime = value; + } + }; + + Player.prototype.setVolume = function (value) { + if ((typeof value === 'number') && (value <= 100) && (value >= 0)) { + this.video.volume = value * 0.01; + } + }; + + Player.prototype.getCurrentTime = function () { + return this.video.currentTime; + }; + + Player.prototype.playVideo = function () { + this.video.play(); + }; + + Player.prototype.getPlayerState = function () { + return this.playerState; + }; + + Player.prototype.getVolume = function () { + return this.video.volume; + }; + + Player.prototype.getDuration = function () { + if (isFinite(this.video.duration) === false) { + return 0; + } + + return this.video.duration; + }; + + Player.prototype.setPlaybackRate = function (value) { + var newSpeed; + + newSpeed = parseFloat(value); + + if (isFinite(newSpeed) === true) { + this.video.playbackRate = value; + } + }; + + Player.prototype.getAvailablePlaybackRates = function () { + return [0.75, 1.0, 1.25, 1.5]; + }; + + return Player; + + /* + * Constructor function for HTML5 Video player. + * + * @el - A DOM element where the HTML5 player will be inserted (as returned by jQuery(selector) function), + * or a selector string which will be used to select an element. This is a required parameter. + * + * @config - An object whose properties will be used as configuration options for the HTML5 video + * player. This is an optional parameter. In the case if this parameter is missing, or some of the config + * object's properties are missing, defaults will be used. The available options (and their defaults) are as + * follows: + * + * config = { + * + * 'videoSources': {}, // An object with properties being video sources. The property name is the + * // video format of the source. Supported video formats are: 'mp4', 'webm', and + * // 'ogg'. + * + * 'playerVars': { // Object's properties identify player parameters. + * 'start': 0, // Possible values: positive integer. Position from which to start playing the + * // video. Measured in seconds. If value is non-numeric, or 'start' property is + * // not specified, the video will start playing from the beginning. + * + * 'end': null // Possible values: positive integer. Position when to stop playing the + * // video. Measured in seconds. If value is null, or 'end' property is not + * // specified, the video will end playing at the end. + * + * }, + * + * 'events': { // Object's properties identify the events that the API fires, and the + * // functions (event listeners) that the API will call when those events occur. + * // If value is null, or property is not specified, then no callback will be + * // called for that event. + * + * 'onReady': null, + * 'onStateChange': null + * } + * } + */ + function Player(el, config) { + var sourceStr, _this; + + // If el is string, we assume it is an ID of a DOM element. Get the element, and check that the ID + // really belongs to an element. If we didn't get a DOM element, return. At this stage, nothing will + // break because other parts of the video player are waiting for 'onReady' callback to be called. + if (typeof el === 'string') { + this.el = $(el); + + if (this.el.length === 0) { + return; + } + } else if (el instanceof jQuery) { + this.el = el; + } else { + return; + } + + // A simple test to see that the 'config' is a normal object. + if ($.isPlainObject(config) === true) { + this.config = config; + } else { + return; + } + + // We should have at least one video source. Otherwise there is no point to continue. + if (config.hasOwnProperty('videoSources') === false) { + return; + } + + // From the start, all sources are empty. We will populate this object below. + sourceStr = { + 'mp4': ' ', + 'webm': ' ', + 'ogg': ' ' + }; + + // Will be used in inner functions to point to the current object. + _this = this; + + // Create HTML markup for individual sources of the HTML5
    +

    + Flag as inappropriate content for later review +

    diff --git a/lms/templates/main.html b/lms/templates/main.html index 5d3fd29104..42d5a71228 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -29,13 +29,18 @@ +% if not suppress_toplevel_navigation: <%include file="navigation.html" /> +% endif +
    ${self.body()} <%block name="bodyextra"/>
    +% if not suppress_toplevel_navigation: <%include file="footer.html" /> +% endif <%static:js group='application'/> <%static:js group='module-js'/> diff --git a/lms/templates/open_ended_combined_rubric.html b/lms/templates/open_ended_combined_rubric.html index 7f988a38dc..61393cdc95 100644 --- a/lms/templates/open_ended_combined_rubric.html +++ b/lms/templates/open_ended_combined_rubric.html @@ -1,7 +1,7 @@
    % for i in range(len(categories)): <% category = categories[i] %> - ${category['description']}
    + ${category['description']}
      % for j in range(len(category['options'])): <% option = category['options'][j] %> diff --git a/lms/templates/open_ended_rubric.html b/lms/templates/open_ended_rubric.html index da2be513d9..2cbab3ab3b 100644 --- a/lms/templates/open_ended_rubric.html +++ b/lms/templates/open_ended_rubric.html @@ -4,7 +4,7 @@
      % for i in range(len(categories)): <% category = categories[i] %> - ${category['description']}
      + ${category['description']}
        % for j in range(len(category['options'])): <% option = category['options'][j] %> diff --git a/lms/templates/quickedit.html b/lms/templates/quickedit.html deleted file mode 100644 index bc8e74eb65..0000000000 --- a/lms/templates/quickedit.html +++ /dev/null @@ -1,180 +0,0 @@ -<%namespace name='static' file='static_content.html'/> - - -## ----------------------------------------------------------------------------- -## Template for courseware.views.quickedit -## -## Used for quick-edit link present when viewing capa-format assesment problems. -## ----------------------------------------------------------------------------- - - -## -## - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:css group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: -## -% endif - - - - - -% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - <%static:js group='application'/> -% endif - -% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: - % for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]: - - % endfor -% endif - -## codemirror - - - -## alternate codemirror -## -## -## - -## image input: for clicking on images (see imageinput.html) - - -## - - - -<%block name="headextra"/> - - - <%include file="mathjax_include.html" /> - - - - - - - -## ----------------------------------------------------------------------------- -## information and i4x PSL code - -
        -

        QuickEdit

        -
        -
          -
        • File = ${filename}
        • -
        • ID = ${id}
        • -
        - -
        - -
        - - - -
        - -${msg|n} - -## ----------------------------------------------------------------------------- -## rendered problem display - - - -
        - - - - - - - -
        -
        -
        - ${phtml} -
        -
        -
        - - - - - -## - - - - - - - - <%block name="js_extra"/> - - - diff --git a/lms/templates/staff_problem_info.html b/lms/templates/staff_problem_info.html index 61cda0c52b..9324445dd1 100644 --- a/lms/templates/staff_problem_info.html +++ b/lms/templates/staff_problem_info.html @@ -1,5 +1,6 @@ ${module_content} -%if edit_link: +%if location.category in ['problem','video','html']: +% if edit_link:
        Edit / QA
        -% endif +% endif
      -

      Our mission is to transform learning.

      +

      Our mission is to transform learning.

      -
      -

      “EdX represents a unique opportunity to improve education on our campuses through online learning, while simultaneously creating a bold new educational path for millions of learners worldwide.”

      - —Rafael Reif, MIT President -
      +
      +

      “EdX represents a unique opportunity to improve education on our campuses through online learning, while simultaneously creating a bold new educational path for millions of learners worldwide.”

      + —Rafael Reif, MIT President +
      -
      -

      “EdX gives Harvard and MIT an unprecedented opportunity to dramatically extend our collective reach by conducting groundbreaking research into effective education and by extending online access to quality higher education.”

      - —Drew Faust, Harvard President -
      +
      +

      “EdX gives Harvard and MIT an unprecedented opportunity to dramatically extend our collective reach by conducting groundbreaking research into effective education and by extending online access to quality higher education.”

      + —Drew Faust, Harvard President +
    @@ -34,25 +34,45 @@
    -
    +

    EdX is looking to add new talent to our team!

    Our mission is to give a world-class education to everyone, everywhere, regardless of gender, income or social status

    Today, EdX.org, a not-for-profit provides hundreds of thousands of people from around the globe with access to free education.  We offer amazing quality classes by the best professors from the best schools. We enable our members to uncover a new passion that will transform their lives and their communities.

    -

    Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you're results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.

    +

    Around the world-from coast to coast, in over 192 countries, people are making the decision to take one or several of our courses. As we continue to grow our operations, we are looking for talented, passionate people with great ideas to join the edX team. We aim to create an environment that is supportive, diverse, and as fun as our brand. If you’re results-oriented, dedicated, and ready to contribute to an unparalleled member experience for our community, we really want you to apply.

    As part of the edX team, you’ll receive:

    • Competitive compensation
    • Generous benefits package
    • Free lunch every day
    • -
    • A great working experience where everyone cares
    • +
    • A great working experience where everyone cares and wants to change the world (no, we’re not kidding)
    -

    While we appreciate every applicant's interest, only those under consideration will be contacted. We regret that phone calls will not be accepted.

    +

    While we appreciate every applicant’s interest, only those under consideration will be contacted. We regret that phone calls will not be accepted. Equal opportunity employer.

    -
    +
    + +
    +
    +

    DIRECTOR OF EDUCATIONAL SERVICES

    +

    The edX Director of Education Services reporting to the VP of Engineering and Educational Services is responsible for:

    +
      +
    1. Delivering 20 new courses in 2013 in collaboration with the partner Universities +
        +
      • Reporting to the Director of Educational Services are the Video production team, responsible for post-production of Course Video. The Director must understand how to balance artistic quality and learning objectives, and reduce production time so that video capabilities are readily accessible and at reasonable costs.

        +
      • Reporting to the Director are a small team of Program Managers, who are responsible for managing the day to day of course production and operations. The Director must be experienced in capacity planning and operations, understand how to deploy lean collaboration and able to build alliances inside edX and the University. In conjunction with the Program Managers, the Director of Educational Services will supervise the collection of research, the retrospectives with Professors and the assembly of best practices in course production and operations. The three key deliverables are the use of a well-defined lean process for onboarding Professors, the development of tracking tools, and assessment of effectiveness of Best Practices. +
      • Also reporting to the Director of Education Services are content engineers and Course Fellows, skilled in the development of edX assessments. The Director of Educational Services will also be responsible for communicating to the VP of Engineering requirements for new types of course assessments. Course Fellows are extremely talented Ph.D.’s who work directly with the Professors to define and develop assessments and course curriculum.
      • +
      +
    2. +
    3. Training and Onboarding of 30 Partner Universities and Affiliates +
        +
      • The edX Director of Educational Services is responsible for building out the Training capabilities and delivery mechanisms for onboarding Professors at partner Universities. The edX Director must build out both the Training Team and the curriculum. Training will be delivered in both online courses, self-paced formats, and workshops. The training must cover a curriculum that enables partner institutions to be completely independent. Additionally, partner institutions should be engaged to contribute to the curriculum and partner with edX in the delivery of the material. The curriculum must exemplify the best in online learning, so the Universities are inspired to offer the kind of learning they have experienced in their edX Training.
      • +
      • Expand and extend the education goals of the partner Universities by operationalizing best practices.
      • +
      • Engage with University Boards to design and define the success that the technology makes possible.
      • +
      +
    4. +
    5. Growing the Team, Growing the Business +
        +
      • The edX Director will be responsible for working with Business Development to identify revenue opportunities and build profitable plans to grow the business and grow the team.
      • +
      • Maintain for-profit nimbleness in an organization committed to non-profit ideals.
      • +
      • Design scalable solutions to opportunities revealed by technical innovations
      • +
      +
    6. +
    7. Integrating a Strong Team within Strong Organization +
        +
      • Connect organization’s management and University leadership with consistent and high quality expectations and deployment
      • +
      • Integrate with a highly collaborative leadership team to maximize talents of the organization
      • +
      • Successfully escalate issues within and beyond the organization to ensure the best possible educational outcome for students and Universities
      • +
      +
    8. +
    +

    Skills:

    + + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + +
    +
    +

    MANAGER OF TRAINING SERVICES

    +

    The Manager of Training Services is an integral member of the edX team, a leader who is also a doer, working hands-on in the development and delivery of edX’s training portfolio. Reporting to the Director of Educational Services, the manager will be a strategic thinker, providing leadership and vision in the development of world-class training solutions tailored to meet the diverse needs of edX Universities, partners and stakeholders

    +

    Responsibilities:

    + +

    Requirements:

    + + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    -

    INSTRUCTIONAL DESIGNER — CONTRACT OPPORTUNITY

    -

    The Instructional Designer will work collaboratively with the edX content and engineering teams to plan, develop and deliver highly engaging and media rich online courses. The Instructional Designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines.

    -

    Responsibilities:

    - -

    Qualifications:

    - -

    Eligible candidates will be invited to respond to an Instructional Design task based on current or future edX course development needs.

    -

    If you are interested in this position, please send an email to jobs@edx.org.

    -
    -
    - -
    -
    -

    MEMBER SERVICES MANAGER

    -

    The edX Member Services Manager is responsible for both defining support best practices and directly supporting edX members by handling or routing issues that come in from our websites, email and social media tools.  We are looking for a passionate person to help us define and own this experience. While this is a Manager level position, we see this candidate quickly moving through the ranks, leading a larger team of employees over time. This staff member will be running our fast growth support organization.

    +

    INSTRUCTIONAL DESIGNER

    +

    The Instructional Designer will work collaboratively with the edX content and engineering teams to plan, develop and deliver highly engaging and media rich online courses. The Instructional Designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines.

    Responsibilities:

    Qualifications:

    + +

    Eligible candidates will be invited to respond to an Instructional Design task based on current or future edX course development needs.

    +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + + +
    +
    +

    PROGRAM MANAGER

    +

    edX Program Managers (PM) lead the edX's course production process. They are systems thinkers who manage the creation of a course from start to finish. PMs work with University Professors and course staff to help them take advantage of edX services to create world class online learning offerings and encourage the exploration of an emerging form of higher education.

    +

    Responsibilities:

    + +

    Qualifications:

    + + +

    Preferred qualifications

    +

    If you are interested in this position, please send an email to jobs@edx.org.

    -
    +
    -

    DIRECTOR OF PR AND COMMUNICATIONS

    -

    The edX Director of PR & Communications is responsible for creating and executing all PR strategy and providing company-wide leadership to help create and refine the edX core messages and identity as the revolutionary global leader in both on-campus and worldwide education. The Director will design and direct a communications program that conveys cohesive and compelling information about edX's mission, activities, personnel and products while establishing a distinct identity for edX as the leader in online education for both students and learning institutions.

    +

    PROJECT MANAGER (PMO)

    +

    As a fast paced, rapidly growing organization serving the evolving online higher education market, edX maximizes its talents and resources. To help make the most of this unapologetically intelligent and dedicated team, we seek a project manager to increase the accuracy of our resource and schedule estimates and our stakeholder satisfaction.

    Responsibilities:

      -
    • Develop and execute goals and strategy for a comprehensive external and internal communications program focused on driving student engagement around courses and institutional adoption of the edX learning platform.
    • -
    • Work with media, either directly or through our agency of record, to establish edX as the industry leader in global learning.
    • -
    • Work with key influencers including government officials on a global scale to ensure the edX mission, content and tools are embraced and supported worldwide.
    • -
    • Work with marketing colleagues to co-develop and/or monitor and evaluate the content and delivery of all communications messages and collateral.
    • -
    • Initiate and/or plan thought leadership events developed to heighten target-audience awareness; participate in meetings and trade shows
    • -
    • Conduct periodic research to determine communications benchmarks
    • -
    • Inform employees about edX's vision, values, policies, and strategies to enable them to perform their jobs efficiently and drive morale.
    • -
    • Work with and manage existing communications team to effectively meet strategic goals.
    • +
    • Coordinate multiple projects to bring Courses, Software Product and Marketing initiatives to market, all of which are related, which have both dedicated and shared resources.
    • +
    • Provide, at a moment’s notice, the state of development, so that priorities can be enforced or reset, so that future expectations can be set accurately.
    • +
    • Develop lean processes that supports a wide variety of efforts which draw on a shared resource pool.
    • +
    • Develop metrics on resource use that support the leadership team in optimizing how they respond to unexpected challenges and new opportunities.
    • +
    • Accurately and clearly escalate only those issues which need escalation for productive resolution. Assist in establishing consensus for all other issues.
    • +
    • Advise the team on best practices, whether developed internally or as industry standards.
    • +
    • Recommend to the leadership team how to re-deploy key resources to better match stated priorities.
    • +
    • Help the organization deliver on its commitments with more consistency and efficiency. Allow the organization to respond to new opportunities with more certainty in its ability to forecast resource needs.
    • +
    • Select and maintain project management tools for Scrum and Kanban that can serve as the standard for those we use with our partners.
    • +
    • Forecast future resource needs given the strategic direction of the organization.
    -

    Qualifications:

    +

    Skills:

      -
    • Ten years of experience in PR and communications
    • -
    • Ability to work creatively and provide company-wide leadership in a fast-paced, dynamic start-up environment required
    • -
    • Adaptability - the individual adapts to changes in the work environment, manages competing demands and is able to deal with frequent change, delays or unexpected events.
    • -
    • Experience in working in successful consumer-focused startups preferred
    • -
    • PR agency experience in setting strategy for complex multichannel, multinational organizations a plus.
    • -
    • Extensive writing experience and simply amazing oral, written, and interpersonal communications skills
    • -
    • B.A./B.S. in communications or related field
    • +
    • Bachelor’s degree or higher
    • +
    • Exquisite communication skills, especially listening
    • +
    • Inexhaustible attention to detail with the ability to let go of perfection
    • +
    • Deep commitment to Lean project management, including a dedication to its best intentions not just its rituals
    • +
    • Sense of humor and humility
    • +
    • Ability to hold on to the important in the face of the urgent

    If you are interested in this position, please send an email to jobs@edx.org.

    -
    +
    + + +
    +
    +

    DIRECTOR, PRODUCT MANAGEMENT

    +

    When the power of edX is at its fullest, individuals become the students they had always hoped to be, Professors teach the courses they had always imagined and Universities offer educational opportunities never before seen. None of that happens by accident, so edX is seeking a Product Manager who can keep their eyes on the future and their heart and hands with a team of ferociously intelligent and dedicated technologists. +

    +

    The responsibility of a Product Manager is first and foremost to provide evidence to the development team that what they build will succeed in the marketplace. It is the responsibility of the Product Manager to define the product backlog and the team to build the backlog. The Product Manager is one of the most highly leveraged individuals in the Engineering organization. They work to bring a deep knowledge of the Customer – Students, Professors and Course Staff to the product roadmap. The Product Manager is well-versed in the data and sets the KPI’s that drives the team, the Product Scorecard and the Company Scorecard. They are expected to become experts in the business of online learning, familiar with blended models, MOOC’s and University and Industry needs and the competition. The Product Manager must be able to understand the edX stakeholders. +

    +

    Responsibilities:

    +
      +
    • Assess users’ needs, whether students, Professors or Universities.
    • +
    • Research markets and competitors to provide data driven decisions.
    • +
    • Work with multiple engineering teams, through consensus and with data-backed arguments, in order to provide technology which defines the state of the art for online courses.
    • +
    • Repeatedly build and launch new products and services, complete with the training, documentation and metrics needed to enhance the already impressive brands of the edX partner institutions.
    • +
    • Establish the vision and future direction of the product with input from edX leadership and guidance from partner organizations.
    • +
    • Work in a lean organization, committed to Scrum and Kanban.
    • +
    +

    Qualifications:

    +
      +
    • Bachelor’s degree or higher in a Technical Area
    • +
    • MBA or Masters in Design preferred
    • +
    • Proven ability to develop and implement strategy
    • +
    • Exquisite organizational skills
    • +
    • Deep analytical skills
    • +
    • Social finesse and business sense
    • +
    • Scrum, Kanban
    • +
    • Infatuation with technology, in all its frustrating and fragile complexity
    • +
    • Top flight communication skills, oral and written, with teams which are centrally located and spread all over the world.
    • +
    • Personal commitment and experience of the transformational possibilities of higher education
    • +
    + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + + +
    +
    +

    CONTENT ENGINEER

    +

    Content engineers help create the technology for specific courses. The tasks include:

    +
      +
    • Developing of course-specific user-facing elements, such as the circuit editor and simulator.
    • +
    • Integrating course materials into courses
    • +
    • Creating programs to grade questions designed with complex technical features
    • +
    • Knowledge of Python, XML, and/or JavaScript is desired. Strong interest and background in pedagogy and education is desired as well.
    • +
    • Building course components in straight XML or through our course authoring tool, edX Studio.
    • +
    • Assisting University teams and in house staff take advantage of new course software, including designing and developing technical refinements for implementation.
    • +
    • Pushing content to production servers predictably and cleanly.
    • +
    • Sending high volumes of course email adhering to email engine protocols.
    • +
    +

    Qualifications:

    +
      +
    • Bachelor’s degree or higher
    • +
    • Thorough knowledge of Python, DJango, XML,HTML, CSS , Javascript and backbone.js
    • +
    • Ability to work on multiple projects simultaneously without splintering
    • +
    • Tactfully escalate conflicting deadlines or priorities only when needed. Otherwise help the team members negotiate a solution.
    • +
    • Unfailing attention to detail, especially the details the course teams have seen so often they don’t notice them anymore.
    • +
    • Readily zoom from the big picture to the smallest course component to notice when typos, inconsistencies or repetitions have unknowingly crept in
    • +
    • Curiosity to step into the shoes of an online student working to master the course content.
    • +
    • Solid interpersonal skills, especially good listening
    • +
    + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + + +
    +
    +

    DIRECTOR ENGINEERING, OPEN SOURCE COMMUNITY MANAGER

    +

    In edX courses, students make (and break) electronic circuits, they manipulate molecules on the fly and they do it all at once, in their tens of thousands. We have great Professors and great Universities. But we can’t possibly keep up with all the great ideas out there, so we’re making our platform open source, to turn up the volume on great education. To do that well, we’ll need a Director of Engineering who can lead our Open Source Community efforts.

    +

    Responsibilities:

    +
      +
    • Define and implement software design standards that make the open source community most welcome and productive.
    • +
    • Work with others to establish the governance standards for the edX Open Source Platform, establish the infrastructure, and manage the team to deliver releases and leverage our University partners and stakeholders to
    • make the edX platform the world’s best learning platform. +
    • Help the organization recognize the benefits and limitations inherent in open source solutions.
    • +
    • Establish best practices and key tool usage, especially those based on industry standards.
    • +
    • Provide visibility for the leadership team into the concerns and challenges faced by the open source community.
    • +
    • Foster a thriving community by providing the communication, documentation and feedback that they need to be enthusiastic.
    • +
    • Maximize the good code design coming from the open source community.
    • +
    • Provide the wit and firmness that the community needs to channel their energy productively.
    • +
    • Tactfully balance the internal needs of the organization to pursue new opportunities with the community’s need to participate in the platform’s evolution.
    • +
    • Shorten lines of communication and build trust across entire team
    • +
    +

    Qualifications:

    +
      + +
    • Bachelors, preferably Masters in Computer Science
    • +
    • Solid communication skills, especially written
    • +
    • Committed to Agile practice, Scrum and Kanban
    • +
    • Charm and humor
    • +
    • Deep familiarity with Open Source, participant and contributor
    • +
    • Python, Django, Javascript
    • +
    • Commitment to support your technical recommendations, both within and beyond the organization.
    • +
    + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    + + +
    +
    +

    SOFTWARE ENGINEER

    +

    edX is looking for engineers who can contribute to its Open Source learning platform. We are a small team with a startup, lean culture, committed to building open-source software that scales and dramatically changes the face of education. Our ideal candidates are hands on developers who understand how to build scalable, service based systems, preferably in Python and have a proven track record of bringing their ideas to market. We are looking for engineers with all levels of experience, but you must be a proven leader and outstanding developer to work at edX.

    + +

    There are a number of projects for which we are recruiting engineers:
    + +

    Learning Management System: We are developing an Open Source Standard that allows for the creation of instructional plug-ins and assessments in our platform. You must have a deep interest in semantics of learning, and able to build services at scale.

    + +

    Forums: We are building our own Forums software because we believe that education requires a forums platform capable of supporting learning communities. We are analytics driven. The ideal Forums candidates are focused on metrics and key performance indicators, understand how to build on top of a service based architecture and are wedded to quick iterations and user feedback. + +

    Analytics: We are looking for a platform engineer who has deep MongoDB or no SQL database experience. Our data infrastructure needs to scale to multiple terabytes. Researchers from Harvard, MIT, Berkeley and edX Universities will use our analytics platform to research and examine the fundamentals of learning. The analytics engineer will be responsible for both building out an analytics platform and a pub-sub and real-time pipeline processing architecture. Together they will allow researchers, students and Professors access to never before seen analytics. + +

    Course Development Authoring Tools: We are committed to making it easy for Professors to develop and publish their courses online. So we are building the tools that allow them to readily convert their vision to an online course ready for thousands of students.

    + +

    Requirements:

    +
      +
    • Real-world experience with Python or other dynamic development languages.
    • +
    • Able to code front to back, including HTML, CSS, Javascript, Django, Python
    • +
    • You must be committed to an agile development practices, in Scrum or Kanban
    • +
    • Demonstrated skills in building Service based architecture
    • +
    • Test Driven Development
    • +
    • Committed to Documentation best practices so your code can be consumed in an open source environment
    • +
    • Contributor to or consumer of Open Source Frameworks
    • +
    • BS in Computer Science from top-tier institution
    • +
    • Acknowledged by peers as a technology leader
    • +
    + +

    If you are interested in this position, please send an email to jobs@edx.org.

    +
    +
    +

    Positions

    How to Apply

    -

    E-mail your resume, coverletter and any other materials to jobs@edx.org

    +

    E-mail your resume, cover letter and any other materials to jobs@edx.org

    Our Location

    11 Cambridge Center
    - Cambridge, MA 02142

    + Cambridge, MA 02142

    diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html new file mode 100644 index 0000000000..2028d3c320 --- /dev/null +++ b/lms/templates/videoalpha.html @@ -0,0 +1,43 @@ +% if display_name is not UNDEFINED and display_name is not None: +

    ${display_name}

    +% endif + +%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: +
    +%else: +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +%endif + +% if sources.get('main'): +
    +

    Download video here.

    +
    +% endif + +% if track: +
    +

    Download subtitles here.

    +
    +% endif diff --git a/lms/urls.py b/lms/urls.py index b25c4d259e..fc42577085 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -320,10 +320,6 @@ if settings.COURSEWARE_ENABLED: 'courseware.views.static_tab', name="static_tab"), ) -if settings.QUICKEDIT: - urlpatterns += (url(r'^quickedit/(?P[^/]*)$', 'dogfood.views.quickedit'),) - urlpatterns += (url(r'^dogfood/(?P[^/]*)$', 'dogfood.views.df_capa_problem'),) - if settings.ENABLE_JASMINE: urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),) @@ -361,6 +357,12 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): url(r'^event_logs/(?P.+)$', 'track.views.view_tracking_log'), ) +# FoldIt views +urlpatterns += ( + # The path is hardcoded into their app... + url(r'^comm/foldit_ops', 'foldit.views.foldit_ops', name="foldit_ops"), +) + urlpatterns = patterns(*urlpatterns) if settings.DEBUG: diff --git a/requirements.txt b/requirements.txt index 0faf2e3ba5..7bfaa11bc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,6 @@ django_nose nosexcover==1.0.7 rednose==0.3.3 GitPython==0.3.2.RC1 -django-override-settings==1.2 mock==0.8.0 PyYAML==3.10 South==0.7.6