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/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index e3680018e3..b79d86b52f 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -214,12 +214,21 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') self.assertTrue(fs.exists('grading_policy.json')) + course = ms.get_item(location) # compare what's on disk compared to what we have in our course with fs.open('grading_policy.json','r') as grading_policy: - on_disk = loads(grading_policy.read()) - course = ms.get_item(location) + 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) @@ -333,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) @@ -369,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) @@ -392,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) @@ -420,7 +429,7 @@ class TemplateTestCase(ModuleStoreTestCase): # insert a bogus template in the store bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus') - source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Empty') + source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page') ms.clone_item(source_template_location, bogus_template_location) diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 925b2431b9..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'}) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 87a2943773..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,7 +131,8 @@ 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, 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff @@ -161,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, @@ -173,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, @@ -273,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 = [ @@ -730,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 @@ -796,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): @@ -819,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: @@ -831,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 @@ -865,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 @@ -1124,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) }) diff --git a/cms/envs/common.py b/cms/envs/common.py index 30aac6ea01..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 = ( 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 d81f577940..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"> diff --git a/cms/templates/export.html b/cms/templates/export.html index fcdd26458a..27045d82ce 100644 --- a/cms/templates/export.html +++ b/cms/templates/export.html @@ -2,10 +2,19 @@ <%namespace name='static' file='static_content.html'/> <%! from django.core.urlresolvers import reverse %> -<%block name="title">Export -<%block name="bodyclass">export +<%block name="title">Export Course +<%block name="bodyclass">is-signedin course tools export <%block name="content"> +
    +
    +
    + Tools +

    Course Export

    +
    +
    +
    +
    diff --git a/cms/templates/howitworks.html b/cms/templates/howitworks.html new file mode 100644 index 0000000000..1cf9b17710 --- /dev/null +++ b/cms/templates/howitworks.html @@ -0,0 +1,185 @@ +<%inherit file="base.html" /> +<%! from django.core.urlresolvers import reverse %> + +<%block name="title">Welcome +<%block name="bodyclass">not-signedin index howitworks + +<%block name="content"> + +
    +
    +
    +

    Welcome to

    +

    Studio helps manage your courses online, so you can focus on teaching them

    +
    +
    +
    + +
    +
    +
    +

    Studio's Many Features

    +
    + +
      +
    1. +
      + + Studio Helps You Keep Your Courses Organized +
      Studio Helps You Keep Your Courses Organized
      + + + +
      +
      + +
      +

      Keeping Your Course Organized

      +

      The backbone of your course is how it is organized. Studio offers an Outline editor, providing a simple hierarchy and easy drag and drop to help you and your students stay organized.

      + +
        +
      • +

        Simple Organization For Content

        +

        Studio uses a simple hierarchy of sections and subsections to organize your content.

        +
      • + +
      • +

        Change Your Mind Anytime

        +

        Draft your outline and build content anywhere. Simple drag and drop tools let your reorganize quickly.

        +
      • + +
      • +

        Go A Week Or A Semester At A Time

        +

        Build and release sections to your students incrementally. You don't have to have it all done at once.

        +
      • +
      +
      +
    2. + +
    3. +
      + + Learning is More than Just Lectures +
      Learning is More than Just Lectures
      + + + +
      +
      + +
      +

      Learning is More than Just Lectures

      +

      Studio lets you weave your content together in a way that reinforces learning — short video lectures interleaved with exercises and more. Insert videos and author a wide variety of exercise types with just a few clicks.

      + +
        +
      • +

        Create Learning Pathways

        +

        Help your students understand a small interactive piece at a time with multimedia, HTML, and exercises.

        +
      • + +
      • +

        Work Visually, Organize Quickly

        +

        Work visually and see exactly what your students will see. Reorganize all your content with drag and drop.

        +
      • + +
      • +

        A Broad Library of Problem Types

        +

        It's more than just multiple choice. Studio has nearly a dozen types of problems to challenge your learners.

        +
      • +
      +
      +
    4. + +
    5. +
      + + Studio Gives You Simple, Fast, and Incremental Publishing. With Friends. +
      Studio Gives You Simple, Fast, and Incremental Publishing. With Friends.
      + + + +
      +
      + +
      +

      Simple, Fast, and Incremental Publishing. With Friends.

      +

      Studio works like web applications you already know, yet understands how you build curriculum. Instant publishing to the web when you want it, incremental release when it makes sense. And with co-authors, you can have a whole team building a course, together.

      + +
        +
      • +

        Instant Changes

        +

        Caught a bug? No problem. When you want, your changes to live when you hit Save.

        +
      • + +
      • +

        Release-On Date Publishing

        +

        When you've finished a section, pick when you want it to go live and Studio takes care of the rest. Build your course incrementally.

        +
      • + +
      • +

        Work in Teams

        +

        Co-authors have full access to all the same authoring tools. Make your course better through a team effort.

        +
      • +
      +
      +
    6. +
    +
    +
    + +
    +
    +
    +

    Sign Up for Studio Today!

    +
    + + +
    +
    + +
    +

    Outlining Your Course

    +
    + +
    Simple two-level outline to organize your couse. Drag and drop, and see your course at a glance.
    +
    + + + + close modal + +
    + +
    +

    More than Just Lectures

    +
    + +
    Quickly create videos, text snippets, inline discussions, and a variety of problem types.
    +
    + + + + close modal + +
    + +
    +

    Publishing on Date

    +
    + +
    Simply set the date of a section or subsection, and Studio will publish it to your students for you.
    +
    + + + + close modal + +
    + \ No newline at end of file diff --git a/cms/templates/import.html b/cms/templates/import.html index e4f8019714..b0a9f04903 100644 --- a/cms/templates/import.html +++ b/cms/templates/import.html @@ -2,10 +2,19 @@ <%namespace name='static' file='static_content.html'/> <%! from django.core.urlresolvers import reverse %> -<%block name="title">Import -<%block name="bodyclass">import +<%block name="title">Import Course +<%block name="bodyclass">is-signedin course tools import <%block name="content"> +
    +
    +
    + Tools +

    Course Import

    +
    +
    +
    +
    diff --git a/cms/templates/index.html b/cms/templates/index.html index 45c4edc176..fdb46612a0 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -1,6 +1,7 @@ <%inherit file="base.html" /> -<%block name="bodyclass">index + <%block name="title">Courses +<%block name="bodyclass">is-signedin index dashboard <%block name="header_extras"> - - + \ No newline at end of file diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 99ac279bfb..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.

    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 c96d5686fd..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/lms/djangoapps/terrain/__init__.py b/common/djangoapps/terrain/__init__.py similarity index 100% rename from lms/djangoapps/terrain/__init__.py rename to common/djangoapps/terrain/__init__.py diff --git a/lms/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py similarity index 87% rename from lms/djangoapps/terrain/browser.py rename to common/djangoapps/terrain/browser.py index e1925bde0b..8c2a8ba7a5 100644 --- a/lms/djangoapps/terrain/browser.py +++ b/common/djangoapps/terrain/browser.py @@ -11,8 +11,9 @@ from django.core.management import call_command @before.harvest def initial_setup(server): - # Launch firefox + # Launch the browser app (choose one of these below) world.browser = Browser('chrome') + # world.browser = Browser('firefox') @before.each_scenario diff --git a/lms/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py similarity index 100% rename from lms/djangoapps/terrain/factories.py rename to common/djangoapps/terrain/factories.py diff --git a/lms/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py similarity index 100% rename from lms/djangoapps/terrain/steps.py rename to common/djangoapps/terrain/steps.py diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index cbcd13b02b..a1a4e6b65e 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -633,9 +633,11 @@ class MultipleChoiceResponse(LoncapaResponse): # define correct choices (after calling secondary setup) xml = self.xml cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id')) + # contextualize correct attribute and then select ones for which # correct = "true" - self.correct_choices = [contextualize_text(choice.get('name'), self.context) + self.correct_choices = [ + contextualize_text(choice.get('name'), self.context) for choice in cxml if contextualize_text(choice.get('correct'), self.context) == "true"] diff --git a/common/lib/capa/capa/templates/codeinput.html b/common/lib/capa/capa/templates/codeinput.html index 5c2ff2aca5..eb8cad0d70 100644 --- a/common/lib/capa/capa/templates/codeinput.html +++ b/common/lib/capa/capa/templates/codeinput.html @@ -50,6 +50,7 @@ }, smartIndent: false }); + $("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))}); });
    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/xmodule/setup.py b/common/lib/xmodule/setup.py index 117b0bf43d..835085d8ea 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -37,6 +37,7 @@ setup( "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", @@ -45,7 +46,8 @@ setup( "custom_tag_template = xmodule.raw_module:RawDescriptor", "about = xmodule.html_module:AboutDescriptor", "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", - "annotatable = xmodule.annotatable_module:AnnotatableDescriptor" - ] + "annotatable = xmodule.annotatable_module:AnnotatableDescriptor", + "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/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/combined_open_ended_modulev1.py index 8bd7df86c1..ce5d55d7b7 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_modulev1.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_modulev1.py @@ -8,6 +8,7 @@ from lxml.html import rewrite_links from path import path import os import sys +import re from pkg_resources import resource_string @@ -21,7 +22,7 @@ from .xml_module import XmlDescriptor from xmodule.modulestore import Location import self_assessment_module import open_ended_module -from combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError +from combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError, GRADER_TYPE_IMAGE_DICT, HUMAN_GRADER_TYPE, LEGEND_LIST from .stringify import stringify_children import dateutil import dateutil.parser @@ -55,7 +56,7 @@ TRUE_DICT = ["True", True, "TRUE", "true"] HUMAN_TASK_TYPE = { 'selfassessment' : "Self Assessment", - 'openended' : "External Grader", + 'openended' : "edX Assessment", } class CombinedOpenEndedV1Module(): @@ -191,9 +192,9 @@ class CombinedOpenEndedV1Module(): # completion (doesn't matter if you self-assessed correct/incorrect). self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) - rubric_renderer = CombinedOpenEndedRubric(system, True) + self.rubric_renderer = CombinedOpenEndedRubric(system, True) rubric_string = stringify_children(definition['rubric']) - rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED, self._max_score) + self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED, self._max_score) #Static data is passed to the child modules to render self.static_data = { @@ -354,9 +355,10 @@ class CombinedOpenEndedV1Module(): 'state': self.state, 'task_count': len(self.task_xml), 'task_number': self.current_task_number + 1, - 'status': self.get_status(), + 'status': self.get_status(False), 'display_name': self.display_name, 'accept_file_upload': self.accept_file_upload, + 'legend_list' : LEGEND_LIST, } return context @@ -431,6 +433,9 @@ class CombinedOpenEndedV1Module(): last_score = task.latest_score() last_post_assessment = task.latest_post_assessment(self.system) last_post_feedback = "" + feedback_dicts = [{}] + grader_ids = [0] + submission_ids = [0] if task_type == "openended": last_post_assessment = task.latest_post_assessment(self.system, short_feedback=False, join_feedback=False) if isinstance(last_post_assessment, list): @@ -441,6 +446,18 @@ class CombinedOpenEndedV1Module(): else: last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment) last_post_assessment = last_post_evaluation + rubric_data = task._parse_score_msg(task.history[-1].get('post_assessment', ""), self.system) + rubric_scores = rubric_data['rubric_scores'] + grader_types = rubric_data['grader_types'] + feedback_items = rubric_data['feedback_items'] + feedback_dicts = rubric_data['feedback_dicts'] + grader_ids = rubric_data['grader_ids'] + submission_ids = rubric_data['submission_ids'] + elif task_type== "selfassessment": + rubric_scores = last_post_assessment + grader_types = ['SA'] + feedback_items = [''] + last_post_assessment = "" last_correctness = task.is_last_response_correct() max_score = task.max_score() state = task.state @@ -453,6 +470,16 @@ class CombinedOpenEndedV1Module(): human_state = task.HUMAN_NAMES[state] else: human_state = state + if len(grader_types)>0: + grader_type = grader_types[0] + else: + grader_type = "IN" + + if grader_type in HUMAN_GRADER_TYPE: + human_grader_name = HUMAN_GRADER_TYPE[grader_type] + else: + human_grader_name = grader_type + last_response_dict = { 'response': last_response, 'score': last_score, @@ -465,8 +492,15 @@ class CombinedOpenEndedV1Module(): 'correct': last_correctness, 'min_score_to_attempt': min_score_to_attempt, 'max_score_to_attempt': max_score_to_attempt, + 'rubric_scores' : rubric_scores, + 'grader_types' : grader_types, + 'feedback_items' : feedback_items, + 'grader_type' : grader_type, + 'human_grader_type' : human_grader_name, + 'feedback_dicts' : feedback_dicts, + 'grader_ids' : grader_ids, + 'submission_ids' : submission_ids, } - return last_response_dict def update_task_states(self): @@ -502,19 +536,95 @@ class CombinedOpenEndedV1Module(): pass return return_html + def get_rubric(self, get): + """ + Gets the results of a given grader via ajax. + Input: AJAX get dictionary + Output: Dictionary to be rendered via ajax that contains the result html. + """ + all_responses = [] + loop_up_to_task = self.current_task_number+1 + for i in xrange(0,loop_up_to_task): + all_responses.append(self.get_last_response(i)) + rubric_scores = [all_responses[i]['rubric_scores'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['rubric_scores'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()] + grader_types = [all_responses[i]['grader_types'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['grader_types'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()] + feedback_items = [all_responses[i]['feedback_items'] for i in xrange(0,len(all_responses)) if len(all_responses[i]['feedback_items'])>0 and all_responses[i]['grader_types'][0] in HUMAN_GRADER_TYPE.keys()] + rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']), rubric_scores, + grader_types, feedback_items) + + response_dict = all_responses[-1] + context = { + 'results': rubric_html, + 'task_name' : 'Scored Rubric', + 'class_name' : 'combined-rubric-container' + } + html = self.system.render_template('combined_open_ended_results.html', context) + return {'html': html, 'success': True} + + def get_legend(self, get): + """ + Gets the results of a given grader via ajax. + Input: AJAX get dictionary + Output: Dictionary to be rendered via ajax that contains the result html. + """ + context = { + 'legend_list' : LEGEND_LIST, + } + html = self.system.render_template('combined_open_ended_legend.html', context) + return {'html': html, 'success': True} + def get_results(self, get): """ Gets the results of a given grader via ajax. Input: AJAX get dictionary Output: Dictionary to be rendered via ajax that contains the result html. """ - task_number = int(get['task_number']) self.update_task_states() - response_dict = self.get_last_response(task_number) - context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1} + loop_up_to_task = self.current_task_number+1 + all_responses =[] + for i in xrange(0,loop_up_to_task): + all_responses.append(self.get_last_response(i)) + context_list = [] + for ri in all_responses: + for i in xrange(0,len(ri['rubric_scores'])): + feedback = ri['feedback_dicts'][i].get('feedback','') + rubric_data = self.rubric_renderer.render_rubric(stringify_children(self.static_data['rubric']), ri['rubric_scores'][i]) + if rubric_data['success']: + rubric_html = rubric_data['html'] + else: + rubric_html = '' + context = { + 'rubric_html': rubric_html, + 'grader_type': ri['grader_type'], + 'feedback' : feedback, + 'grader_id' : ri['grader_ids'][i], + 'submission_id' : ri['submission_ids'][i], + } + context_list.append(context) + feedback_table = self.system.render_template('open_ended_result_table.html', { + 'context_list' : context_list, + 'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT, + 'human_grader_types' : HUMAN_GRADER_TYPE, + 'rows': 50, + 'cols': 50, + }) + context = { + 'results': feedback_table, + 'task_name' : "Feedback", + 'class_name' : "result-container", + } html = self.system.render_template('combined_open_ended_results.html', context) return {'html': html, 'success': True} + def get_status_ajax(self, get): + """ + Gets the results of a given grader via ajax. + Input: AJAX get dictionary + Output: Dictionary to be rendered via ajax that contains the result html. + """ + html = self.get_status(True) + return {'html': html, 'success': True} + def handle_ajax(self, dispatch, get): """ This is called by courseware.module_render, to handle an AJAX call. @@ -529,7 +639,10 @@ class CombinedOpenEndedV1Module(): handlers = { 'next_problem': self.next_problem, 'reset': self.reset, - 'get_results': self.get_results + 'get_results': self.get_results, + 'get_combined_rubric': self.get_rubric, + 'get_status' : self.get_status_ajax, + 'get_legend' : self.get_legend, } if dispatch not in handlers: @@ -593,7 +706,7 @@ class CombinedOpenEndedV1Module(): return json.dumps(state) - def get_status(self): + def get_status(self, render_via_ajax): """ Gets the status panel to be displayed at the top right. Input: None @@ -604,7 +717,13 @@ class CombinedOpenEndedV1Module(): task_data = self.get_last_response(i) task_data.update({'task_number': i + 1}) status.append(task_data) - context = {'status_list': status} + + context = { + 'status_list': status, + 'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT, + 'legend_list' : LEGEND_LIST, + 'render_via_ajax' : render_via_ajax, + } status_html = self.system.render_template("combined_open_ended_status.html", context) return status_html diff --git a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py index 689103a86a..7c00c5f029 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_rubric.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_rubric.py @@ -3,6 +3,27 @@ from lxml import etree log = logging.getLogger(__name__) +GRADER_TYPE_IMAGE_DICT = { + '8B' : '/static/images/random_grading_icon.png', + 'SA' : '/static/images/self_assessment_icon.png', + 'PE' : '/static/images/peer_grading_icon.png', + 'ML' : '/static/images/ml_grading_icon.png', + 'IN' : '/static/images/peer_grading_icon.png', + 'BC' : '/static/images/ml_grading_icon.png', + } + +HUMAN_GRADER_TYPE = { + '8B' : 'Magic-8-Ball-Assessment', + 'SA' : 'Self-Assessment', + 'PE' : 'Peer-Assessment', + 'IN' : 'Instructor-Assessment', + 'ML' : 'AI-Assessment', + 'BC' : 'AI-Assessment', + } + +DO_NOT_DISPLAY = ['BC', 'IN'] + +LEGEND_LIST = [{'name' : HUMAN_GRADER_TYPE[k], 'image' : GRADER_TYPE_IMAGE_DICT[k]} for k in GRADER_TYPE_IMAGE_DICT.keys() if k not in DO_NOT_DISPLAY ] class RubricParsingError(Exception): def __init__(self, msg): @@ -16,7 +37,7 @@ class CombinedOpenEndedRubric(object): self.view_only = view_only self.system = system - def render_rubric(self, rubric_xml): + def render_rubric(self, rubric_xml, score_list = None): ''' render_rubric: takes in an xml string and outputs the corresponding html for that xml, given the type of rubric we're generating @@ -29,22 +50,36 @@ class CombinedOpenEndedRubric(object): success = False try: rubric_categories = self.extract_categories(rubric_xml) + if score_list and len(score_list)==len(rubric_categories): + for i in xrange(0,len(rubric_categories)): + category = rubric_categories[i] + for j in xrange(0,len(category['options'])): + if score_list[i]==j: + rubric_categories[i]['options'][j]['selected'] = True + rubric_scores = [cat['score'] for cat in rubric_categories] max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories) max_score = max(max_scores) - html = self.system.render_template('open_ended_rubric.html', + rubric_template = 'open_ended_rubric.html' + if self.view_only: + rubric_template = 'open_ended_view_only_rubric.html' + html = self.system.render_template(rubric_template, {'categories': rubric_categories, 'has_score': self.has_score, 'view_only': self.view_only, - 'max_score': max_score}) + 'max_score': max_score, + 'combined_rubric' : False + }) success = True except: error_message = "[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml) log.error(error_message) raise RubricParsingError(error_message) - return success, html + return {'success' : success, 'html' : html, 'rubric_scores' : rubric_scores} def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score): - success, rubric_feedback = self.render_rubric(rubric_string) + rubric_dict = self.render_rubric(rubric_string) + success = rubric_dict['success'] + rubric_feedback = rubric_dict['html'] if not success: error_message = "Could not parse rubric : {0} for location {1}".format(rubric_string, location.url()) log.error(error_message) @@ -149,7 +184,33 @@ class CombinedOpenEndedRubric(object): options = sorted(options, key=lambda option: option['points']) CombinedOpenEndedRubric.validate_options(options) - return {'description': description, 'options': options} + return {'description': description, 'options': options, 'score' : score} + + def render_combined_rubric(self,rubric_xml,scores,score_types,feedback_types): + success, score_tuples = CombinedOpenEndedRubric.reformat_scores_for_rendering(scores,score_types,feedback_types) + rubric_categories = self.extract_categories(rubric_xml) + max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories) + max_score = max(max_scores) + for i in xrange(0,len(rubric_categories)): + category = rubric_categories[i] + for j in xrange(0,len(category['options'])): + rubric_categories[i]['options'][j]['grader_types'] = [] + for tuple in score_tuples: + if tuple[1] == i and tuple[2] ==j: + for grader_type in tuple[3]: + rubric_categories[i]['options'][j]['grader_types'].append(grader_type) + + log.debug(rubric_categories) + html = self.system.render_template('open_ended_combined_rubric.html', + {'categories': rubric_categories, + 'has_score': True, + 'view_only': True, + 'max_score': max_score, + 'combined_rubric' : True, + 'grader_type_image_dict' : GRADER_TYPE_IMAGE_DICT, + 'human_grader_types' : HUMAN_GRADER_TYPE, + }) + return html @staticmethod @@ -167,3 +228,79 @@ class CombinedOpenEndedRubric(object): raise RubricParsingError("[extract_category]: found duplicate point values between two different options") else: prev = option['points'] + + @staticmethod + def reformat_scores_for_rendering(scores, score_types, feedback_types): + """ + Takes in a list of rubric scores, the types of those scores, and feedback associated with them + Outputs a reformatted list of score tuples (count, rubric category, rubric score, [graders that gave this score], [feedback types]) + @param scores: + @param score_types: + @param feedback_types: + @return: + """ + success = False + if len(scores)==0: + log.error("Score length is 0.") + return success, "" + + if len(scores) != len(score_types) or len(feedback_types) != len(scores): + log.error("Length mismatches.") + return success, "" + + score_lists = [] + score_type_list = [] + feedback_type_list = [] + for i in xrange(0,len(scores)): + score_cont_list = scores[i] + for j in xrange(0,len(score_cont_list)): + score_list = score_cont_list[j] + score_lists.append(score_list) + score_type_list.append(score_types[i][j]) + feedback_type_list.append(feedback_types[i][j]) + + score_list_len = len(score_lists[0]) + for i in xrange(0,len(score_lists)): + score_list = score_lists[i] + if len(score_list)!=score_list_len: + return success, "" + + score_tuples = [] + for i in xrange(0,len(score_lists)): + for j in xrange(0,len(score_lists[i])): + tuple = [1,j,score_lists[i][j],[],[]] + score_tuples, tup_ind = CombinedOpenEndedRubric.check_for_tuple_matches(score_tuples,tuple) + score_tuples[tup_ind][0] += 1 + score_tuples[tup_ind][3].append(score_type_list[i]) + score_tuples[tup_ind][4].append(feedback_type_list[i]) + + success = True + return success, score_tuples + + @staticmethod + def check_for_tuple_matches(tuples, tuple): + """ + Checks to see if a tuple in a list of tuples is a match for tuple. + If not match, creates a new tuple matching tuple. + @param tuples: list of tuples + @param tuple: tuples to match + @return: a new list of tuples, and the index of the tuple that matches tuple + """ + category = tuple[1] + score = tuple[2] + tup_ind = -1 + for t in xrange(0,len(tuples)): + if tuples[t][1] == category and tuples[t][2] == score: + tup_ind = t + break + + if tup_ind == -1: + tuples.append([0,category,score,[],[]]) + tup_ind = len(tuples)-1 + return tuples, tup_ind + + + + + + diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss index 8d921f828b..20700ab092 100644 --- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss +++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss @@ -24,14 +24,11 @@ section.combined-open-ended { @include clearfix; .status-container { - float:right; - width:40%; + padding-bottom: 5px; } .item-container { - float:left; - width: 53%; - padding-bottom: 50px; + padding-bottom: 10px; } .result-container @@ -46,14 +43,26 @@ section.combined-open-ended { } } +section.legend-container { + .legenditem { + background-color : #d4d4d4; + font-size: .9em; + padding: 2px; + display: inline; + width: 20%; + } + margin-bottom: 5px; +} + section.combined-open-ended-status { .statusitem { - background-color: #FAFAFA; color: #2C2C2C; - font-family: monospace; - font-size: 1em; - padding: 10px; + background-color : #d4d4d4; + font-size: .9em; + padding: 2px; + display: inline; + width: 20%; .show-results { margin-top: .3em; text-align:right; @@ -61,12 +70,12 @@ section.combined-open-ended-status { .show-results-button { font: 1em monospace; } - } + } .statusitem-current { - background-color: #d4d4d4; + background-color: #B2B2B2; color: #222; - } + } span { &.unanswered { @@ -98,8 +107,29 @@ section.combined-open-ended-status { } } -div.result-container { +div.combined-rubric-container { + ul.rubric-list{ + list-style-type: none; + padding:0; + margin:0; + li { + &.rubric-list-item{ + margin-bottom: 2px; + padding: 0px; + } + } + } + span.rubric-category { + font-size: .9em; + } + padding-bottom: 5px; + padding-top: 10px; +} + +div.result-container { + padding-top: 10px; + padding-bottom: 5px; .evaluation { p { @@ -113,9 +143,8 @@ div.result-container { } .evaluation-response { - margin-bottom: 10px; + margin-bottom: 2px; header { - text-align: right; a { font-size: .85em; } @@ -198,20 +227,6 @@ div.result-container { } } - .result-correct { - background: url('../images/correct-icon.png') left 20px no-repeat; - .result-actual-output { - color: #090; - } - } - - .result-incorrect { - background: url('../images/incorrect-icon.png') left 20px no-repeat; - .result-actual-output { - color: #B00; - } - } - .markup-text{ margin: 5px; padding: 20px 0px 15px 50px; @@ -229,6 +244,16 @@ div.result-container { } } } + .rubric-result-container { + .rubric-result { + font-size: .9em; + padding: 2px; + display: inline-table; + } + padding: 2px; + margin: 0px; + display : inline; + } } @@ -404,7 +429,7 @@ section.open-ended-child { div.short-form-response { background: #F6F6F6; border: 1px solid #ddd; - margin-bottom: 20px; + margin-bottom: 0px; overflow-y: auto; height: 200px; @include clearfix; @@ -478,6 +503,18 @@ section.open-ended-child { margin-left: .75rem; } + ul.rubric-list{ + list-style-type: none; + padding:0; + margin:0; + li { + &.rubric-list-item{ + margin-bottom: 0px; + padding: 0px; + } + } + } + ol { list-style: decimal outside none; margin-bottom: lh(); @@ -503,9 +540,8 @@ section.open-ended-child { } li { - line-height: 1.4em; - margin-bottom: lh(.5); - + margin-bottom: 0px; + padding: 0px; &:last-child { margin-bottom: 0; } 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/grading_service_module.py b/common/lib/xmodule/xmodule/grading_service_module.py index 10c6f16adb..9af28a72c5 100644 --- a/common/lib/xmodule/xmodule/grading_service_module.py +++ b/common/lib/xmodule/xmodule/grading_service_module.py @@ -114,7 +114,9 @@ class GradingService(object): if 'rubric' in response_json: rubric = response_json['rubric'] rubric_renderer = CombinedOpenEndedRubric(self.system, view_only) - success, rubric_html = rubric_renderer.render_rubric(rubric) + rubric_dict = rubric_renderer.render_rubric(rubric) + success = rubric_dict['success'] + rubric_html = rubric_dict['html'] response_json['rubric'] = rubric_html return response_json # if we can't parse the rubric into HTML, 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 ae63171ed4..fd0391450b 100644 --- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee @@ -4,11 +4,11 @@ class @Rubric # finds the scores for each rubric category @get_score_list: () => # find the number of categories: - num_categories = $('table.rubric tr').length + num_categories = $('.rubric-category').length score_lst = [] # get the score for each one - for i in [0..(num_categories-2)] + for i in [0..(num_categories-1)] score = $("input[name='score-selection-#{i}']:checked").val() score_lst.push(score) @@ -23,9 +23,8 @@ class @Rubric @check_complete: () -> # check to see whether or not any categories have not been scored - num_categories = $('table.rubric tr').length - # -2 because we want to skip the header - for i in [0..(num_categories-2)] + num_categories = $('.rubric-category').length + for i in [0..(num_categories-1)] score = $("input[name='score-selection-#{i}']:checked").val() if score == undefined return false @@ -52,22 +51,30 @@ class @CombinedOpenEnded @reset_button.click @reset @next_problem_button = @$('.next-step-button') @next_problem_button.click @next_problem + @status_container = @$('.status-elements') @show_results_button=@$('.show-results-button') @show_results_button.click @show_results + @question_header = @$('.question-header') + @question_header.click @collapse_question + # valid states: 'initial', 'assessing', 'post_assessment', 'done' Collapsible.setCollapsibles(@el) @submit_evaluation_button = $('.submit-evaluation-button') @submit_evaluation_button.click @message_post @results_container = $('.result-container') + @combined_rubric_container = $('.combined-rubric-container') + + @legend_container= $('.legend-container') + @show_legend_current() # Where to put the rubric once we load it @el = $(element).find('section.open-ended-child') @errors_area = @$('.error') @answer_area = @$('textarea.answer') - + @prompt_container = @$('.prompt') @rubric_wrapper = @$('.rubric-wrapper') @hint_wrapper = @$('.hint-wrapper') @message_wrapper = @$('.message-wrapper') @@ -82,11 +89,20 @@ class @CombinedOpenEnded @can_upload_files = false @open_ended_child= @$('.open-ended-child') + if @task_number>1 + @prompt_hide() + else if @task_number==1 and @child_state!='initial' + @prompt_hide() + @find_assessment_elements() @find_hint_elements() @rebind() + if @task_number>1 + @show_combined_rubric_current() + @show_results_current() + # locally scoped jquery. $: (selector) -> $(selector, @el) @@ -102,7 +118,7 @@ class @CombinedOpenEnded Collapsible.setCollapsibles(@results_container) show_results: (event) => - status_item = $(event.target).parent().parent() + status_item = $(event.target).parent() status_number = status_item.data('status-number') data = {'task_number' : status_number} $.postWithPrefix "#{@ajax_url}/get_results", data, (response) => @@ -115,6 +131,27 @@ class @CombinedOpenEnded else @gentle_alert response.error + show_combined_rubric_current: () => + data = {} + $.postWithPrefix "#{@ajax_url}/get_combined_rubric", data, (response) => + if response.success + @combined_rubric_container.after(response.html).remove() + @combined_rubric_container= $('div.combined_rubric_container') + + show_status_current: () => + data = {} + $.postWithPrefix "#{@ajax_url}/get_status", data, (response) => + if response.success + @status_container.after(response.html).remove() + @status_container= $('.status-elements') + + show_legend_current: () => + data = {} + $.postWithPrefix "#{@ajax_url}/get_legend", data, (response) => + if response.success + @legend_container.after(response.html).remove() + @legend_container= $('.legend-container') + message_post: (event)=> Logger.log 'message_post', @answers external_grader_message=$(event.target).parent().parent().parent() @@ -156,6 +193,11 @@ class @CombinedOpenEnded @next_problem_button.hide() @hide_file_upload() @hint_area.attr('disabled', false) + if @task_number>1 or @child_state!='initial' + @show_status_current() + + if @task_number==1 and @child_state=='assessing' + @prompt_hide() if @child_state == 'done' @rubric_wrapper.hide() if @child_type=="openended" @@ -257,7 +299,8 @@ class @CombinedOpenEnded event.preventDefault() if @child_state == 'assessing' && Rubric.check_complete() checked_assessment = Rubric.get_total_score() - data = {'assessment' : checked_assessment} + score_list = Rubric.get_score_list() + data = {'assessment' : checked_assessment, 'score_list' : score_list} $.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) => if response.success @child_state = response.state @@ -267,7 +310,6 @@ class @CombinedOpenEnded @find_hint_elements() else if @child_state == 'done' @rubric_wrapper.hide() - @message_wrapper.html(response.message_html) @rebind() else @@ -367,13 +409,13 @@ class @CombinedOpenEnded window.queuePollerID = window.setTimeout(@poll, 10000) setup_file_upload: => - if window.File and window.FileReader and window.FileList and window.Blob - if @accept_file_upload == "True" - @can_upload_files = true - @file_upload_area.html('') - @file_upload_area.show() - else - @gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.' + if @accept_file_upload == "True" + if window.File and window.FileReader and window.FileList and window.Blob + @can_upload_files = true + @file_upload_area.html('') + @file_upload_area.show() + else + @gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.' hide_file_upload: => if @accept_file_upload == "True" @@ -390,3 +432,26 @@ class @CombinedOpenEnded # wrap this so that it can be mocked reload: -> location.reload() + + collapse_question: () => + @prompt_container.slideToggle() + @prompt_container.toggleClass('open') + if @question_header.text() == "(Hide)" + new_text = "(Show)" + else + new_text = "(Hide)" + @question_header.text(new_text) + + prompt_show: () => + if @prompt_container.is(":hidden")==true + @prompt_container.slideToggle() + @prompt_container.toggleClass('open') + @question_header.text("(Hide)") + + prompt_hide: () => + if @prompt_container.is(":visible")==true + @prompt_container.slideToggle() + @prompt_container.toggleClass('open') + @question_header.text("(Show)") + + diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee index deeb82900b..5770238649 100644 --- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee +++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee @@ -180,12 +180,17 @@ class @PeerGradingProblem @content_panel = $('.content-panel') @grading_message = $('.grading-message') @grading_message.hide() + @question_header = $('.question-header') + @question_header.click @collapse_question @grading_wrapper =$('.grading-wrapper') @calibration_feedback_panel = $('.calibration-feedback') @interstitial_page = $('.interstitial-page') @interstitial_page.hide() + @calibration_interstitial_page = $('.calibration-interstitial-page') + @calibration_interstitial_page.hide() + @error_container = $('.error-container') @submission_key_input = $("input[name='submission-key']") @@ -201,7 +206,9 @@ class @PeerGradingProblem @action_button = $('.action-button') @calibration_feedback_button = $('.calibration-feedback-button') @interstitial_page_button = $('.interstitial-page-button') + @calibration_interstitial_page_button = $('.calibration-interstitial-page-button') @flag_student_checkbox = $('.flag-checkbox') + @collapse_question() Collapsible.setCollapsibles(@content_panel) @@ -210,12 +217,21 @@ class @PeerGradingProblem @calibration_feedback_button.click => @calibration_feedback_panel.hide() @grading_wrapper.show() + @gentle_alert "Calibration essay saved. Fetched the next essay." @is_calibrated_check() @interstitial_page_button.click => @interstitial_page.hide() @is_calibrated_check() + @calibration_interstitial_page_button.click => + @calibration_interstitial_page.hide() + @is_calibrated_check() + + @calibration_feedback_button.hide() + @calibration_feedback_panel.hide() + @error_container.hide() + @is_calibrated_check() @@ -233,6 +249,9 @@ class @PeerGradingProblem fetch_submission_essay: () => @backend.post('get_next_submission', {location: @location}, @render_submission) + gentle_alert: (msg) => + @grading_message.fadeIn() + @grading_message.html("

    " + msg + "

    ") construct_data: () -> data = @@ -273,6 +292,9 @@ class @PeerGradingProblem else if response.calibrated and @calibration == true @calibration = false @render_interstitial_page() + else if not response.calibrated and @calibration==null + @calibration=true + @render_calibration_interstitial_page() else @calibration = true @fetch_calibration_essay() @@ -296,7 +318,7 @@ class @PeerGradingProblem if response.success @is_calibrated_check() @grading_message.fadeIn() - @grading_message.html("

    Grade sent successfully.

    ") + @grading_message.html("

    Successfully saved your feedback. Fetched the next essay.

    ") else if response.error @render_error(response.error) @@ -308,6 +330,7 @@ class @PeerGradingProblem # check to see whether or not any categories have not been scored if Rubric.check_complete() # show button if we have scores for all categories + @grading_message.hide() @show_submit_button() @grade = Rubric.get_total_score() @@ -323,7 +346,7 @@ class @PeerGradingProblem if response.success # load in all the data - @submission_container.html("

    Training Essay

    ") + @submission_container.html("") @render_submission_data(response) # TODO: indicate that we're in calibration mode @calibration_panel.addClass('current-state') @@ -337,6 +360,7 @@ class @PeerGradingProblem @calibration_panel.find('.grading-text').hide() @grading_panel.find('.grading-text').hide() @flag_student_container.hide() + @feedback_area.val("") @submit_button.unbind('click') @submit_button.click @submit_calibration_essay @@ -350,7 +374,7 @@ class @PeerGradingProblem render_submission: (response) => if response.success @submit_button.hide() - @submission_container.html("

    Submitted Essay

    ") + @submission_container.html("") @render_submission_data(response) @calibration_panel.removeClass('current-state') @@ -364,6 +388,7 @@ class @PeerGradingProblem @calibration_panel.find('.grading-text').show() @grading_panel.find('.grading-text').show() @flag_student_container.show() + @feedback_area.val("") @submit_button.unbind('click') @submit_button.click @submit_grade @@ -408,18 +433,25 @@ class @PeerGradingProblem actual_score = parseInt(response.actual_score) if score == actual_score - calibration_wrapper.append("

    Congratulations! Your score matches the actual score!

    ") + calibration_wrapper.append("

    Your score matches the actual score!

    ") else - calibration_wrapper.append("

    Please try to understand the grading critera better to be more accurate next time.

    ") + calibration_wrapper.append("

    You may want to review the rubric again.

    ") # disable score selection and submission from the grading interface $("input[name='score-selection']").attr('disabled', true) @submit_button.hide() + @calibration_feedback_button.show() render_interstitial_page: () => @content_panel.hide() + @grading_message.hide() @interstitial_page.show() + render_calibration_interstitial_page: () => + @content_panel.hide() + @action_button.hide() + @calibration_interstitial_page.show() + render_error: (error_message) => @error_container.show() @calibration_feedback_panel.hide() @@ -433,3 +465,12 @@ class @PeerGradingProblem setup_score_selection: (max_score) => # And now hook up an event handler again $("input[class='score-selection']").change @graded_callback + + collapse_question: () => + @prompt_container.slideToggle() + @prompt_container.toggleClass('open') + if @question_header.text() == "(Hide)" + new_text = "(Show)" + else + new_text = "(Hide)" + @question_header.text(new_text) 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
    diff --git a/lms/templates/self_assessment_hint.html b/lms/templates/self_assessment_hint.html index 1adfc69e39..8c6eacba11 100644 --- a/lms/templates/self_assessment_hint.html +++ b/lms/templates/self_assessment_hint.html @@ -1,6 +1,6 @@
    - ${hint_prompt} + Please enter a hint below:
    diff --git a/lms/templates/self_assessment_prompt.html b/lms/templates/self_assessment_prompt.html index 364009b134..5347e23844 100644 --- a/lms/templates/self_assessment_prompt.html +++ b/lms/templates/self_assessment_prompt.html @@ -5,7 +5,7 @@ ${prompt}
    -

    Answer

    +

    Response

    @@ -14,9 +14,9 @@
    ${initial_rubric}
    -
    ${initial_hint}
    +
    -
    ${initial_message}
    +
    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 a203d468e7..fc42577085 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -357,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: