Merge in master (again).
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -77,3 +77,4 @@ Slater Victoroff <slater.r.victoroff@gmail.com>
|
||||
Peter Fogg <peter.p.fogg@gmail.com>
|
||||
Bethany LaPenta <lapentab@mit.edu>
|
||||
Renzo Lucioni <renzolucioni@gmail.com>
|
||||
Felix Sun <felixsun@mit.edu>
|
||||
|
||||
@@ -8,6 +8,22 @@ the top. Include a label indicating the component affected.
|
||||
Studio: Remove XML from the video component editor. All settings are
|
||||
moved to be edited as metadata.
|
||||
|
||||
XModule: Only write out assets files if the contents have changed.
|
||||
|
||||
XModule: Don't delete generated xmodule asset files when compiling (for
|
||||
instance, when XModule provides a coffeescript file, don't delete
|
||||
the associated javascript)
|
||||
|
||||
Studio: For courses running on edx.org (marketing site), disable fields in
|
||||
Course Settings that do not apply.
|
||||
|
||||
Common: Make asset watchers run as singletons (so they won't start if the
|
||||
watcher is already running in another shell).
|
||||
|
||||
Common: Use coffee directly when watching for coffeescript file changes.
|
||||
|
||||
Common: Make rake provide better error messages if packages are missing.
|
||||
|
||||
Common: Repairs development documentation generation by sphinx.
|
||||
|
||||
LMS: Problem rescoring. Added options on the Grades tab of the
|
||||
@@ -17,6 +33,8 @@ students' number of attempts to zero. Provides a list of background
|
||||
tasks that are currently running for the course, and an option to
|
||||
see a history of background tasks for a given problem.
|
||||
|
||||
LMS: Fixed the preferences scope for storing data in xmodules.
|
||||
|
||||
LMS: Forums. Added handling for case where discussion module can get `None` as
|
||||
value of lms.start in `lms/djangoapps/django_comment_client/utils.py`
|
||||
|
||||
|
||||
1
Gemfile
1
Gemfile
@@ -4,3 +4,4 @@ gem 'sass', '3.1.15'
|
||||
gem 'bourbon', '~> 1.3.6'
|
||||
gem 'colorize', '~> 0.5.8'
|
||||
gem 'launchy', '~> 2.1.2'
|
||||
gem 'sys-proctable', '~> 0.9.3'
|
||||
|
||||
@@ -31,11 +31,10 @@ def press_the_notification_button(step, name):
|
||||
|
||||
# Save was clicked if either the save notification bar is gone, or we have a error notification
|
||||
# overlaying it (expected in the case of typing Object into display_name).
|
||||
save_clicked = lambda : world.is_css_not_present('.is-shown.wrapper-notification-warning') or \
|
||||
world.is_css_present('.is-shown.wrapper-notification-error')
|
||||
save_clicked = lambda: world.is_css_not_present('.is-shown.wrapper-notification-warning') or\
|
||||
world.is_css_present('.is-shown.wrapper-notification-error')
|
||||
|
||||
assert_true(world.css_click(css, success_condition=save_clicked),
|
||||
'The save button was not clicked after 5 attempts.')
|
||||
assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.')
|
||||
|
||||
|
||||
@step(u'I edit the value of a policy key$')
|
||||
|
||||
53
cms/djangoapps/contentstore/features/grading.feature
Normal file
53
cms/djangoapps/contentstore/features/grading.feature
Normal file
@@ -0,0 +1,53 @@
|
||||
Feature: Course Grading
|
||||
As a course author, I want to be able to configure how my course is graded
|
||||
|
||||
Scenario: Users can add grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
When I add "1" new grade
|
||||
Then I see I now have "3" grades
|
||||
|
||||
Scenario: Users can only have up to 5 grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
When I add "6" new grades
|
||||
Then I see I now have "5" grades
|
||||
|
||||
#Cannot reliably make the delete button appear so using javascript instead
|
||||
Scenario: Users can delete grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
When I add "1" new grade
|
||||
And I delete a grade
|
||||
Then I see I now have "2" grades
|
||||
|
||||
Scenario: Users can move grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
When I move a grading section
|
||||
Then I see that the grade range has changed
|
||||
|
||||
Scenario: Users can modify Assignment types
|
||||
Given I have opened a new course in Studio
|
||||
And I have populated the course
|
||||
And I am viewing the grading settings
|
||||
When I change assignment type "Homework" to "New Type"
|
||||
And I go back to the main course page
|
||||
Then I do see the assignment name "New Type"
|
||||
And I do not see the assignment name "Homework"
|
||||
|
||||
Scenario: Users can delete Assignment types
|
||||
Given I have opened a new course in Studio
|
||||
And I have populated the course
|
||||
And I am viewing the grading settings
|
||||
When I delete the assignment type "Homework"
|
||||
And I go back to the main course page
|
||||
Then I do not see the assignment name "Homework"
|
||||
|
||||
Scenario: Users can add Assignment types
|
||||
Given I have opened a new course in Studio
|
||||
And I have populated the course
|
||||
And I am viewing the grading settings
|
||||
When I add a new assignment type "New Type"
|
||||
And I go back to the main course page
|
||||
Then I do see the assignment name "New Type"
|
||||
108
cms/djangoapps/contentstore/features/grading.py
Normal file
108
cms/djangoapps/contentstore/features/grading.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
|
||||
@step(u'I am viewing the grading settings')
|
||||
def view_grading_settings(step):
|
||||
world.click_course_settings()
|
||||
link_css = 'li.nav-course-settings-grading a'
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@step(u'I add "([^"]*)" new grade')
|
||||
def add_grade(step, many):
|
||||
grade_css = '.new-grade-button'
|
||||
for i in range(int(many)):
|
||||
world.css_click(grade_css)
|
||||
|
||||
|
||||
@step(u'I delete a grade')
|
||||
def delete_grade(step):
|
||||
#grade_css = 'li.grade-specific-bar > a.remove-button'
|
||||
#range_css = '.grade-specific-bar'
|
||||
#world.css_find(range_css)[1].mouseover()
|
||||
#world.css_click(grade_css)
|
||||
world.browser.execute_script('document.getElementsByClassName("remove-button")[0].click()')
|
||||
|
||||
|
||||
@step(u'I see I now have "([^"]*)" grades$')
|
||||
def view_grade_slider(step, how_many):
|
||||
grade_slider_css = '.grade-specific-bar'
|
||||
all_grades = world.css_find(grade_slider_css)
|
||||
assert len(all_grades) == int(how_many)
|
||||
|
||||
|
||||
@step(u'I move a grading section')
|
||||
def move_grade_slider(step):
|
||||
moveable_css = '.ui-resizable-e'
|
||||
f = world.css_find(moveable_css).first
|
||||
f.action_chains.drag_and_drop_by_offset(f._element, 100, 0).perform()
|
||||
|
||||
|
||||
@step(u'I see that the grade range has changed')
|
||||
def confirm_change(step):
|
||||
range_css = '.range'
|
||||
all_ranges = world.css_find(range_css)
|
||||
for i in range(len(all_ranges)):
|
||||
assert all_ranges[i].html != '0-50'
|
||||
|
||||
|
||||
@step(u'I change assignment type "([^"]*)" to "([^"]*)"$')
|
||||
def change_assignment_name(step, old_name, new_name):
|
||||
name_id = '#course-grading-assignment-name'
|
||||
index = get_type_index(old_name)
|
||||
f = world.css_find(name_id)[index]
|
||||
assert index != -1
|
||||
for count in range(len(old_name)):
|
||||
f._element.send_keys(Keys.END, Keys.BACK_SPACE)
|
||||
f._element.send_keys(new_name)
|
||||
|
||||
|
||||
@step(u'I go back to the main course page')
|
||||
def main_course_page(step):
|
||||
main_page_link_css = 'a[href="/MITx/999/course/Robot_Super_Course"]'
|
||||
world.css_click(main_page_link_css)
|
||||
|
||||
|
||||
@step(u'I do( not)? see the assignment name "([^"]*)"$')
|
||||
def see_assignment_name(step, do_not, name):
|
||||
assignment_menu_css = 'ul.menu > li > a'
|
||||
assignment_menu = world.css_find(assignment_menu_css)
|
||||
allnames = [item.html for item in assignment_menu]
|
||||
if do_not:
|
||||
assert not name in allnames
|
||||
else:
|
||||
assert name in allnames
|
||||
|
||||
|
||||
@step(u'I delete the assignment type "([^"]*)"$')
|
||||
def delete_assignment_type(step, to_delete):
|
||||
delete_css = '.remove-grading-data'
|
||||
world.css_click(delete_css, index=get_type_index(to_delete))
|
||||
|
||||
|
||||
@step(u'I add a new assignment type "([^"]*)"$')
|
||||
def add_assignment_type(step, new_name):
|
||||
add_button_css = '.add-grading-data'
|
||||
world.css_click(add_button_css)
|
||||
name_id = '#course-grading-assignment-name'
|
||||
f = world.css_find(name_id)[4]
|
||||
f._element.send_keys(new_name)
|
||||
|
||||
|
||||
@step(u'I have populated the course')
|
||||
def populate_course(step):
|
||||
step.given('I have added a new section')
|
||||
step.given('I have added a new subsection')
|
||||
|
||||
|
||||
def get_type_index(name):
|
||||
name_id = '#course-grading-assignment-name'
|
||||
f = world.css_find(name_id)
|
||||
for i in range(len(f)):
|
||||
if f[i].value == name:
|
||||
return i
|
||||
return -1
|
||||
@@ -1,11 +1,16 @@
|
||||
"""
|
||||
Tests for Studio Course Settings.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import copy
|
||||
import mock
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import UTC
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
|
||||
@@ -21,6 +26,9 @@ from xmodule.fields import Date
|
||||
|
||||
|
||||
class CourseTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Base class for test classes below.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
These tests need a user in the DB so that the django Test Client
|
||||
@@ -51,6 +59,9 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
|
||||
|
||||
class CourseDetailsTestCase(CourseTestCase):
|
||||
"""
|
||||
Tests the first course settings page (course dates, overview, etc.).
|
||||
"""
|
||||
def test_virgin_fetch(self):
|
||||
details = CourseDetails.fetch(self.course_location)
|
||||
self.assertEqual(details.course_location, self.course_location, "Location not copied into")
|
||||
@@ -81,9 +92,9 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
Test the encoder out of its original constrained purpose to see if it functions for general use
|
||||
"""
|
||||
details = {'location': Location(['tag', 'org', 'course', 'category', 'name']),
|
||||
'number': 1,
|
||||
'string': 'string',
|
||||
'datetime': datetime.datetime.now(UTC())}
|
||||
'number': 1,
|
||||
'string': 'string',
|
||||
'datetime': datetime.datetime.now(UTC())}
|
||||
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
|
||||
jsondetails = json.loads(jsondetails)
|
||||
|
||||
@@ -118,8 +129,50 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
jsondetails.effort, "After set effort"
|
||||
)
|
||||
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def test_marketing_site_fetch(self):
|
||||
settings_details_url = reverse('settings_details',
|
||||
kwargs={'org': self.course_location.org, 'name': self.course_location.name,
|
||||
'course': self.course_location.course})
|
||||
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
response = self.client.get(settings_details_url)
|
||||
self.assertContains(response, "Course Summary Page")
|
||||
self.assertContains(response, "course summary page will not be viewable")
|
||||
|
||||
self.assertContains(response, "Course Start Date")
|
||||
self.assertContains(response, "Course End Date")
|
||||
self.assertNotContains(response, "Enrollment Start Date")
|
||||
self.assertNotContains(response, "Enrollment End Date")
|
||||
self.assertContains(response, "not the dates shown on your course summary page")
|
||||
|
||||
self.assertNotContains(response, "Introducing Your Course")
|
||||
self.assertNotContains(response, "Requirements")
|
||||
|
||||
def test_regular_site_fetch(self):
|
||||
settings_details_url = reverse('settings_details',
|
||||
kwargs={'org': self.course_location.org, 'name': self.course_location.name,
|
||||
'course': self.course_location.course})
|
||||
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
|
||||
response = self.client.get(settings_details_url)
|
||||
self.assertContains(response, "Course Summary Page")
|
||||
self.assertNotContains(response, "course summary page will not be viewable")
|
||||
|
||||
self.assertContains(response, "Course Start Date")
|
||||
self.assertContains(response, "Course End Date")
|
||||
self.assertContains(response, "Enrollment Start Date")
|
||||
self.assertContains(response, "Enrollment End Date")
|
||||
self.assertNotContains(response, "not the dates shown on your course summary page")
|
||||
|
||||
self.assertContains(response, "Introducing Your Course")
|
||||
self.assertContains(response, "Requirements")
|
||||
|
||||
|
||||
class CourseDetailsViewTest(CourseTestCase):
|
||||
"""
|
||||
Tests for modifying content on the first course settings page (course dates, overview, etc.).
|
||||
"""
|
||||
def alter_field(self, url, details, field, val):
|
||||
setattr(details, field, val)
|
||||
# Need to partially serialize payload b/c the mock doesn't handle it correctly
|
||||
@@ -181,6 +234,9 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
|
||||
|
||||
class CourseGradingTest(CourseTestCase):
|
||||
"""
|
||||
Tests for the course settings grading page.
|
||||
"""
|
||||
def test_initial_grader(self):
|
||||
descriptor = get_modulestore(self.course_location).get_item(self.course_location)
|
||||
test_grader = CourseGradingModel(descriptor)
|
||||
@@ -256,6 +312,9 @@ class CourseGradingTest(CourseTestCase):
|
||||
|
||||
|
||||
class CourseMetadataEditingTest(CourseTestCase):
|
||||
"""
|
||||
Tests for CourseMetadata.
|
||||
"""
|
||||
def setUp(self):
|
||||
CourseTestCase.setUp(self)
|
||||
# add in the full class too
|
||||
|
||||
@@ -227,7 +227,8 @@ def get_course_settings(request, org, course, name):
|
||||
kwargs={"org": org,
|
||||
"course": course,
|
||||
"name": name,
|
||||
"section": "details"})
|
||||
"section": "details"}),
|
||||
'about_page_editable': not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +1,40 @@
|
||||
// studio - elements - system help
|
||||
// ====================
|
||||
|
||||
// notices - in-context: to be used as notices to users within the context of a form/action
|
||||
.notice-incontext {
|
||||
@extend .ui-well;
|
||||
@include border-radius(($baseline/10));
|
||||
|
||||
.title {
|
||||
@extend .t-title7;
|
||||
margin-bottom: ($baseline/4);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .t-copy-sub1;
|
||||
@include transition(opacity 0.25s ease-in-out 0);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
.copy {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// particular warnings around a workflow for something
|
||||
.notice-workflow {
|
||||
background: $yellow-l5;
|
||||
|
||||
.copy {
|
||||
color: $gray-d1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ body.course.settings {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
.message-status {
|
||||
display: none;
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@@ -52,6 +52,12 @@ body.course.settings {
|
||||
}
|
||||
}
|
||||
|
||||
// notices - used currently for edx mktg
|
||||
.notice-workflow {
|
||||
margin-top: ($baseline);
|
||||
}
|
||||
|
||||
|
||||
// in form - elements
|
||||
.group-settings {
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Schedule & Details Settings</%block>
|
||||
<%block name="bodyclass">is-signedin course schedule settings</%block>
|
||||
@@ -50,8 +52,8 @@ from contentstore import utils
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-subtitle">
|
||||
<h1 class="page-header">
|
||||
<small class="subtitle">Settings</small>
|
||||
<span class="sr">> </span>Schedule & Details
|
||||
<small class="subtitle">${_("Settings")}</small>
|
||||
<span class="sr">> </span>${_("Schedule & Details")}
|
||||
</h1>
|
||||
</header>
|
||||
</div>
|
||||
@@ -62,59 +64,68 @@ from contentstore import utils
|
||||
<form id="settings_details" class="settings-details" method="post" action="">
|
||||
<section class="group-settings basic">
|
||||
<header>
|
||||
<h2 class="title-2">Basic Information</h2>
|
||||
<span class="tip">The nuts and bolts of your course</span>
|
||||
<h2 class="title-2">${_("Basic Information")}</h2>
|
||||
<span class="tip">${_("The nuts and bolts of your course")}</span>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text is-not-editable" id="field-course-organization">
|
||||
<label for="course-organization">Organization</label>
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
|
||||
<label for="course-organization">${_("Organization")}</label>
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
|
||||
</li>
|
||||
|
||||
<li class="field text is-not-editable" id="field-course-number">
|
||||
<label for="course-number">Course Number</label>
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly>
|
||||
<label for="course-number">${_("Course Number")}</label>
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="short" id="course-number" value="[Course No.]" readonly>
|
||||
</li>
|
||||
|
||||
<li class="field text is-not-editable" id="field-course-name">
|
||||
<label for="course-name">Course Name</label>
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
|
||||
<label for="course-name">${_("Course Name")}</label>
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-name" value="[Course Name]" readonly />
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="note note-promotion note-promotion-courseURL has-actions">
|
||||
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3>
|
||||
<h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3>
|
||||
<div class="copy">
|
||||
<p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i> Invite your students</a>
|
||||
<a title="${_('Send a note to students via email')}" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
% if not about_page_editable:
|
||||
<div class="notice notice-incontext notice-workflow">
|
||||
<h3 class="title">${_("Promoting Your Course with edX")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings schedule">
|
||||
<header>
|
||||
<h2 class="title-2">Course Schedule</h2>
|
||||
<span class="tip">Important steps and segments of your course</span>
|
||||
<h2 class="title-2">${_('Course Schedule')}</h2>
|
||||
<span class="tip">${_('Dates that control when your course can be viewed.')}</span>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field-group field-group-course-start" id="course-start">
|
||||
<div class="field date" id="field-course-start-date">
|
||||
<label for="course-start-date">Course Start Date</label>
|
||||
<label for="course-start-date">${_("Course Start Date")}</label>
|
||||
<input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">First day the course begins</span>
|
||||
<span class="tip tip-stacked">${_("First day the course begins")}</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-course-start-time">
|
||||
<label for="course-start-time">Course Start Time</label>
|
||||
<label for="course-start-time">${_("Course Start Time")}</label>
|
||||
<input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
@@ -122,29 +133,30 @@ from contentstore import utils
|
||||
|
||||
<li class="field-group field-group-course-end" id="course-end">
|
||||
<div class="field date" id="field-course-end-date">
|
||||
<label for="course-end-date">Course End Date</label>
|
||||
<label for="course-end-date">${_("Course End Date")}</label>
|
||||
<input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Last day your course is active</span>
|
||||
<span class="tip tip-stacked">${_("Last day your course is active")}</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-course-end-time">
|
||||
<label for="course-end-time">Course End Time</label>
|
||||
<label for="course-end-time">${_("Course End Time")}</label>
|
||||
<input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
% if about_page_editable:
|
||||
<ol class="list-input">
|
||||
<li class="field-group field-group-enrollment-start" id="enrollment-start">
|
||||
<div class="field date" id="field-enrollment-start-date">
|
||||
<label for="course-enrollment-start-date">Enrollment Start Date</label>
|
||||
<label for="course-enrollment-start-date">${_("Enrollment Start Date")}</label>
|
||||
<input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">First day students can enroll</span>
|
||||
<span class="tip tip-stacked">${_("First day students can enroll")}</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-enrollment-start-time">
|
||||
<label for="course-enrollment-start-time">Enrollment Start Time</label>
|
||||
<label for="course-enrollment-start-time">${_("Enrollment Start Time")}</label>
|
||||
<input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
@@ -152,91 +164,106 @@ from contentstore import utils
|
||||
|
||||
<li class="field-group field-group-enrollment-end" id="enrollment-end">
|
||||
<div class="field date" id="field-enrollment-end-date">
|
||||
<label for="course-enrollment-end-date">Enrollment End Date</label>
|
||||
<label for="course-enrollment-end-date">${_("Enrollment End Date")}</label>
|
||||
<input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Last day students can enroll</span>
|
||||
<span class="tip tip-stacked">${_("Last day students can enroll")}</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-enrollment-end-time">
|
||||
<label for="course-enrollment-end-time">Enrollment End Time</label>
|
||||
<label for="course-enrollment-end-time">${_("Enrollment End Time")}</label>
|
||||
<input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
% endif
|
||||
|
||||
% if not about_page_editable:
|
||||
<div class="notice notice-incontext notice-workflow">
|
||||
<h3 class="title">${_("These Dates Are Not Used When Promoting Your Course")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_('These dates impact <strong>when your courseware can be viewed</strong>, but they are <strong>not the dates shown on your course summary page</strong>. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
<hr class="divide" />
|
||||
% if about_page_editable:
|
||||
<section class="group-settings marketing">
|
||||
<header>
|
||||
<h2 class="title-2">${_("Introducing Your Course")}</h2>
|
||||
<span class="tip">${_("Information for prospective students")}</span>
|
||||
</header>
|
||||
|
||||
<section class="group-settings marketing">
|
||||
<header>
|
||||
<h2 class="title-2">Introducing Your Course</h2>
|
||||
<span class="tip">Information for prospective students</span>
|
||||
</header>
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-overview">
|
||||
<label for="course-overview">${_("Course Overview")}</label>
|
||||
<textarea class="tinymce text-editor" id="course-overview"></textarea>
|
||||
<%def name='overview_text()'><%
|
||||
a_link_start = '<a class="link-courseURL" rel="external" href="'
|
||||
a_link_end = '">' + _("your course summary page") + '</a>'
|
||||
a_link = a_link_start + utils.get_lms_link_for_about_page(course_location) + a_link_end
|
||||
text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link
|
||||
%>${text}</%def>
|
||||
<span class="tip tip-stacked">${overview_text()}</span>
|
||||
</li>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-overview">
|
||||
<label for="course-overview">Course Overview</label>
|
||||
<textarea class="tinymce text-editor" id="course-overview"></textarea>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a> (formatted in HTML)</span>
|
||||
</li>
|
||||
<li class="field video" id="field-course-introduction-video">
|
||||
<label for="course-overview">${_("Course Introduction Video")}</label>
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span>${_("Delete Current Video")}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<li class="field video" id="field-course-introduction-video">
|
||||
<label for="course-overview">Course Introduction Video</label>
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Current Video</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" />
|
||||
<span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<div class="input">
|
||||
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Enter your YouTube video's ID (along with any restriction parameters)</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<hr class="divide" />
|
||||
|
||||
<hr class="divide" />
|
||||
<section class="group-settings requirements">
|
||||
<header>
|
||||
<h2 class="title-2">${_("Requirements")}</h2>
|
||||
<span class="tip">${_("Expectations of the students taking this course")}</span>
|
||||
</header>
|
||||
|
||||
<section class="group-settings requirements">
|
||||
<header>
|
||||
<h2 class="title-2">Requirements</h2>
|
||||
<span class="tip">Expectations of the students taking this course</span>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-effort">
|
||||
<label for="course-effort">Hours of Effort per Week</label>
|
||||
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
|
||||
<span class="tip tip-inline">Time spent on all course work</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-effort">
|
||||
<label for="course-effort">${_("Hours of Effort per Week")}</label>
|
||||
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
|
||||
<span class="tip tip-inline">${_("Time spent on all course work")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
% endif
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">How will these settings be used?</h3>
|
||||
<p>Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.</p>
|
||||
<h3 class="title-3">${_("How will these settings be used?")}</h3>
|
||||
<p>${_("Your course's schedule settings determine when students can enroll in and begin a course.")}</p>
|
||||
|
||||
<p>Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.</p>
|
||||
<p>${_("Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
<% ctx_loc = context_course.location %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<h3 class="title-3">Other Course Settings</h3>
|
||||
<h3 class="title-3">${_("Other Course Settings")}</h3>
|
||||
<nav class="nav-related">
|
||||
<ul>
|
||||
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li>
|
||||
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
|
||||
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li>
|
||||
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
|
||||
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li>
|
||||
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
% endif
|
||||
|
||||
@@ -49,7 +49,7 @@ def css_has_text(css_selector, text):
|
||||
|
||||
@world.absorb
|
||||
def css_find(css, wait_time=5):
|
||||
def is_visible(driver):
|
||||
def is_visible(_driver):
|
||||
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
|
||||
|
||||
world.browser.is_element_present_by_css(css, wait_time=wait_time)
|
||||
@@ -58,7 +58,7 @@ def css_find(css, wait_time=5):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True):
|
||||
def css_click(css_selector, index=0, attempts=5, success_condition=lambda: True):
|
||||
"""
|
||||
Perform a click on a CSS selector, retrying if it initially fails.
|
||||
|
||||
@@ -90,15 +90,15 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_click_at(css, x=10, y=10):
|
||||
def css_click_at(css, x_cord=10, y_cord=10):
|
||||
'''
|
||||
A method to click at x,y coordinates of the element
|
||||
rather than in the center of the element
|
||||
'''
|
||||
e = css_find(css).first
|
||||
e.action_chains.move_to_element_with_offset(e._element, x, y)
|
||||
e.action_chains.click()
|
||||
e.action_chains.perform()
|
||||
element = css_find(css).first
|
||||
element.action_chains.move_to_element_with_offset(element._element, x_cord, y_cord)
|
||||
element.action_chains.click()
|
||||
element.action_chains.perform()
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -143,7 +143,7 @@ def css_visible(css_selector):
|
||||
|
||||
@world.absorb
|
||||
def dialogs_closed():
|
||||
def are_dialogs_closed(driver):
|
||||
def are_dialogs_closed(_driver):
|
||||
'''
|
||||
Return True when no modal dialogs are visible
|
||||
'''
|
||||
@@ -154,12 +154,12 @@ def dialogs_closed():
|
||||
|
||||
@world.absorb
|
||||
def save_the_html(path='/tmp'):
|
||||
u = world.browser.url
|
||||
url = world.browser.url
|
||||
html = world.browser.html.encode('ascii', 'ignore')
|
||||
filename = '%s.html' % quote_plus(u)
|
||||
f = open('%s/%s' % (path, filename), 'w')
|
||||
f.write(html)
|
||||
f.close()
|
||||
filename = '%s.html' % quote_plus(url)
|
||||
file = open('%s/%s' % (path, filename), 'w')
|
||||
file.write(html)
|
||||
file.close()
|
||||
|
||||
|
||||
@world.absorb
|
||||
|
||||
@@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content
|
||||
that is defined by XModules and XModuleDescriptors (javascript and css)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
import os
|
||||
import errno
|
||||
@@ -15,6 +16,9 @@ from path import path
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def write_module_styles(output_root):
|
||||
return _write_styles('.xmodule_display', output_root, _list_modules())
|
||||
|
||||
@@ -121,18 +125,32 @@ def _write_js(output_root, classes):
|
||||
type=filetype)
|
||||
contents[filename] = fragment
|
||||
|
||||
_write_files(output_root, contents)
|
||||
_write_files(output_root, contents, {'.coffee': '.js'})
|
||||
|
||||
return [output_root / filename for filename in contents.keys()]
|
||||
|
||||
|
||||
def _write_files(output_root, contents):
|
||||
def _write_files(output_root, contents, generated_suffix_map=None):
|
||||
_ensure_dir(output_root)
|
||||
for extra_file in set(output_root.files()) - set(contents.keys()):
|
||||
extra_file.remove_p()
|
||||
to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys())
|
||||
|
||||
if generated_suffix_map:
|
||||
for output_file in contents.keys():
|
||||
for suffix, generated_suffix in generated_suffix_map.items():
|
||||
if output_file.endswith(suffix):
|
||||
to_delete.discard(output_file.replace(suffix, generated_suffix))
|
||||
|
||||
for extra_file in to_delete:
|
||||
(output_root / extra_file).remove_p()
|
||||
|
||||
for filename, file_content in contents.iteritems():
|
||||
(output_root / filename).write_bytes(file_content)
|
||||
output_file = output_root / filename
|
||||
|
||||
if not output_file.isfile() or output_file.read_md5() != hashlib.md5(file_content).digest():
|
||||
LOG.debug("Writing %s", output_file)
|
||||
output_file.write_bytes(file_content)
|
||||
else:
|
||||
LOG.debug("%s unchanged, skipping", output_file)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -189,3 +189,10 @@
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// UI archetypes - well
|
||||
.ui-well {
|
||||
@include box-shadow(inset 0 1px 2px 1px $shadow-l1);
|
||||
padding: ($baseline*0.75);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,25 @@ To get a full list of available rake tasks, use:
|
||||
|
||||
rake -T
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Reference Error: XModule is not defined (javascript)
|
||||
This means that the javascript defining an xmodule hasn't loaded correctly. There are a number
|
||||
of different things that could be causing this:
|
||||
|
||||
1. See `Error: watch EMFILE`
|
||||
|
||||
#### Error: watch EMFILE (coffee)
|
||||
When running a development server, we also start a watcher process alongside to recompile coffeescript
|
||||
and sass as changes are made. On Mac OSX systems, the coffee watcher process takes more file handles
|
||||
than are allowed by default. This will result in `EMFILE` errors when coffeescript is running, and
|
||||
will prevent javascript from compiling, leading to the error 'XModule is not defined'
|
||||
|
||||
To work around this issue, we use `Process::setrlimit` to set the number of allowed open files.
|
||||
Coffee watches both directories and files, so you will need to set this fairly high (anecdotally,
|
||||
8000 seems to do the trick on OSX 10.7.5, 10.8.3, and 10.8.4)
|
||||
|
||||
|
||||
## Running Tests
|
||||
|
||||
See `testing.md` for instructions on running the test suite.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
Feature: Video component
|
||||
As a student, I want to view course videos in LMS.
|
||||
|
||||
Scenario: Autoplay is enabled in LMS
|
||||
Given the course has a Video component
|
||||
Then when I view the video it has autoplay enabled
|
||||
Scenario: Autoplay is enabled in LMS for a Video component
|
||||
Given the course has a Video component
|
||||
Then when I view the video it has autoplay enabled
|
||||
|
||||
Scenario: Autoplay is enabled in the LMS for a VideoAlpha component
|
||||
Given the course has a VideoAlpha component
|
||||
Then when I view the video it has autoplay enabled
|
||||
|
||||
@@ -27,8 +27,30 @@ def view_video(_step):
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
@step('the course has a VideoAlpha component')
|
||||
def view_videoalpha(step):
|
||||
coursename = TEST_COURSE_NAME.replace(' ', '_')
|
||||
i_am_registered_for_the_course(step, coursename)
|
||||
|
||||
# Make sure we have a videoalpha
|
||||
add_videoalpha_to_course(coursename)
|
||||
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
|
||||
section_name = chapter_name
|
||||
url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' %
|
||||
(chapter_name, section_name))
|
||||
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
def add_video_to_course(course):
|
||||
template_name = 'i4x://edx/templates/video/default'
|
||||
world.ItemFactory.create(parent_location=section_location(course),
|
||||
template=template_name,
|
||||
display_name='Video')
|
||||
|
||||
|
||||
def add_videoalpha_to_course(course):
|
||||
template_name = 'i4x://edx/templates/videoalpha/Video_Alpha'
|
||||
world.ItemFactory.create(parent_location=section_location(course),
|
||||
template=template_name,
|
||||
display_name='Video Alpha')
|
||||
|
||||
@@ -163,7 +163,7 @@ class ModelDataCache(object):
|
||||
return self._chunked_query(
|
||||
XModuleStudentPrefsField,
|
||||
'module_type__in',
|
||||
set(descriptor.location.category for descriptor in self.descriptors),
|
||||
set(descriptor.module_class.__name__ for descriptor in self.descriptors),
|
||||
student=self.user.pk,
|
||||
field_name__in=set(field.name for field in fields),
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ class StudentPrefsFactory(DjangoModelFactory):
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
student = SubFactory(UserFactory)
|
||||
module_type = 'problem'
|
||||
module_type = 'MockProblemModule'
|
||||
|
||||
|
||||
class StudentInfoFactory(DjangoModelFactory):
|
||||
|
||||
@@ -29,6 +29,7 @@ def mock_descriptor(fields=[], lms_fields=[]):
|
||||
descriptor.location = location('def_id')
|
||||
descriptor.module_class.fields = fields
|
||||
descriptor.module_class.lms.fields = lms_fields
|
||||
descriptor.module_class.__name__ = 'MockProblemModule'
|
||||
return descriptor
|
||||
|
||||
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
|
||||
@@ -37,7 +38,7 @@ course_id = 'edX/test_course/test'
|
||||
content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
|
||||
settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id'))
|
||||
user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id'))
|
||||
prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem')
|
||||
prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'MockProblemModule')
|
||||
user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
|
||||
|
||||
|
||||
@@ -190,6 +191,10 @@ class StorageTestBase(object):
|
||||
self.mdc = ModelDataCache([mock_descriptor([mock_field(self.scope, 'existing_field')])], course_id, self.user)
|
||||
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
|
||||
|
||||
def test_set_and_get_existing_field(self):
|
||||
self.kvs.set(self.key_factory('existing_field'), 'test_value')
|
||||
self.assertEquals('test_value', self.kvs.get(self.key_factory('existing_field')))
|
||||
|
||||
def test_get_existing_field(self):
|
||||
"Test that getting an existing field in an existing Storage Field works"
|
||||
self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field')))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<h2> ${display_name} </h2>
|
||||
% endif
|
||||
|
||||
<<<<<<< HEAD
|
||||
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
<div id="stub_out_video_for_testing">
|
||||
<div class="video" data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
|
||||
@@ -43,6 +44,35 @@
|
||||
data-end="${end}"
|
||||
data-caption-asset-path="${caption_asset_path}"
|
||||
data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
|
||||
=======
|
||||
%if settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id:
|
||||
<object width="640" height="390">
|
||||
<param name="movie"
|
||||
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
value="https://www.youtube.com/v/${normal_speed_video_id}?version=3&autoplay=1&rel=0">
|
||||
% endif
|
||||
</param>
|
||||
<param name="allowScriptAccess" value="always"></param>
|
||||
<embed
|
||||
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
src="https://www.youtube.com/v/${normal_speed_video_id}?version=3&autoplay=1&rel=0"
|
||||
% endif
|
||||
type="application/x-shockwave-flash"
|
||||
allowscriptaccess="always"
|
||||
width="640" height="390"></embed>
|
||||
</object>
|
||||
%else:
|
||||
<div id="video_${id}" class="video"
|
||||
|
||||
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
data-streams="${streams}"
|
||||
% endif
|
||||
|
||||
data-show-captions="${show_captions}"
|
||||
data-start="${start}" data-end="${end}"
|
||||
data-caption-asset-path="${caption_asset_path}"
|
||||
data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
|
||||
>>>>>>> master
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
|
||||
@@ -2,34 +2,38 @@
|
||||
<h2> ${display_name} </h2>
|
||||
% endif
|
||||
|
||||
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
<div id="stub_out_video_for_testing"></div>
|
||||
%else:
|
||||
<div
|
||||
id="video_${id}"
|
||||
class="video"
|
||||
data-streams="${youtube_streams}"
|
||||
${'data-sub="{}"'.format(sub) if sub else ''}
|
||||
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
|
||||
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
|
||||
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
|
||||
data-caption-data-dir="${data_dir}"
|
||||
data-show-captions="${show_captions}"
|
||||
data-start="${start}"
|
||||
data-end="${end}"
|
||||
data-caption-asset-path="${caption_asset_path}"
|
||||
data-autoplay="${autoplay}"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
<div id="${id}"></div>
|
||||
</section>
|
||||
<section class="video-controls"></section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
<div
|
||||
id="video_${id}"
|
||||
class="video"
|
||||
|
||||
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
data-streams="${youtube_streams}"
|
||||
% endif
|
||||
|
||||
${'data-sub="{}"'.format(sub) if sub else ''}
|
||||
|
||||
% if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
|
||||
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
|
||||
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
|
||||
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
|
||||
% endif
|
||||
|
||||
data-caption-data-dir="${data_dir}"
|
||||
data-show-captions="${show_captions}"
|
||||
data-start="${start}"
|
||||
data-end="${end}"
|
||||
data-caption-asset-path="${caption_asset_path}"
|
||||
data-autoplay="${autoplay}"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
<section class="video-player">
|
||||
<div id="${id}"></div>
|
||||
</section>
|
||||
<section class="video-controls"></section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
% if sources.get('main'):
|
||||
<div class="video-sources">
|
||||
|
||||
14
rakefile
14
rakefile
@@ -1,9 +1,11 @@
|
||||
require 'json'
|
||||
require 'rake/clean'
|
||||
require './rakefiles/helpers.rb'
|
||||
|
||||
Dir['rakefiles/*.rake'].each do |rakefile|
|
||||
import rakefile
|
||||
begin
|
||||
require 'json'
|
||||
require 'rake/clean'
|
||||
require './rakelib/helpers.rb'
|
||||
rescue LoadError => error
|
||||
puts "Import faild (#{error})"
|
||||
puts "Please run `bundle install` to bootstrap ruby dependencies"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Build Constants
|
||||
|
||||
@@ -6,6 +6,8 @@ if USE_CUSTOM_THEME
|
||||
THEME_SASS = File.join(THEME_ROOT, "static", "sass")
|
||||
end
|
||||
|
||||
MINIMAL_DARWIN_NOFILE_LIMIT = 8000
|
||||
|
||||
def xmodule_cmd(watch=false, debug=false)
|
||||
xmodule_cmd = 'xmodule_assets common/static/xmodule'
|
||||
if watch
|
||||
@@ -21,24 +23,14 @@ def xmodule_cmd(watch=false, debug=false)
|
||||
end
|
||||
|
||||
def coffee_cmd(watch=false, debug=false)
|
||||
if watch
|
||||
# On OSx, coffee fails with EMFILE when
|
||||
# trying to watch all of our coffee files at the same
|
||||
# time.
|
||||
#
|
||||
# Ref: https://github.com/joyent/node/issues/2479
|
||||
#
|
||||
# So, instead, we use watchmedo, which works around the problem
|
||||
"watchmedo shell-command " +
|
||||
"--command 'node_modules/.bin/coffee -c ${watch_src_path}' " +
|
||||
"--recursive " +
|
||||
"--patterns '*.coffee' " +
|
||||
"--ignore-directories " +
|
||||
"--wait " +
|
||||
"."
|
||||
else
|
||||
'node_modules/.bin/coffee --compile .'
|
||||
if watch && Launchy::Application.new.host_os_family.darwin?
|
||||
available_files = Process::getrlimit(:NOFILE)[0]
|
||||
if available_files < MINIMAL_DARWIN_NOFILE_LIMIT
|
||||
Process.setrlimit(:NOFILE, MINIMAL_DARWIN_NOFILE_LIMIT)
|
||||
|
||||
end
|
||||
end
|
||||
"node_modules/.bin/coffee --compile #{watch ? '--watch' : ''} ."
|
||||
end
|
||||
|
||||
def sass_cmd(watch=false, debug=false)
|
||||
@@ -55,8 +47,9 @@ def sass_cmd(watch=false, debug=false)
|
||||
"#{watch ? '--watch' : '--update'} -E utf-8 #{sass_watch_paths.join(' ')}"
|
||||
end
|
||||
|
||||
# This task takes arguments purely to pass them via dependencies to the preprocess task
|
||||
desc "Compile all assets"
|
||||
multitask :assets => 'assets:all'
|
||||
task :assets, [:system, :env] => 'assets:all'
|
||||
|
||||
namespace :assets do
|
||||
|
||||
@@ -80,8 +73,9 @@ namespace :assets do
|
||||
{:xmodule => [:install_python_prereqs],
|
||||
:coffee => [:install_node_prereqs, :'assets:coffee:clobber'],
|
||||
:sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks|
|
||||
# This task takes arguments purely to pass them via dependencies to the preprocess task
|
||||
desc "Compile all #{asset_type} assets"
|
||||
task asset_type => prereq_tasks do
|
||||
task asset_type, [:system, :env] => prereq_tasks do |t, args|
|
||||
cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false)
|
||||
if cmd.kind_of?(Array)
|
||||
cmd.each {|c| sh(c)}
|
||||
@@ -90,7 +84,8 @@ namespace :assets do
|
||||
end
|
||||
end
|
||||
|
||||
multitask :all => asset_type
|
||||
# This task takes arguments purely to pass them via dependencies to the preprocess task
|
||||
multitask :all, [:system, :env] => asset_type
|
||||
multitask :debug => "assets:#{asset_type}:debug"
|
||||
multitask :_watch => "assets:#{asset_type}:_watch"
|
||||
|
||||
@@ -111,9 +106,9 @@ namespace :assets do
|
||||
task :_watch => (prereq_tasks + ["assets:#{asset_type}:debug"]) do
|
||||
cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true)
|
||||
if cmd.kind_of?(Array)
|
||||
cmd.each {|c| background_process(c)}
|
||||
cmd.each {|c| singleton_process(c)}
|
||||
else
|
||||
background_process(cmd)
|
||||
singleton_process(cmd)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,6 @@
|
||||
require 'digest/md5'
|
||||
require 'sys/proctable'
|
||||
require 'colorize'
|
||||
|
||||
def find_executable(exec)
|
||||
path = %x(which #{exec}).strip
|
||||
@@ -84,6 +86,16 @@ def background_process(*command)
|
||||
end
|
||||
end
|
||||
|
||||
# Runs a command as a background process, as long as no other processes
|
||||
# tagged with the same tag are running
|
||||
def singleton_process(*command)
|
||||
if Sys::ProcTable.ps.select {|proc| proc.cmdline.include?(command.join(' '))}.empty?
|
||||
background_process(*command)
|
||||
else
|
||||
puts "Process '#{command.join(' ')} already running, skipping".blue
|
||||
end
|
||||
end
|
||||
|
||||
def environments(system)
|
||||
Dir["#{system}/envs/**/*.py"].select{|file| ! (/__init__.py$/ =~ file)}.map do |env_file|
|
||||
env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.')
|
||||
@@ -1,5 +1,3 @@
|
||||
require './rakefiles/helpers.rb'
|
||||
|
||||
PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache')
|
||||
|
||||
CLOBBER.include(PREREQS_MD5_DIR)
|
||||
Reference in New Issue
Block a user