Commit 1d8d507f by Peter Fogg

Merge in master (again).

parents ea56a0cd 1a5b58ac
...@@ -77,3 +77,4 @@ Slater Victoroff <slater.r.victoroff@gmail.com> ...@@ -77,3 +77,4 @@ Slater Victoroff <slater.r.victoroff@gmail.com>
Peter Fogg <peter.p.fogg@gmail.com> Peter Fogg <peter.p.fogg@gmail.com>
Bethany LaPenta <lapentab@mit.edu> Bethany LaPenta <lapentab@mit.edu>
Renzo Lucioni <renzolucioni@gmail.com> Renzo Lucioni <renzolucioni@gmail.com>
Felix Sun <felixsun@mit.edu>
...@@ -8,6 +8,22 @@ the top. Include a label indicating the component affected. ...@@ -8,6 +8,22 @@ the top. Include a label indicating the component affected.
Studio: Remove XML from the video component editor. All settings are Studio: Remove XML from the video component editor. All settings are
moved to be edited as metadata. 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. Common: Repairs development documentation generation by sphinx.
LMS: Problem rescoring. Added options on the Grades tab of the 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 ...@@ -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 tasks that are currently running for the course, and an option to
see a history of background tasks for a given problem. 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 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` value of lms.start in `lms/djangoapps/django_comment_client/utils.py`
......
...@@ -4,3 +4,4 @@ gem 'sass', '3.1.15' ...@@ -4,3 +4,4 @@ gem 'sass', '3.1.15'
gem 'bourbon', '~> 1.3.6' gem 'bourbon', '~> 1.3.6'
gem 'colorize', '~> 0.5.8' gem 'colorize', '~> 0.5.8'
gem 'launchy', '~> 2.1.2' gem 'launchy', '~> 2.1.2'
gem 'sys-proctable', '~> 0.9.3'
...@@ -31,11 +31,10 @@ def press_the_notification_button(step, name): ...@@ -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 # 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). # 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 \ save_clicked = lambda: world.is_css_not_present('.is-shown.wrapper-notification-warning') or\
world.is_css_present('.is-shown.wrapper-notification-error') world.is_css_present('.is-shown.wrapper-notification-error')
assert_true(world.css_click(css, success_condition=save_clicked), assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.')
'The save button was not clicked after 5 attempts.')
@step(u'I edit the value of a policy key$') @step(u'I edit the value of a policy key$')
......
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"
#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
"""
Tests for Studio Course Settings.
"""
import datetime import datetime
import json import json
import copy import copy
import mock
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import Client from django.test.client import Client
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.timezone import UTC from django.utils.timezone import UTC
from django.test.utils import override_settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder) from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
...@@ -21,6 +26,9 @@ from xmodule.fields import Date ...@@ -21,6 +26,9 @@ from xmodule.fields import Date
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
"""
Base class for test classes below.
"""
def setUp(self): def setUp(self):
""" """
These tests need a user in the DB so that the django Test Client These tests need a user in the DB so that the django Test Client
...@@ -51,6 +59,9 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -51,6 +59,9 @@ class CourseTestCase(ModuleStoreTestCase):
class CourseDetailsTestCase(CourseTestCase): class CourseDetailsTestCase(CourseTestCase):
"""
Tests the first course settings page (course dates, overview, etc.).
"""
def test_virgin_fetch(self): def test_virgin_fetch(self):
details = CourseDetails.fetch(self.course_location) details = CourseDetails.fetch(self.course_location)
self.assertEqual(details.course_location, self.course_location, "Location not copied into") self.assertEqual(details.course_location, self.course_location, "Location not copied into")
...@@ -81,9 +92,9 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -81,9 +92,9 @@ class CourseDetailsTestCase(CourseTestCase):
Test the encoder out of its original constrained purpose to see if it functions for general use 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']), details = {'location': Location(['tag', 'org', 'course', 'category', 'name']),
'number': 1, 'number': 1,
'string': 'string', 'string': 'string',
'datetime': datetime.datetime.now(UTC())} 'datetime': datetime.datetime.now(UTC())}
jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
jsondetails = json.loads(jsondetails) jsondetails = json.loads(jsondetails)
...@@ -118,8 +129,50 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -118,8 +129,50 @@ class CourseDetailsTestCase(CourseTestCase):
jsondetails.effort, "After set effort" 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): 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): def alter_field(self, url, details, field, val):
setattr(details, field, val) setattr(details, field, val)
# Need to partially serialize payload b/c the mock doesn't handle it correctly # Need to partially serialize payload b/c the mock doesn't handle it correctly
...@@ -181,6 +234,9 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -181,6 +234,9 @@ class CourseDetailsViewTest(CourseTestCase):
class CourseGradingTest(CourseTestCase): class CourseGradingTest(CourseTestCase):
"""
Tests for the course settings grading page.
"""
def test_initial_grader(self): def test_initial_grader(self):
descriptor = get_modulestore(self.course_location).get_item(self.course_location) descriptor = get_modulestore(self.course_location).get_item(self.course_location)
test_grader = CourseGradingModel(descriptor) test_grader = CourseGradingModel(descriptor)
...@@ -256,6 +312,9 @@ class CourseGradingTest(CourseTestCase): ...@@ -256,6 +312,9 @@ class CourseGradingTest(CourseTestCase):
class CourseMetadataEditingTest(CourseTestCase): class CourseMetadataEditingTest(CourseTestCase):
"""
Tests for CourseMetadata.
"""
def setUp(self): def setUp(self):
CourseTestCase.setUp(self) CourseTestCase.setUp(self)
# add in the full class too # add in the full class too
......
...@@ -227,7 +227,8 @@ def get_course_settings(request, org, course, name): ...@@ -227,7 +227,8 @@ def get_course_settings(request, org, course, name):
kwargs={"org": org, kwargs={"org": org,
"course": course, "course": course,
"name": name, "name": name,
"section": "details"}) "section": "details"}),
'about_page_editable': not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False)
}) })
......
// studio - elements - system help // 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 { ...@@ -21,7 +21,7 @@ body.course.settings {
font-size: 14px; font-size: 14px;
} }
.message-status { .message-status {
display: none; display: none;
@include border-top-radius(2px); @include border-top-radius(2px);
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -52,6 +52,12 @@ body.course.settings { ...@@ -52,6 +52,12 @@ body.course.settings {
} }
} }
// notices - used currently for edx mktg
.notice-workflow {
margin-top: ($baseline);
}
// in form - elements // in form - elements
.group-settings { .group-settings {
margin: 0 0 ($baseline*2) 0; margin: 0 0 ($baseline*2) 0;
......
...@@ -49,7 +49,7 @@ def css_has_text(css_selector, text): ...@@ -49,7 +49,7 @@ def css_has_text(css_selector, text):
@world.absorb @world.absorb
def css_find(css, wait_time=5): 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,)) return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
world.browser.is_element_present_by_css(css, wait_time=wait_time) world.browser.is_element_present_by_css(css, wait_time=wait_time)
...@@ -58,7 +58,7 @@ def css_find(css, wait_time=5): ...@@ -58,7 +58,7 @@ def css_find(css, wait_time=5):
@world.absorb @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. 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): ...@@ -90,15 +90,15 @@ def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True):
@world.absorb @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 A method to click at x,y coordinates of the element
rather than in the center of the element rather than in the center of the element
''' '''
e = css_find(css).first element = css_find(css).first
e.action_chains.move_to_element_with_offset(e._element, x, y) element.action_chains.move_to_element_with_offset(element._element, x_cord, y_cord)
e.action_chains.click() element.action_chains.click()
e.action_chains.perform() element.action_chains.perform()
@world.absorb @world.absorb
...@@ -143,7 +143,7 @@ def css_visible(css_selector): ...@@ -143,7 +143,7 @@ def css_visible(css_selector):
@world.absorb @world.absorb
def dialogs_closed(): def dialogs_closed():
def are_dialogs_closed(driver): def are_dialogs_closed(_driver):
''' '''
Return True when no modal dialogs are visible Return True when no modal dialogs are visible
''' '''
...@@ -154,12 +154,12 @@ def dialogs_closed(): ...@@ -154,12 +154,12 @@ def dialogs_closed():
@world.absorb @world.absorb
def save_the_html(path='/tmp'): def save_the_html(path='/tmp'):
u = world.browser.url url = world.browser.url
html = world.browser.html.encode('ascii', 'ignore') html = world.browser.html.encode('ascii', 'ignore')
filename = '%s.html' % quote_plus(u) filename = '%s.html' % quote_plus(url)
f = open('%s/%s' % (path, filename), 'w') file = open('%s/%s' % (path, filename), 'w')
f.write(html) file.write(html)
f.close() file.close()
@world.absorb @world.absorb
......
...@@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content ...@@ -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) that is defined by XModules and XModuleDescriptors (javascript and css)
""" """
import logging
import hashlib import hashlib
import os import os
import errno import errno
...@@ -15,6 +16,9 @@ from path import path ...@@ -15,6 +16,9 @@ from path import path
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
LOG = logging.getLogger(__name__)
def write_module_styles(output_root): def write_module_styles(output_root):
return _write_styles('.xmodule_display', output_root, _list_modules()) return _write_styles('.xmodule_display', output_root, _list_modules())
...@@ -121,18 +125,32 @@ def _write_js(output_root, classes): ...@@ -121,18 +125,32 @@ def _write_js(output_root, classes):
type=filetype) type=filetype)
contents[filename] = fragment contents[filename] = fragment
_write_files(output_root, contents) _write_files(output_root, contents, {'.coffee': '.js'})
return [output_root / filename for filename in contents.keys()] 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) _ensure_dir(output_root)
for extra_file in set(output_root.files()) - set(contents.keys()): to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys())
extra_file.remove_p()
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(): 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(): def main():
......
...@@ -189,3 +189,10 @@ ...@@ -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: ...@@ -63,6 +63,25 @@ To get a full list of available rake tasks, use:
rake -T 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 ## Running Tests
See `testing.md` for instructions on running the test suite. See `testing.md` for instructions on running the test suite.
......
Feature: Video component Feature: Video component
As a student, I want to view course videos in LMS. As a student, I want to view course videos in LMS.
Scenario: Autoplay is enabled in LMS Scenario: Autoplay is enabled in LMS for a Video component
Given the course has a Video component Given the course has a Video component
Then when I view the video it has autoplay enabled 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): ...@@ -27,8 +27,30 @@ def view_video(_step):
world.browser.visit(url) 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): def add_video_to_course(course):
template_name = 'i4x://edx/templates/video/default' template_name = 'i4x://edx/templates/video/default'
world.ItemFactory.create(parent_location=section_location(course), world.ItemFactory.create(parent_location=section_location(course),
template=template_name, template=template_name,
display_name='Video') 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): ...@@ -163,7 +163,7 @@ class ModelDataCache(object):
return self._chunked_query( return self._chunked_query(
XModuleStudentPrefsField, XModuleStudentPrefsField,
'module_type__in', '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, student=self.user.pk,
field_name__in=set(field.name for field in fields), field_name__in=set(field.name for field in fields),
) )
......
...@@ -75,7 +75,7 @@ class StudentPrefsFactory(DjangoModelFactory): ...@@ -75,7 +75,7 @@ class StudentPrefsFactory(DjangoModelFactory):
field_name = 'existing_field' field_name = 'existing_field'
value = json.dumps('old_value') value = json.dumps('old_value')
student = SubFactory(UserFactory) student = SubFactory(UserFactory)
module_type = 'problem' module_type = 'MockProblemModule'
class StudentInfoFactory(DjangoModelFactory): class StudentInfoFactory(DjangoModelFactory):
......
...@@ -29,6 +29,7 @@ def mock_descriptor(fields=[], lms_fields=[]): ...@@ -29,6 +29,7 @@ def mock_descriptor(fields=[], lms_fields=[]):
descriptor.location = location('def_id') descriptor.location = location('def_id')
descriptor.module_class.fields = fields descriptor.module_class.fields = fields
descriptor.module_class.lms.fields = lms_fields descriptor.module_class.lms.fields = lms_fields
descriptor.module_class.__name__ = 'MockProblemModule'
return descriptor return descriptor
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem') location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
...@@ -37,7 +38,7 @@ course_id = 'edX/test_course/test' ...@@ -37,7 +38,7 @@ course_id = 'edX/test_course/test'
content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id')) content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
settings_key = partial(LmsKeyValueStore.Key, Scope.settings, 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')) 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) user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
...@@ -190,6 +191,10 @@ class StorageTestBase(object): ...@@ -190,6 +191,10 @@ class StorageTestBase(object):
self.mdc = ModelDataCache([mock_descriptor([mock_field(self.scope, 'existing_field')])], course_id, self.user) self.mdc = ModelDataCache([mock_descriptor([mock_field(self.scope, 'existing_field')])], course_id, self.user)
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc) 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): def test_get_existing_field(self):
"Test that getting an existing field in an existing Storage Field works" "Test that getting an existing field in an existing Storage Field works"
self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field'))) self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field')))
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
<h2> ${display_name} </h2> <h2> ${display_name} </h2>
% endif % endif
<<<<<<< HEAD
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: %if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
<div id="stub_out_video_for_testing"> <div id="stub_out_video_for_testing">
<div class="video" data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}"> <div class="video" data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}">
...@@ -43,6 +44,35 @@ ...@@ -43,6 +44,35 @@
data-end="${end}" data-end="${end}"
data-caption-asset-path="${caption_asset_path}" data-caption-asset-path="${caption_asset_path}"
data-autoplay="${settings.MITX_FEATURES['AUTOPLAY_VIDEOS']}"> 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&amp;autoplay=1&amp;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&amp;autoplay=1&amp;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"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <section class="video-player">
......
...@@ -2,34 +2,38 @@ ...@@ -2,34 +2,38 @@
<h2> ${display_name} </h2> <h2> ${display_name} </h2>
% endif % endif
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']: <div
<div id="stub_out_video_for_testing"></div> id="video_${id}"
%else: class="video"
<div
id="video_${id}" % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
class="video" data-streams="${youtube_streams}"
data-streams="${youtube_streams}" % endif
${'data-sub="{}"'.format(sub) if sub else ''}
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''} ${'data-sub="{}"'.format(sub) if sub 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 ''} % if not settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
data-caption-data-dir="${data_dir}" ${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
data-show-captions="${show_captions}" ${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
data-start="${start}" ${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
data-end="${end}" % endif
data-caption-asset-path="${caption_asset_path}"
data-autoplay="${autoplay}" data-caption-data-dir="${data_dir}"
> data-show-captions="${show_captions}"
<div class="tc-wrapper"> data-start="${start}"
<article class="video-wrapper"> data-end="${end}"
<section class="video-player"> data-caption-asset-path="${caption_asset_path}"
<div id="${id}"></div> data-autoplay="${autoplay}"
</section> >
<section class="video-controls"></section> <div class="tc-wrapper">
</article> <article class="video-wrapper">
</div> <section class="video-player">
</div> <div id="${id}"></div>
%endif </section>
<section class="video-controls"></section>
</article>
</div>
</div>
% if sources.get('main'): % if sources.get('main'):
<div class="video-sources"> <div class="video-sources">
......
require 'json' begin
require 'rake/clean' require 'json'
require './rakefiles/helpers.rb' require 'rake/clean'
require './rakelib/helpers.rb'
Dir['rakefiles/*.rake'].each do |rakefile| rescue LoadError => error
import rakefile puts "Import faild (#{error})"
puts "Please run `bundle install` to bootstrap ruby dependencies"
exit 1
end end
# Build Constants # Build Constants
......
...@@ -6,6 +6,8 @@ if USE_CUSTOM_THEME ...@@ -6,6 +6,8 @@ if USE_CUSTOM_THEME
THEME_SASS = File.join(THEME_ROOT, "static", "sass") THEME_SASS = File.join(THEME_ROOT, "static", "sass")
end end
MINIMAL_DARWIN_NOFILE_LIMIT = 8000
def xmodule_cmd(watch=false, debug=false) def xmodule_cmd(watch=false, debug=false)
xmodule_cmd = 'xmodule_assets common/static/xmodule' xmodule_cmd = 'xmodule_assets common/static/xmodule'
if watch if watch
...@@ -21,24 +23,14 @@ def xmodule_cmd(watch=false, debug=false) ...@@ -21,24 +23,14 @@ def xmodule_cmd(watch=false, debug=false)
end end
def coffee_cmd(watch=false, debug=false) def coffee_cmd(watch=false, debug=false)
if watch if watch && Launchy::Application.new.host_os_family.darwin?
# On OSx, coffee fails with EMFILE when available_files = Process::getrlimit(:NOFILE)[0]
# trying to watch all of our coffee files at the same if available_files < MINIMAL_DARWIN_NOFILE_LIMIT
# time. Process.setrlimit(:NOFILE, MINIMAL_DARWIN_NOFILE_LIMIT)
#
# Ref: https://github.com/joyent/node/issues/2479 end
#
# 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 .'
end end
"node_modules/.bin/coffee --compile #{watch ? '--watch' : ''} ."
end end
def sass_cmd(watch=false, debug=false) def sass_cmd(watch=false, debug=false)
...@@ -55,8 +47,9 @@ 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(' ')}" "#{watch ? '--watch' : '--update'} -E utf-8 #{sass_watch_paths.join(' ')}"
end end
# This task takes arguments purely to pass them via dependencies to the preprocess task
desc "Compile all assets" desc "Compile all assets"
multitask :assets => 'assets:all' task :assets, [:system, :env] => 'assets:all'
namespace :assets do namespace :assets do
...@@ -80,8 +73,9 @@ namespace :assets do ...@@ -80,8 +73,9 @@ namespace :assets do
{:xmodule => [:install_python_prereqs], {:xmodule => [:install_python_prereqs],
:coffee => [:install_node_prereqs, :'assets:coffee:clobber'], :coffee => [:install_node_prereqs, :'assets:coffee:clobber'],
:sass => [:install_ruby_prereqs, :preprocess]}.each_pair do |asset_type, prereq_tasks| :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" 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) cmd = send(asset_type.to_s + "_cmd", watch=false, debug=false)
if cmd.kind_of?(Array) if cmd.kind_of?(Array)
cmd.each {|c| sh(c)} cmd.each {|c| sh(c)}
...@@ -90,7 +84,8 @@ namespace :assets do ...@@ -90,7 +84,8 @@ namespace :assets do
end end
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 :debug => "assets:#{asset_type}:debug"
multitask :_watch => "assets:#{asset_type}:_watch" multitask :_watch => "assets:#{asset_type}:_watch"
...@@ -111,9 +106,9 @@ namespace :assets do ...@@ -111,9 +106,9 @@ namespace :assets do
task :_watch => (prereq_tasks + ["assets:#{asset_type}:debug"]) do task :_watch => (prereq_tasks + ["assets:#{asset_type}:debug"]) do
cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true) cmd = send(asset_type.to_s + "_cmd", watch=true, debug=true)
if cmd.kind_of?(Array) if cmd.kind_of?(Array)
cmd.each {|c| background_process(c)} cmd.each {|c| singleton_process(c)}
else else
background_process(cmd) singleton_process(cmd)
end end
end end
end end
......
require 'digest/md5' require 'digest/md5'
require 'sys/proctable'
require 'colorize'
def find_executable(exec) def find_executable(exec)
path = %x(which #{exec}).strip path = %x(which #{exec}).strip
...@@ -84,6 +86,16 @@ def background_process(*command) ...@@ -84,6 +86,16 @@ def background_process(*command)
end end
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) def environments(system)
Dir["#{system}/envs/**/*.py"].select{|file| ! (/__init__.py$/ =~ file)}.map do |env_file| Dir["#{system}/envs/**/*.py"].select{|file| ! (/__init__.py$/ =~ file)}.map do |env_file|
env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.') env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.')
......
require './rakefiles/helpers.rb'
PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache') PREREQS_MD5_DIR = ENV["PREREQ_CACHE_DIR"] || File.join(REPO_ROOT, '.prereqs_cache')
CLOBBER.include(PREREQS_MD5_DIR) CLOBBER.include(PREREQS_MD5_DIR)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment