Merge pull request #11133 from edx/rc/2016-01-05

Release Candidate rc/2016-01-05
This commit is contained in:
Matt Drayer
2016-01-06 12:16:20 -05:00
444 changed files with 28452 additions and 28247 deletions

View File

@@ -53,10 +53,12 @@ def i_click_on_error_dialog(step):
problem_string = unicode(world.scenario_dict['COURSE'].id.make_usage_key("problem", 'ignore'))
problem_string = u"Problem {}".format(problem_string[:problem_string.rfind('ignore')])
css_selector = "span.inline-error"
world.wait_for_visible(css_selector)
assert_true(
world.css_html("span.inline-error").startswith(problem_string),
world.css_html(css_selector).startswith(problem_string),
u"{} does not start with {}".format(
world.css_html("span.inline-error"), problem_string
world.css_html(css_selector), problem_string
))
# we don't know the actual ID of the vertical. So just check that we did go to a
# vertical page in the course (there should only be one).

View File

@@ -12,12 +12,14 @@ def go_to_updates(_step):
updates_css = 'li.nav-course-courseware-updates a'
world.css_click(menu_css)
world.css_click(updates_css)
world.wait_for_visible('#course-handouts-view')
@step(u'I add a new update with the text "([^"]*)"$')
def add_update(_step, text):
update_css = 'a.new-update-button'
world.css_click(update_css)
world.wait_for_visible('.CodeMirror')
change_text(text)

View File

@@ -15,9 +15,13 @@ from edx_proctoring.api import (
update_exam,
create_exam,
get_all_exams_for_course,
update_review_policy,
create_exam_review_policy,
remove_review_policy,
)
from edx_proctoring.exceptions import (
ProctoredExamNotFoundException
ProctoredExamNotFoundException,
ProctoredExamReviewPolicyNotFoundException
)
log = logging.getLogger(__name__)
@@ -72,7 +76,7 @@ def register_special_exams(course_key):
try:
exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location))
# update case, make sure everything is synced
update_exam(
exam_id = update_exam(
exam_id=exam['id'],
exam_name=timed_exam.display_name,
time_limit_mins=timed_exam.default_time_limit_minutes,
@@ -83,6 +87,7 @@ def register_special_exams(course_key):
)
msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id'])
log.info(msg)
except ProctoredExamNotFoundException:
exam_id = create_exam(
course_id=unicode(course_key),
@@ -97,6 +102,30 @@ def register_special_exams(course_key):
msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id)
log.info(msg)
# only create/update exam policy for the proctored exams
if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam:
try:
update_review_policy(
exam_id=exam_id,
set_by_user_id=timed_exam.edited_by,
review_policy=timed_exam.exam_review_rules
)
except ProctoredExamReviewPolicyNotFoundException:
if timed_exam.exam_review_rules: # won't save an empty rule.
create_exam_review_policy(
exam_id=exam_id,
set_by_user_id=timed_exam.edited_by,
review_policy=timed_exam.exam_review_rules
)
msg = 'Created new exam review policy with exam_id {exam_id}'.format(exam_id=exam_id)
log.info(msg)
else:
try:
# remove any associated review policy
remove_review_policy(exam_id=exam_id)
except ProctoredExamReviewPolicyNotFoundException:
pass
# then see which exams we have in edx-proctoring that are not in
# our current list. That means the the user has disabled it
exams = get_all_exams_for_course(course_key)

View File

@@ -9,8 +9,11 @@ from mock import patch, Mock
import ddt
from django.test import RequestFactory
from xmodule.course_module import CourseSummary
from contentstore.views.course import _accessible_courses_list, _accessible_courses_list_from_groups, AccessListFallback
from contentstore.views.course import (_accessible_courses_list, _accessible_courses_list_from_groups,
AccessListFallback, get_courses_accessible_to_user,
_staff_accessible_course_list)
from contentstore.utils import delete_course_and_groups
from contentstore.tests.utils import AjaxEnabledTestClient
from student.tests.factories import UserFactory
@@ -19,7 +22,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
from xmodule.modulestore import ModuleStoreEnum
from opaque_keys.edx.locations import CourseLocator
from xmodule.modulestore.django import modulestore
from xmodule.error_module import ErrorDescriptor
from course_action_state.models import CourseRerunState
@@ -46,7 +48,7 @@ class TestCourseListing(ModuleStoreTestCase):
self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password='test')
def _create_course_with_access_groups(self, course_location, user=None):
def _create_course_with_access_groups(self, course_location, user=None, store=ModuleStoreEnum.Type.split):
"""
Create dummy course with 'CourseFactory' and role (instructor/staff) groups
"""
@@ -54,7 +56,7 @@ class TestCourseListing(ModuleStoreTestCase):
org=course_location.org,
number=course_location.course,
run=course_location.run,
default_store=ModuleStoreEnum.Type.mongo
default_store=store
)
if user is not None:
@@ -87,54 +89,101 @@ class TestCourseListing(ModuleStoreTestCase):
# check both course lists have same courses
self.assertEqual(courses_list, courses_list_by_groups)
def test_errored_course_global_staff(self):
@ddt.data(
(ModuleStoreEnum.Type.split, 'xmodule.modulestore.split_mongo.split_mongo_kvs.SplitMongoKVS'),
(ModuleStoreEnum.Type.mongo, 'xmodule.modulestore.mongo.base.MongoKeyValueStore')
)
@ddt.unpack
def test_errored_course_global_staff(self, store, path_to_patch):
"""
Test the course list for global staff when get_course returns an ErrorDescriptor
"""
GlobalStaff().add_users(self.user)
course_key = self.store.make_course_key('Org1', 'Course1', 'Run1')
self._create_course_with_access_groups(course_key, self.user)
with self.store.default_store(store):
course_key = self.store.make_course_key('Org1', 'Course1', 'Run1')
self._create_course_with_access_groups(course_key, self.user, store=store)
with patch('xmodule.modulestore.mongo.base.MongoKeyValueStore', Mock(side_effect=Exception)):
self.assertIsInstance(modulestore().get_course(course_key), ErrorDescriptor)
with patch(path_to_patch, Mock(side_effect=Exception)):
self.assertIsInstance(self.store.get_course(course_key), ErrorDescriptor)
# get courses through iterating all courses
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(courses_list, [])
# get courses through iterating all courses
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(courses_list, [])
# get courses by reversing group name formats
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(courses_list_by_groups, [])
# get courses by reversing group name formats
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(courses_list_by_groups, [])
def test_errored_course_regular_access(self):
@ddt.data(
(ModuleStoreEnum.Type.split, 5),
(ModuleStoreEnum.Type.mongo, 3)
)
@ddt.unpack
def test_staff_course_listing(self, default_store, mongo_calls):
"""
Create courses and verify they take certain amount of mongo calls to call get_courses_accessible_to_user.
Also verify that fetch accessible courses list for staff user returns CourseSummary instances.
"""
# Assign & verify staff role to the user
GlobalStaff().add_users(self.user)
self.assertTrue(GlobalStaff().has_user(self.user))
with self.store.default_store(default_store):
# Create few courses
for num in xrange(TOTAL_COURSES_COUNT):
course_location = self.store.make_course_key('Org', 'CreatedCourse' + str(num), 'Run')
self._create_course_with_access_groups(course_location, self.user, default_store)
# Fetch accessible courses list & verify their count
courses_list_by_staff, __ = get_courses_accessible_to_user(self.request)
self.assertEqual(len(courses_list_by_staff), TOTAL_COURSES_COUNT)
# Verify fetched accessible courses list is a list of CourseSummery instances
self.assertTrue(all(isinstance(course, CourseSummary) for course in courses_list_by_staff))
# Now count the db queries for staff
with check_mongo_calls(mongo_calls):
_staff_accessible_course_list(self.request)
@ddt.data(
(ModuleStoreEnum.Type.split, 'xmodule.modulestore.split_mongo.split_mongo_kvs.SplitMongoKVS'),
(ModuleStoreEnum.Type.mongo, 'xmodule.modulestore.mongo.base.MongoKeyValueStore')
)
@ddt.unpack
def test_errored_course_regular_access(self, store, path_to_patch):
"""
Test the course list for regular staff when get_course returns an ErrorDescriptor
"""
GlobalStaff().remove_users(self.user)
CourseStaffRole(self.store.make_course_key('Non', 'Existent', 'Course')).add_users(self.user)
course_key = self.store.make_course_key('Org1', 'Course1', 'Run1')
self._create_course_with_access_groups(course_key, self.user)
with self.store.default_store(store):
CourseStaffRole(self.store.make_course_key('Non', 'Existent', 'Course')).add_users(self.user)
with patch('xmodule.modulestore.mongo.base.MongoKeyValueStore', Mock(side_effect=Exception)):
self.assertIsInstance(modulestore().get_course(course_key), ErrorDescriptor)
course_key = self.store.make_course_key('Org1', 'Course1', 'Run1')
self._create_course_with_access_groups(course_key, self.user, store)
# get courses through iterating all courses
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(courses_list, [])
with patch(path_to_patch, Mock(side_effect=Exception)):
self.assertIsInstance(self.store.get_course(course_key), ErrorDescriptor)
# get courses by reversing group name formats
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(courses_list_by_groups, [])
self.assertEqual(courses_list, courses_list_by_groups)
# get courses through iterating all courses
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(courses_list, [])
def test_get_course_list_with_invalid_course_location(self):
# get courses by reversing group name formats
courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(courses_list_by_groups, [])
self.assertEqual(courses_list, courses_list_by_groups)
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
def test_get_course_list_with_invalid_course_location(self, store):
"""
Test getting courses with invalid course location (course deleted from modulestore).
"""
course_key = self.store.make_course_key('Org', 'Course', 'Run')
self._create_course_with_access_groups(course_key, self.user)
with self.store.default_store(store):
course_key = self.store.make_course_key('Org', 'Course', 'Run')
self._create_course_with_access_groups(course_key, self.user, store)
# get courses through iterating all courses
courses_list, __ = _accessible_courses_list(self.request)
@@ -155,7 +204,12 @@ class TestCourseListing(ModuleStoreTestCase):
courses_list, __ = _accessible_courses_list(self.request)
self.assertEqual(len(courses_list), 0)
def test_course_listing_performance(self):
@ddt.data(
(ModuleStoreEnum.Type.split, 150, 505),
(ModuleStoreEnum.Type.mongo, USER_COURSES_COUNT, 3)
)
@ddt.unpack
def test_course_listing_performance(self, store, courses_list_from_group_calls, courses_list_calls):
"""
Create large number of courses and give access of some of these courses to the user and
compare the time to fetch accessible courses for the user through traversing all courses and
@@ -165,15 +219,16 @@ class TestCourseListing(ModuleStoreTestCase):
user_course_ids = random.sample(range(TOTAL_COURSES_COUNT), USER_COURSES_COUNT)
# create courses and assign those to the user which have their number in user_course_ids
for number in range(TOTAL_COURSES_COUNT):
org = 'Org{0}'.format(number)
course = 'Course{0}'.format(number)
run = 'Run{0}'.format(number)
course_location = self.store.make_course_key(org, course, run)
if number in user_course_ids:
self._create_course_with_access_groups(course_location, self.user)
else:
self._create_course_with_access_groups(course_location)
with self.store.default_store(store):
for number in range(TOTAL_COURSES_COUNT):
org = 'Org{0}'.format(number)
course = 'Course{0}'.format(number)
run = 'Run{0}'.format(number)
course_location = self.store.make_course_key(org, course, run)
if number in user_course_ids:
self._create_course_with_access_groups(course_location, self.user, store=store)
else:
self._create_course_with_access_groups(course_location, store=store)
# time the get courses by iterating through all courses
with Timer() as iteration_over_courses_time_1:
@@ -201,29 +256,29 @@ class TestCourseListing(ModuleStoreTestCase):
self.assertGreaterEqual(iteration_over_courses_time_2.elapsed, iteration_over_groups_time_2.elapsed)
# Now count the db queries
with check_mongo_calls(USER_COURSES_COUNT):
with check_mongo_calls(courses_list_from_group_calls):
_accessible_courses_list_from_groups(self.request)
with check_mongo_calls(courses_list_calls):
_accessible_courses_list(self.request)
# Calls:
# 1) query old mongo
# 2) get_more on old mongo
# 3) query split (but no courses so no fetching of data)
with check_mongo_calls(3):
_accessible_courses_list(self.request)
def test_course_listing_errored_deleted_courses(self):
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
def test_course_listing_errored_deleted_courses(self, store):
"""
Create good courses, courses that won't load, and deleted courses which still have
roles. Test course listing.
"""
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
with self.store.default_store(store):
course_location = self.store.make_course_key('testOrg', 'testCourse', 'RunBabyRun')
self._create_course_with_access_groups(course_location, self.user, store)
course_location = self.store.make_course_key('testOrg', 'testCourse', 'RunBabyRun')
self._create_course_with_access_groups(course_location, self.user)
course_location = self.store.make_course_key('testOrg', 'doomedCourse', 'RunBabyRun')
self._create_course_with_access_groups(course_location, self.user)
store.delete_course(course_location, self.user.id)
course_location = self.store.make_course_key('testOrg', 'doomedCourse', 'RunBabyRun')
self._create_course_with_access_groups(course_location, self.user, store)
self.store.delete_course(course_location, self.user.id) # pylint: disable=no-member
courses_list, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list), 1, courses_list)
@@ -241,7 +296,7 @@ class TestCourseListing(ModuleStoreTestCase):
run=org_course_one.run
)
org_course_two = self.store.make_course_key('AwesomeOrg', 'Course2', 'RunRunRun')
org_course_two = self.store.make_course_key('AwesomeOrg', 'Course2', 'RunBabyRun')
CourseFactory.create(
org=org_course_two.org,
number=org_course_two.course,

View File

@@ -11,7 +11,10 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from contentstore.signals import listen_for_course_publish
from edx_proctoring.api import get_all_exams_for_course
from edx_proctoring.api import (
get_all_exams_for_course,
get_review_policy_by_exam_id
)
@ddt.ddt
@@ -44,21 +47,28 @@ class TestProctoredExams(ModuleStoreTestCase):
self.assertEqual(len(exams), 1)
exam = exams[0]
if exam['is_proctored'] and not exam['is_practice_exam']:
# get the review policy object
exam_review_policy = get_review_policy_by_exam_id(exam['id'])
self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules)
self.assertEqual(exam['course_id'], unicode(self.course.id))
self.assertEqual(exam['content_id'], unicode(sequence.location))
self.assertEqual(exam['exam_name'], sequence.display_name)
self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes)
self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam)
self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam)
self.assertEqual(exam['is_active'], expected_active)
@ddt.data(
(True, 10, True, True, False),
(True, 10, False, True, False),
(True, 10, True, True, True),
(True, 10, True, False, True, False),
(True, 10, False, False, True, False),
(True, 10, True, True, True, True),
)
@ddt.unpack
def test_publishing_exam(self, is_time_limited, default_time_limit_minutes,
is_proctored_exam, expected_active, republish):
is_proctored_exam, is_practice_exam, expected_active, republish):
"""
Happy path testing to see that when a course is published which contains
a proctored exam, it will also put an entry into the exam tables
@@ -73,7 +83,9 @@ class TestProctoredExams(ModuleStoreTestCase):
is_time_limited=is_time_limited,
default_time_limit_minutes=default_time_limit_minutes,
is_proctored_exam=is_proctored_exam,
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1)
is_practice_exam=is_practice_exam,
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1),
exam_review_rules="allow_use_of_paper"
)
listen_for_course_publish(self, self.course.id)
@@ -205,7 +217,8 @@ class TestProctoredExams(ModuleStoreTestCase):
graded=True,
is_time_limited=True,
default_time_limit_minutes=10,
is_proctored_exam=True
is_proctored_exam=True,
exam_review_rules="allow_use_of_paper"
)
listen_for_course_publish(self, self.course.id)

View File

@@ -359,7 +359,10 @@ def certificates_list_handler(request, course_key_string):
course_id=course.id, include_expired=True
) if mode.slug != 'audit'
]
if len(course_modes) > 0:
has_certificate_modes = len(course_modes) > 0
if has_certificate_modes:
certificate_web_view_url = get_lms_link_for_certificate_web_view(
user_id=request.user.id,
course_key=course_key,
@@ -382,6 +385,7 @@ def certificates_list_handler(request, course_key_string):
'course_outline_url': course_outline_url,
'upload_asset_url': upload_asset_url,
'certificates': certificates,
'has_certificate_modes': has_certificate_modes,
'course_modes': course_modes,
'certificate_web_view_url': certificate_web_view_url,
'is_active': is_active,

View File

@@ -17,6 +17,7 @@ import django.utils
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods, require_GET
from django.views.decorators.csrf import ensure_csrf_cookie
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location
@@ -345,6 +346,39 @@ def _course_outline_json(request, course_module):
)
def get_in_process_course_actions(request):
"""
Get all in-process course actions
"""
return [
course for course in
CourseRerunState.objects.find_all(
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED}, should_display=True
)
if has_studio_read_access(request.user, course.course_key)
]
def _staff_accessible_course_list(request):
"""
List all courses available to the logged in user by iterating through all the courses
"""
def course_filter(course_summary):
"""
Filter out unusable and inaccessible courses
"""
# pylint: disable=fixme
# TODO remove this condition when templates purged from db
if course_summary.location.course == 'templates':
return False
return has_studio_read_access(request.user, course_summary.id)
courses_summary = filter(course_filter, modulestore().get_course_summaries())
in_process_course_actions = get_in_process_course_actions(request)
return courses_summary, in_process_course_actions
def _accessible_courses_list(request):
"""
List all courses available to the logged in user by iterating through all the courses
@@ -364,13 +398,8 @@ def _accessible_courses_list(request):
return has_studio_read_access(request.user, course.id)
courses = filter(course_filter, modulestore().get_courses())
in_process_course_actions = [
course for course in
CourseRerunState.objects.find_all(
exclude_args={'state': CourseRerunUIStateManager.State.SUCCEEDED}, should_display=True
)
if has_studio_read_access(request.user, course.course_key)
]
in_process_course_actions = get_in_process_course_actions(request)
return courses, in_process_course_actions
@@ -593,7 +622,7 @@ def get_courses_accessible_to_user(request):
"""
if GlobalStaff().has_user(request.user):
# user has global access so no need to get courses from django groups
courses, in_process_course_actions = _accessible_courses_list(request)
courses, in_process_course_actions = _staff_accessible_course_list(request)
else:
try:
courses, in_process_course_actions = _accessible_courses_list_from_groups(request)
@@ -626,9 +655,9 @@ def _remove_in_process_courses(courses, in_process_course_actions):
in_process_action_course_keys = [uca.course_key for uca in in_process_course_actions]
courses = [
format_course_for_view(c)
for c in courses
if not isinstance(c, ErrorDescriptor) and (c.id not in in_process_action_course_keys)
format_course_for_view(course)
for course in courses
if not isinstance(course, ErrorDescriptor) and (course.id not in in_process_action_course_keys)
]
return courses

View File

@@ -834,7 +834,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info = {
"id": unicode(xblock.location),
"display_name": xblock.display_name_with_default,
"display_name": xblock.display_name_with_default_escaped,
"category": xblock.category,
"edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None,
"published": published,
@@ -869,6 +869,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"is_proctored_exam": xblock.is_proctored_exam,
"is_practice_exam": xblock.is_practice_exam,
"is_time_limited": xblock.is_time_limited,
"exam_review_rules": xblock.exam_review_rules,
"default_time_limit_minutes": xblock.default_time_limit_minutes
})
@@ -1097,4 +1098,4 @@ def _xblock_type_and_display_name(xblock):
"""
return _('{section_or_subsection} "{display_name}"').format(
section_or_subsection=xblock_type_display_name(xblock),
display_name=xblock.display_name_with_default)
display_name=xblock.display_name_with_default_escaped)

View File

@@ -1,4 +1,5 @@
"""Programs views for use with Studio."""
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.http import Http404, JsonResponse
@@ -27,6 +28,7 @@ class ProgramAuthoringView(View):
return render_to_response('program_authoring.html', {
'show_programs_header': programs_config.is_studio_tab_enabled,
'authoring_app_config': programs_config.authoring_app_config,
'lms_base_url': '//{}/'.format(settings.LMS_BASE),
'programs_api_url': programs_config.public_api_url,
'programs_token_url': reverse('programs_id_token'),
'studio_home_url': reverse('home'),

View File

@@ -195,6 +195,7 @@ class CertificatesBaseTestCase(object):
self.assertTrue('must have name of the certificate' in context.exception)
@ddt.ddt
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods):
"""
@@ -340,6 +341,40 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat
self.assertContains(response, 'verified')
self.assertNotContains(response, 'audit')
def test_audit_only_disables_cert(self):
"""
Tests audit course mode is skipped when rendering certificates page.
"""
CourseModeFactory.create(course_id=self.course.id, mode_slug='audit')
response = self.client.get_html(
self._url(),
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'This course does not use a mode that offers certificates.')
self.assertNotContains(response, 'This module is not enabled.')
self.assertNotContains(response, 'Loading')
@ddt.data(
['audit', 'verified'],
['verified'],
['audit', 'verified', 'credit'],
['verified', 'credit'],
['professional']
)
def test_non_audit_enables_cert(self, slugs):
"""
Tests audit course mode is skipped when rendering certificates page.
"""
for slug in slugs:
CourseModeFactory.create(course_id=self.course.id, mode_slug=slug)
response = self.client.get_html(
self._url(),
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'This course does not use a mode that offers certificates.')
self.assertNotContains(response, 'This module is not enabled.')
self.assertContains(response, 'Loading')
def test_assign_unique_identifier_to_certificates(self):
"""
Test certificates have unique ids

View File

@@ -1374,15 +1374,6 @@ class TestComponentTemplates(CourseTestCase):
self.assertNotEqual(only_template.get('category'), 'video')
self.assertNotEqual(only_template.get('category'), 'openassessment')
def test_advanced_components_without_display_name(self):
"""
Test that advanced components without display names display their category instead.
"""
self.course.advanced_modules.append('graphical_slider_tool')
self.templates = get_component_templates(self.course)
template = self.get_templates_of_type('advanced')[0]
self.assertEqual(template.get('display_name'), 'graphical_slider_tool')
def test_advanced_problems(self):
"""
Test the handling of advanced problem templates.

View File

@@ -49,6 +49,7 @@ class CourseMetadata(object):
'is_proctored_enabled',
'is_time_limited',
'is_practice_exam',
'exam_review_rules',
'self_paced'
]

View File

@@ -306,7 +306,6 @@ EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update(AUTH_TOKE
EVENT_TRACKING_BACKENDS['segmentio']['OPTIONS']['processors'][0]['OPTIONS']['whitelist'].extend(
AUTH_TOKENS.get("EVENT_TRACKING_SEGMENTIO_EMIT_WHITELIST", []))
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
##### ACCOUNT LOCKOUT DEFAULT PARAMETERS #####

View File

@@ -74,8 +74,6 @@
"ENTRANCE_EXAMS": true,
"MILESTONES_APP": true,
"PREVIEW_LMS_BASE": "localhost:8003",
"SUBDOMAIN_BRANDING": false,
"SUBDOMAIN_COURSE_LISTINGS": false,
"ALLOW_ALL_ADVANCED_COMPONENTS": true,
"ENABLE_CONTENT_LIBRARIES": true,
"ENABLE_SPECIAL_EXAMS": true

View File

@@ -803,6 +803,9 @@ INSTALLED_APPS = (
# edX Proctoring
'edx_proctoring',
# Bookmarks
'openedx.core.djangoapps.bookmarks',
# programs support
'openedx.core.djangoapps.programs',
@@ -997,7 +1000,6 @@ ADVANCED_COMPONENT_TYPES = [
'videoannotation', # module for annotating video (with annotation table)
'imageannotation', # module for annotating image (with annotation table)
'word_cloud',
'graphical_slider_tool',
'lti',
'lti_consumer',
'library_content',
@@ -1115,7 +1117,11 @@ CREDIT_PROVIDER_TIMESTAMP_EXPIRATION = 15 * 60
################################ Deprecated Blocks Info ################################
DEPRECATED_BLOCK_TYPES = ['peergrading', 'combinedopenended']
DEPRECATED_BLOCK_TYPES = [
'peergrading',
'combinedopenended',
'graphical_slider_tool',
]
#### PROCTORING CONFIGURATION DEFAULTS

View File

@@ -24,7 +24,7 @@ require.config({
"jquery.ui": "js/vendor/jquery-ui.min",
"jquery.form": "js/vendor/jquery.form",
"jquery.markitup": "js/vendor/markitup/jquery.markitup",
"jquery.leanModal": "js/vendor/jquery.leanModal.min",
"jquery.leanModal": "js/vendor/jquery.leanModal",
"jquery.ajaxQueue": "js/vendor/jquery.ajaxQueue",
"jquery.smoothScroll": "js/vendor/jquery.smooth-scroll.min",
"jquery.timepicker": "js/vendor/timepicker/jquery.timepicker",
@@ -283,7 +283,7 @@ require.config({
"osda":{
exports: "osda",
deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars", "URI"]
},
}
// end of annotation tool files
}
});

View File

@@ -7,7 +7,7 @@ requirejs.config({
"jquery.ui": "xmodule_js/common_static/js/vendor/jquery-ui.min",
"jquery.form": "xmodule_js/common_static/js/vendor/jquery.form",
"jquery.markitup": "xmodule_js/common_static/js/vendor/markitup/jquery.markitup",
"jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal.min",
"jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal",
"jquery.ajaxQueue": "xmodule_js/common_static/js/vendor/jquery.ajaxQueue",
"jquery.smoothScroll": "xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min",
"jquery.scrollTo": "xmodule_js/common_static/js/vendor/jquery.scrollTo-1.4.2-min",
@@ -253,6 +253,7 @@ define([
"js/spec/views/xblock_validation_spec",
"js/spec/views/license_spec",
"js/spec/views/paging_spec",
"js/spec/views/login_studio_spec",
"js/spec/views/pages/container_spec",
"js/spec/views/pages/container_subviews_spec",

View File

@@ -7,7 +7,7 @@ requirejs.config({
"jquery.ui": "xmodule_js/common_static/js/vendor/jquery-ui.min",
"jquery.form": "xmodule_js/common_static/js/vendor/jquery.form",
"jquery.markitup": "xmodule_js/common_static/js/vendor/markitup/jquery.markitup",
"jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal.min",
"jquery.leanModal": "xmodule_js/common_static/js/vendor/jquery.leanModal",
"jquery.smoothScroll": "xmodule_js/common_static/js/vendor/jquery.smooth-scroll.min",
"jquery.scrollTo": "xmodule_js/common_static/js/vendor/jquery.scrollTo-1.4.2-min",
"jquery.timepicker": "xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker",

View File

@@ -1,4 +1,6 @@
define ["jquery", "js/spec_helpers/edit_helpers", "coffee/src/views/module_edit", "js/models/module_info", "xmodule"], ($, edit_helpers, ModuleEdit, ModuleModel) ->
define ["jquery", "common/js/components/utils/view_utils", "js/spec_helpers/edit_helpers",
"coffee/src/views/module_edit", "js/models/module_info", "xmodule"],
($, ViewUtils, edit_helpers, ModuleEdit, ModuleModel) ->
describe "ModuleEdit", ->
beforeEach ->
@@ -60,7 +62,7 @@ define ["jquery", "js/spec_helpers/edit_helpers", "coffee/src/views/module_edit"
spyOn(@moduleEdit, 'loadDisplay')
spyOn(@moduleEdit, 'delegateEvents')
spyOn($.fn, 'append')
spyOn($, 'getScript').andReturn($.Deferred().resolve().promise())
spyOn(ViewUtils, 'loadJavaScript').andReturn($.Deferred().resolve().promise());
window.MockXBlock = (runtime, element) ->
return { }
@@ -150,7 +152,7 @@ define ["jquery", "js/spec_helpers/edit_helpers", "coffee/src/views/module_edit"
expect($('head').append).toHaveBeenCalledWith("<script>inline-js</script>")
it "loads js urls from fragments", ->
expect($.getScript).toHaveBeenCalledWith("js-url")
expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith("js-url")
it "loads head html", ->
expect($('head').append).toHaveBeenCalledWith("head-html")

View File

@@ -1,4 +1,4 @@
define(['jquery.cookie', 'utility'], function() {
define(['jquery.cookie', 'utility', 'common/js/components/utils/view_utils'], function(cookie, utility, ViewUtils) {
'use strict';
return function (homepageURL) {
function postJSON(url, data, callback) {
@@ -22,15 +22,19 @@ define(['jquery.cookie', 'utility'], function() {
$('form#login_form').submit(function(event) {
event.preventDefault();
var submitButton = $('#submit'),
deferred = new $.Deferred(),
promise = deferred.promise();
ViewUtils.disableElementWhileRunning(submitButton, function() { return promise; });
var submit_data = $('#login_form').serialize();
postJSON('/login_post', submit_data, function(json) {
if(json.success) {
var next = /next=([^&]*)/g.exec(decodeURIComponent(window.location.search));
if (next && next.length > 1 && !isExternal(next[1])) {
location.href = next[1];
ViewUtils.redirect(next[1]);
} else {
location.href = homepageURL;
ViewUtils.redirect(homepageURL);
}
} else if($('#login_error').length === 0) {
$('#login_form').prepend(
@@ -39,11 +43,13 @@ define(['jquery.cookie', 'utility'], function() {
'</span></div>'
);
$('#login_error').addClass('is-shown');
deferred.resolve();
} else {
$('#login_error')
.stop()
.addClass('is-shown')
.html(json.value);
deferred.resolve();
}
});
});

View File

@@ -0,0 +1,32 @@
define(['jquery', 'js/factories/login', 'common/js/spec_helpers/ajax_helpers', 'common/js/components/utils/view_utils'],
function($, LoginFactory, AjaxHelpers, ViewUtils) {
'use strict';
describe("Studio Login Page", function() {
var submitButton;
beforeEach(function() {
loadFixtures('mock/login.underscore');
/*jshint unused: false*/
var login_factory = new LoginFactory("/home/");
submitButton = $('#submit');
});
it('disable the submit button once it is clicked', function() {
spyOn(ViewUtils, 'redirect').andCallFake(function(){});
var requests = AjaxHelpers.requests(this);
expect(submitButton).not.toHaveClass('is-disabled');
submitButton.click();
AjaxHelpers.respondWithJson(requests, {'success': true});
expect(submitButton).toHaveClass('is-disabled');
});
it('It will not disable the submit button if there are errors in ajax request', function() {
var requests = AjaxHelpers.requests(this);
expect(submitButton).not.toHaveClass('is-disabled');
submitButton.click();
expect(submitButton).toHaveClass('is-disabled');
AjaxHelpers.respondWithError(requests, {});
expect(submitButton).not.toHaveClass('is-disabled');
});
});
});

View File

@@ -216,7 +216,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
'course-outline', 'xblock-string-field-editor', 'modal-button',
'basic-modal', 'course-outline-modal', 'release-date-editor',
'due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor', 'timed-examination-preference-editor'
'staff-lock-editor', 'settings-tab-section', 'timed-examination-preference-editor'
]);
appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON({}, [
@@ -580,7 +580,8 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
describe("Subsection", function() {
var getDisplayNameWrapper, setEditModalValues, mockServerValuesJson,
selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam;
selectDisableSpecialExams, selectGeneralSettings, selectAdvancedSettings,
selectTimedExam, selectProctoredExam, selectPracticeExam;
getDisplayNameWrapper = function() {
return getItemHeaders('subsection').find('.wrapper-xblock-field');
@@ -597,6 +598,14 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
this.$("#id_not_timed").prop('checked', true).trigger('change');
};
selectGeneralSettings = function() {
this.$(".modal-section .general-settings-button").click();
};
selectAdvancedSettings = function() {
this.$(".modal-section .advanced-settings-button").click();
};
selectTimedExam = function(time_limit) {
this.$("#id_timed_exam").prop('checked', true).trigger('change');
this.$("#id_time_limit").val(time_limit);
@@ -701,6 +710,45 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
collapseItemsAndVerifyState('subsection');
expandItemsAndVerifyState('subsection');
});
it('can show general settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectGeneralSettings();
expect($('.modal-section .general-settings-button')).toHaveClass('active');
expect($('.modal-section .advanced-settings-button')).not.toHaveClass('active');
});
it('can show advanced settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('.modal-section .general-settings-button')).not.toHaveClass('active');
expect($('.modal-section .advanced-settings-button')).toHaveClass('active');
});
it('can select valid time', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
var default_time = "00:30";
var valid_times = ["00:30", "23:00", "24:00", "99:00"];
var invalid_times = ["00:00", "100:00", "01:60"];
var time_limit, i;
for (i = 0; i < valid_times.length; i++){
time_limit = valid_times[i];
selectTimedExam(time_limit);
expect($("#id_time_limit").val()).toEqual(time_limit);
}
for (i = 0; i < invalid_times.length; i++){
time_limit = invalid_times[i];
selectTimedExam(time_limit);
expect($("#id_time_limit").val()).not.toEqual(time_limit);
expect($("#id_time_limit").val()).toEqual(default_time);
}
});
it('can be edited', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
@@ -715,6 +763,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
"visible_to_staff_only": true,
"start":"2014-07-09T00:00:00.000Z",
"due":"2014-07-10T00:00:00.000Z",
"exam_review_rules": "",
"is_time_limited": true,
"is_practice_exam": false,
"is_proctored_enabled": true,

View File

@@ -1,7 +1,7 @@
define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", "js/views/xblock", "js/models/xblock_info",
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
function ($, AjaxHelpers, URI, XBlockView, XBlockInfo) {
define(["jquery", "URI", "common/js/spec_helpers/ajax_helpers", "common/js/components/utils/view_utils",
"js/views/xblock", "js/models/xblock_info", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
function ($, URI, AjaxHelpers, ViewUtils, XBlockView, XBlockInfo) {
"use strict";
describe("XBlockView", function() {
var model, xblockView, mockXBlockHtml;
@@ -89,11 +89,11 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", "js/views/xbloc
it('aborts rendering when a dependent script fails to load', function() {
var requests = AjaxHelpers.requests(this),
mockJavaScriptUrl = "mock.js",
missingJavaScriptUrl = "no_such_file.js",
promise;
spyOn($, 'getScript').andReturn($.Deferred().reject().promise());
spyOn(ViewUtils, 'loadJavaScript').andReturn($.Deferred().reject().promise());
promise = postXBlockRequest(requests, [
["hash5", { mimetype: "application/javascript", kind: "url", data: mockJavaScriptUrl }]
["hash5", { mimetype: "application/javascript", kind: "url", data: missingJavaScriptUrl }]
]);
expect(promise.isRejected()).toBe(true);
});
@@ -104,7 +104,7 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", "js/views/xbloc
postXBlockRequest(AjaxHelpers.requests(this), []);
xblockView.$el.find(".notification-action-button").click();
expect(notifySpy).toHaveBeenCalledWith("add-missing-groups", model.get("id"));
})
});
});
});
});

View File

@@ -98,7 +98,7 @@ define(
file: file,
date: moment().valueOf(),
completed: completed || false
}));
}), {path: window.location.pathname});
};
/**

View File

@@ -84,7 +84,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
getContext: function () {
return $.extend({
xblockInfo: this.model,
introductionMessage: this.getIntroductionMessage()
introductionMessage: this.getIntroductionMessage(),
enable_proctored_exams: this.options.enable_proctored_exams,
enable_timed_exams: this.options.enable_timed_exams
});
},
@@ -114,6 +116,78 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
gettext('Change the settings for %(display_name)s'),
{ display_name: this.model.get('display_name') }, true
);
},
initializeEditors: function () {
var special_exams_editors = this.options.special_exam_editors;
if (typeof special_exams_editors !== 'undefined' && special_exams_editors.length > 0) {
var tabs_html = this.loadTemplate('settings-tab-section');
this.$('.modal-section').html(tabs_html);
this.options.editors = _.map(this.options.editors, function (Editor) {
return new Editor({
parentElement: this.$('.modal-section .general-settings'),
model: this.model,
xblockType: this.options.xblockType,
enable_proctored_exams: this.options.enable_proctored_exams,
enable_timed_exams: this.options.enable_timed_exams
});
}, this);
this.options.special_exam_editors = _.map(special_exams_editors, function (Editor) {
return new Editor({
parentElement: this.$('.modal-section .advanced-settings'),
model: this.model,
xblockType: this.options.xblockType,
enable_proctored_exams: this.options.enable_proctored_exams,
enable_timed_exams: this.options.enable_timed_exams
});
}, this);
this.hideAdvancedSettings();
} else {
CourseOutlineXBlockModal.prototype.initializeEditors.call(this);
}
},
events: {
'click .action-save': 'save',
'click .general-settings-button': 'showGeneralSettings',
'click .advanced-settings-button': 'showAdvancedSettings'
},
/**
* Return request data.
* @return {Object}
*/
getRequestData: function () {
var combined_editors = this.options.editors.concat(this.options.special_exam_editors);
var requestData = _.map(combined_editors, function (editor) {
return editor.getRequestData();
});
return $.extend.apply(this, [true, {}].concat(requestData));
},
hideAdvancedSettings: function() {
this.$('.modal-section .general-settings-button').addClass('active');
this.$('.modal-section .advanced-settings-button').removeClass('active');
this.$('.modal-section .general-settings').show();
this.$('.modal-section .advanced-settings').hide();
},
hideGeneralSettings: function() {
this.$('.modal-section .general-settings-button').removeClass('active');
this.$('.modal-section .advanced-settings-button').addClass('active');
this.$('.modal-section .general-settings').hide();
this.$('.modal-section .advanced-settings').show();
},
showGeneralSettings: function (event) {
event.preventDefault();
this.hideAdvancedSettings();
},
showAdvancedSettings: function (event) {
event.preventDefault();
this.hideGeneralSettings();
}
});
@@ -267,20 +341,40 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
className: 'edit-settings-timed-examination',
events : {
'change #id_not_timed': 'notTimedExam',
'change #id_timed_exam': 'showTimeLimit',
'change #id_practice_exam': 'showTimeLimit',
'change #id_proctored_exam': 'showTimeLimit',
'change #id_timed_exam': 'setTimedExam',
'change #id_practice_exam': 'setPracticeExam',
'change #id_proctored_exam': 'setProctoredExam',
'focusout #id_time_limit': 'timeLimitFocusout'
},
notTimedExam: function (event) {
event.preventDefault();
this.$('#id_time_limit_div').hide();
this.$('.exam-review-rules-list-fields').hide();
this.$('#id_time_limit').val('00:00');
},
showTimeLimit: function (event) {
event.preventDefault();
selectSpecialExam: function (showRulesField) {
this.$('#id_time_limit_div').show();
this.$('#id_time_limit').val("00:30");
if (!this.isValidTimeLimit(this.$('#id_time_limit').val())) {
this.$('#id_time_limit').val('00:30');
}
if (showRulesField) {
this.$('.exam-review-rules-list-fields').show();
}
else {
this.$('.exam-review-rules-list-fields').hide();
}
},
setTimedExam: function (event) {
event.preventDefault();
this.selectSpecialExam(false);
},
setPracticeExam: function (event) {
event.preventDefault();
this.selectSpecialExam(false);
},
setProctoredExam: function (event) {
event.preventDefault();
this.selectSpecialExam(true);
},
timeLimitFocusout: function(event) {
event.preventDefault();
@@ -294,13 +388,15 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
this.$('input.time').timepicker({
'timeFormat' : 'H:i',
'minTime': '00:30',
'maxTime': '05:00',
'maxTime': '24:00',
'forceRoundTime': false
});
this.setExamType(this.model.get('is_time_limited'), this.model.get('is_proctored_exam'),
this.model.get('is_practice_exam'));
this.setExamTime(this.model.get('default_time_limit_minutes'));
this.setReviewRules(this.model.get('exam_review_rules'));
},
setExamType: function(is_time_limited, is_proctored_exam, is_practice_exam) {
if (!is_time_limited) {
@@ -309,12 +405,14 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
this.$('#id_time_limit_div').show();
this.$('.exam-review-rules-list-fields').hide();
if (this.options.enable_proctored_exams && is_proctored_exam) {
if (is_practice_exam) {
this.$('#id_practice_exam').prop('checked', true);
} else {
this.$('#id_proctored_exam').prop('checked', true);
this.$('.exam-review-rules-list-fields').show();
}
} else {
// Since we have an early exit at the top of the method
@@ -327,8 +425,11 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var time = this.convertTimeLimitMinutesToString(value);
this.$('#id_time_limit').val(time);
},
setReviewRules: function (value) {
this.$('#id_exam_review_rules').val(value);
},
isValidTimeLimit: function(time_limit) {
var pattern = new RegExp('^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$');
var pattern = new RegExp('^\\d{1,2}:[0-5][0-9]$');
return pattern.test(time_limit) && time_limit !== "00:00";
},
getExamTimeLimit: function () {
@@ -351,6 +452,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var is_practice_exam;
var is_proctored_exam;
var time_limit = this.getExamTimeLimit();
var exam_review_rules = this.$('#id_exam_review_rules').val();
if (this.$("#id_not_timed").is(':checked')){
is_time_limited = false;
@@ -374,6 +476,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
metadata: {
'is_practice_exam': is_practice_exam,
'is_time_limited': is_time_limited,
'exam_review_rules': exam_review_rules,
// We have to use the legacy field name
// as the Ajax handler directly populates
// the xBlocks fields. We will have to
@@ -584,6 +687,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
getEditModal: function (xblockInfo, options) {
var editors = [];
var special_exam_editors = [];
if (xblockInfo.isChapter()) {
editors = [ReleaseDateEditor, StaffLockEditor];
@@ -592,7 +696,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var enable_special_exams = (options.enable_proctored_exams || options.enable_timed_exams);
if (enable_special_exams) {
editors.push(TimedExaminationPreferenceEditor);
special_exam_editors.push(TimedExaminationPreferenceEditor);
}
editors.push(StaffLockEditor);
@@ -610,6 +714,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
return new SettingsXBlockModal($.extend({
editors: editors,
special_exam_editors: special_exam_editors,
model: xblockInfo
}, options));
},

View File

@@ -1,5 +1,6 @@
define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
function ($, _, BaseView, XBlock) {
define(["jquery", "underscore", "common/js/components/utils/view_utils", "js/views/baseview", "xblock/runtime.v1"],
function ($, _, ViewUtils, BaseView, XBlock) {
'use strict';
var XBlockView = BaseView.extend({
// takes XBlockInfo as a model
@@ -83,7 +84,7 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
* may have thrown JavaScript errors after rendering in which case the xblock parameter
* will be null.
*/
xblockReady: function(xblock) {
xblockReady: function(xblock) { // jshint ignore:line
// Do nothing
},
@@ -95,7 +96,7 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
* represents this process.
* @param fragment The fragment returned from the xblock_handler
* @param element The element into which to render the fragment (defaults to this.$el)
* @returns {jQuery promise} A promise representing the rendering process
* @returns {Promise} A promise representing the rendering process
*/
renderXBlockFragment: function(fragment, element) {
var html = fragment.html,
@@ -131,7 +132,7 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
* Dynamically loads all of an XBlock's dependent resources. This is an asynchronous
* process so a promise is returned.
* @param resources The resources to be rendered
* @returns {jQuery promise} A promise representing the rendering process
* @returns {Promise} A promise representing the rendering process
*/
addXBlockFragmentResources: function(resources) {
var self = this,
@@ -171,7 +172,7 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
/**
* Loads the specified resource into the page.
* @param resource The resource to be loaded.
* @returns {jQuery promise} A promise representing the loading of the resource.
* @returns {Promise} A promise representing the loading of the resource.
*/
loadResource: function(resource) {
var head = $('head'),
@@ -189,8 +190,7 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
if (kind === "text") {
head.append("<script>" + data + "</script>");
} else if (kind === "url") {
// Return a promise for the script resolution
return $.getScript(data);
return ViewUtils.loadJavaScript(data);
}
} else if (mimetype === "text/html") {
if (placement === "head") {
@@ -202,11 +202,11 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
},
fireNotificationActionEvent: function(event) {
var eventName = $(event.currentTarget).data("notification-action");
if (eventName) {
event.preventDefault();
this.notifyRuntime(eventName, this.model.get("id"));
}
var eventName = $(event.currentTarget).data("notification-action");
if (eventName) {
event.preventDefault();
this.notifyRuntime(eventName, this.model.get("id"));
}
}
});

View File

@@ -42,7 +42,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/backbone.paginator.min.js
- xmodule_js/common_static/js/vendor/backbone-relational.min.js
- xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker.js
- xmodule_js/common_static/js/vendor/jquery.leanModal.min.js
- xmodule_js/common_static/js/vendor/jquery.leanModal.js
- xmodule_js/common_static/js/vendor/jquery.ajaxQueue.js
- xmodule_js/common_static/js/vendor/jquery.form.js
- xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill.js

View File

@@ -40,7 +40,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/backbone-associations-min.js
- xmodule_js/common_static/js/vendor/backbone.paginator.min.js
- xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker.js
- xmodule_js/common_static/js/vendor/jquery.leanModal.min.js
- xmodule_js/common_static/js/vendor/jquery.leanModal.js
- xmodule_js/common_static/js/vendor/jquery.form.js
- xmodule_js/common_static/js/vendor/html5-input-polyfills/number-polyfill.js
- xmodule_js/common_static/js/vendor/sinon-1.17.0.js

View File

@@ -99,6 +99,37 @@
&:last-child {
margin-bottom: 0;
}
.settings-tab {
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l3;
li.settings-section {
display: inline-block;
margin-right: $baseline;
.general-settings-button,
.advanced-settings-button {
@extend %t-copy-sub1;
@extend %t-regular;
background-image: none;
background-color: $white;
color: $mediumGrey;
border-radius: 0;
box-shadow: none;
border: 0;
padding: ($baseline/4) ($baseline/2);
text-transform: uppercase;
&:hover {
background-color: $white;
color: $blue;
}
&.active {
border-bottom: 4px solid $blue-d2;
color: $offBlack;
}
}
}
}
}
.modal-section-title {
@@ -528,7 +559,8 @@
.wrapper-modal-window-bulkpublish-subsection,
.wrapper-modal-window-bulkpublish-unit,
.course-outline-modal {
.exam-time-list-fields {
.exam-time-list-fields,
.exam-review-rules-list-fields {
margin: 0 0 ($baseline/2) ($baseline/2);
}
.list-fields {

View File

@@ -16,9 +16,9 @@ from openedx.core.lib.js_utils import (
<%block name="title"></%block> |
% if context_course:
<% ctx_loc = context_course.location %>
${context_course.display_name_with_default | h} |
${context_course.display_name_with_default_escaped | h} |
% elif context_library:
${context_library.display_name_with_default | h} |
${context_library.display_name_with_default_escaped | h} |
% endif
${settings.STUDIO_NAME}
</title>
@@ -81,11 +81,11 @@ from openedx.core.lib.js_utils import (
require(['js/factories/course'], function(CourseFactory) {
CourseFactory({
id: "${escape_js_string(context_course.id) | n}",
name: "${context_course.display_name_with_default | h}",
name: "${context_course.display_name_with_default_escaped | h}",
url_name: "${context_course.location.name | h}",
org: "${context_course.location.org | h}",
num: "${context_course.location.course | h}",
display_course_number: "${_(context_course.display_number_with_default)}",
display_course_number: "${_(context_course.display_coursenumber)}",
revision: "${context_course.location.revision | h}",
self_paced: ${escape_json_dumps(context_course.self_paced) | n}
});

View File

@@ -30,7 +30,9 @@ CMS.User.isGlobalStaff = '${is_global_staff}'=='True' ? true : false;
<%block name="requirejs">
require(["js/certificates/factories/certificates_page_factory"], function(CertificatesPageFactory) {
CertificatesPageFactory(${escape_json_dumps(certificates) | n}, "${certificate_url}", "${course_outline_url}", ${escape_json_dumps(course_modes) | n}, ${escape_json_dumps(certificate_web_view_url) | n}, ${escape_json_dumps(is_active) | n}, ${escape_json_dumps(certificate_activation_handler_url) | n} );
if(${escape_json_dumps(has_certificate_modes)}) {
CertificatesPageFactory(${escape_json_dumps(certificates) | n}, "${certificate_url}", "${course_outline_url}", ${escape_json_dumps(course_modes) | n}, ${escape_json_dumps(certificate_web_view_url) | n}, ${escape_json_dumps(is_active) | n}, ${escape_json_dumps(certificate_activation_handler_url) | n} );
}
});
</%block>
@@ -56,6 +58,12 @@ CMS.User.isGlobalStaff = '${is_global_staff}'=='True' ? true : false;
${_("This module is not enabled.")}
</p>
</div>
% elif not has_certificate_modes:
<div class="notice notice-incontext notice-moduledisabled">
<p class="copy">
${_("This course does not use a mode that offers certificates.")}
</p>
</div>
% else:
<div class="ui-loading">
<p><span class="spin"><i class="icon fa fa-refresh" aria-hidden="true"></i></span> <span class="copy">${_("Loading")}</span></p>

View File

@@ -9,10 +9,10 @@ else:
</%def>
<%!
from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name
from django.utils.translation import ugettext as _
from openedx.core.lib.js_utils import escape_json_dumps
from util.markup import HTML, ugettext as _
%>
<%block name="title">${xblock.display_name_with_default} ${xblock_type_display_name(xblock) | h}</%block>
<%block name="title">${xblock.display_name_with_default_escaped} ${xblock_type_display_name(xblock) | h}</%block>
<%block name="bodyclass">is-signedin course container view-container</%block>
<%namespace name='static' file='static_content.html'/>
@@ -55,15 +55,15 @@ from openedx.core.lib.js_utils import escape_json_dumps
ancestor_url = xblock_studio_url(ancestor)
%>
% if ancestor_url:
<a href="${ancestor_url | h}" class="navigation-item navigation-link navigation-parent">${ancestor.display_name_with_default | h}</a>
<a href="${ancestor_url | h}" class="navigation-item navigation-link navigation-parent">${ancestor.display_name_with_default_escaped | h}</a>
% else:
<span class="navigation-item navigation-parent">${ancestor.display_name_with_default | h}</span>
<span class="navigation-item navigation-parent">${ancestor.display_name_with_default_escaped | h}</span>
% endif
% endfor
</small>
<div class="wrapper-xblock-field incontext-editor is-editable"
data-field="display_name" data-field-display-name="${_("Display Name")}">
<h1 class="page-header-title xblock-field-value incontext-editor-value"><span class="title-value">${xblock.display_name_with_default | h}</span></h1>
<h1 class="page-header-title xblock-field-value incontext-editor-value"><span class="title-value">${xblock.display_name_with_default_escaped | h}</span></h1>
</div>
</div>
@@ -110,10 +110,16 @@ from openedx.core.lib.js_utils import escape_json_dumps
% if xblock.category == 'split_test':
<div class="bit">
<h3 class="title-3">${_("Adding components")}</h3>
<p>${_("Select a component type under {em_start}Add New Component{em_end}. Then select a template.").format(em_start='<strong>', em_end="</strong>") | h}</p>
<p>${_("Select a component type under {strong_start}Add New Component{strong_end}. Then select a template.").format(
strong_start=HTML('<strong>'),
strong_end=HTML("</strong>"),
)}</p>
<p>${_("The new component is added at the bottom of the page or group. You can then edit and move the component.")}</p>
<h3 class="title-3">${_("Editing components")}</h3>
<p>${_("Click the {em_start}Edit{em_end} icon in a component to edit its content.").format(em_start='<strong>', em_end="</strong>") | h}</p>
<p>${_("Click the {strong_start}Edit{strong_end} icon in a component to edit its content.").format(
strong_start=HTML('<strong>'),
strong_end=HTML("</strong>"),
)}</p>
<h3 class="title-3">${_("Reorganizing components")}</h3>
<p>${_("Drag components to new locations within this component.")}</p>
<p>${_("For content experiments, you can drag components to other groups.")}</p>

View File

@@ -22,7 +22,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
<%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'verification-access-editor', 'timed-examination-preference-editor']:
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'settings-tab-section']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>

View File

@@ -1,7 +1,14 @@
<%
var enable_proctored_exams = enable_proctored_exams;
var enable_timed_exams = enable_timed_exams;
%>
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
<div class="message modal-introduction">
<p><%= introductionMessage %></p>
</div>
<% if (!( enable_proctored_exams || enable_timed_exams )) { %>
<div class="message modal-introduction">
<p><%- introductionMessage %></p>
</div>
<% } %>
<div class="modal-section"></div>
</div>

View File

@@ -0,0 +1,12 @@
<div class="wrapper-content wrapper">
<form id="login_form" method="post" action="login_post" onsubmit="return false;">
<input type="hidden" name="csrfmiddlewaretoken" value="csrf"/>
<input id="email" type="email" name="email" placeholder="'example: username@domain.com'"/>
<input id="password" type="password" name="password"/>
<div class="form-actions">
<button type="submit" id="submit" name="submit" class="action action-primary">Sign In</button>
</div>
<input name="honor_code" type="checkbox" value="true" checked="true" hidden="true">
</form>
</div>

View File

@@ -0,0 +1,10 @@
<ul class="settings-tab">
<li class="settings-section">
<button class="general-settings-button" href="#"><%- gettext('General Settings') %></button>
</li>
<li class="settings-section">
<button class="advanced-settings-button" href="#"><%- gettext('Advanced') %></button>
</li>
</ul>
<div class='general-settings'></div>
<div class='advanced-settings'></div>

View File

@@ -1,8 +1,5 @@
<form>
<h3 class="modal-section-title"><%- gettext('Additional Options:') %></h3>
<div class="modal-section-content has-actions">
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<li class="field-radio">
@@ -22,7 +19,7 @@
<%- gettext('Timed') %>
</label>
</li>
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time on per learner basis through the Instructor Dashboard.') %> </p>
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the Instructor Dashboard.') %> </p>
</ul>
</div>
@@ -35,7 +32,7 @@
<%- gettext('Proctored') %>
</label>
</li>
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed, and software records video of each learner taking the exam. These videos are then reviewed by a third party.') %> </p>
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules.') %> </p>
</ul>
</div>
<div class='exam-time-list-fields'>
@@ -46,7 +43,7 @@
<%- gettext('Practice Proctored') %>
</label>
</li>
<p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of the practice exam do not count towards the learner's grade.") %> </p>
<p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner's grade.") %> </p>
</ul>
</div>
<% } %>
@@ -58,7 +55,17 @@
value="" aria-describedby="time-limit-description"
placeholder="HH:MM" class="time_limit release-time time input input-text" autocomplete="off" />
</li>
<p class='field-message' id='time-limit-description'><%- gettext('Learners see warnings when 20% and 5% of the allotted time remains. You can grant learners extra time to complete the exam through the Instructor Dashboard.') %></p>
<p class='field-message' id='time-limit-description'><%- gettext('Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.') %></p>
</ul>
</div>
<div class='exam-review-rules-list-fields is-hidden'>
<ul class="list-fields list-input exam-review-rules">
<li class="field field-text field-exam-review-rules">
<label for="id_exam_review_rules" class="label"><%- gettext('Review Rules') %> </label>
<textarea id="id_exam_review_rules" cols="50" maxlength="255" name="review_rules" aria-describedby="review-rules-description"
class="review-rules input input-text" autocomplete="off" />
</li>
<p class='field-message' id='review-rules-description'><%- gettext('Specify any additional rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed.') %></p>
</ul>
</div>
</div>

View File

@@ -5,7 +5,7 @@ from contentstore.views.helpers import xblock_studio_url, xblock_type_display_na
from django.utils.translation import ugettext as _
from openedx.core.lib.js_utils import escape_json_dumps
%>
<%block name="title">${context_library.display_name_with_default} ${xblock_type_display_name(context_library)}</%block>
<%block name="title">${context_library.display_name_with_default_escaped} ${xblock_type_display_name(context_library)}</%block>
<%block name="bodyclass">is-signedin course container view-container view-library</%block>
<%namespace name='static' file='static_content.html'/>
@@ -45,7 +45,7 @@ from openedx.core.lib.js_utils import escape_json_dumps
<small class="subtitle">${_("Content Library")}</small>
<div class="wrapper-xblock-field incontext-editor is-editable"
data-field="display_name" data-field-display-name="${_("Display Name")}">
<h1 class="page-header-title xblock-field-value incontext-editor-value"><span class="title-value">${context_library.display_name_with_default}</span></h1>
<h1 class="page-header-title xblock-field-value incontext-editor-value"><span class="title-value">${context_library.display_name_with_default_escaped}</span></h1>
</div>
</div>

View File

@@ -107,7 +107,7 @@ from openedx.core.lib.js_utils import escape_json_dumps
<%block name="requirejs">
require(["js/factories/manage_users_lib"], function(ManageLibraryUsersFactory) {
ManageLibraryUsersFactory(
"${context_library.display_name_with_default | h}",
"${context_library.display_name_with_default_escaped | h}",
${escape_json_dumps(users) | n},
"${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'})}",
${ request.user.id },

View File

@@ -12,5 +12,5 @@
</%block>
<%block name="content">
<div class="js-program-admin program-app layout-1q3q layout-reversed" data-api-url=${programs_api_url} data-auth-url=${programs_token_url} data-home-url=${studio_home_url}></div>
<div class="js-program-admin program-app layout-1q3q layout-reversed" data-home-url=${studio_home_url} data-lms-base-url=${lms_base_url} data-programs-api-url=${programs_api_url} data-auth-url=${programs_token_url}></div>
</%block>

View File

@@ -89,10 +89,10 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
<li class="action-item">
<%
email_subject = urllib.quote(_("Enroll in {course_display_name}").format(
course_display_name = context_course.display_name_with_default
course_display_name = context_course.display_name_with_default_escaped
).encode("utf-8"))
email_body = urllib.quote(_('The course "{course_display_name}", provided by {platform_name}, is open for enrollment. Please navigate to this course at {link_for_about_page} to enroll.').format(
course_display_name = context_course.display_name_with_default,
course_display_name = context_course.display_name_with_default_escaped,
platform_name = settings.PLATFORM_NAME,
link_for_about_page = link_for_about_page
).encode("utf-8"))

View File

@@ -9,7 +9,7 @@ xblock_url = xblock_studio_url(xblock)
show_inline = xblock.has_children and not xblock_url
section_class = "level-nesting" if show_inline else "level-element"
collapsible_class = "is-collapsible" if xblock.has_children else ""
label = xblock.display_name_with_default or xblock.scope_ids.block_type
label = xblock.display_name_with_default_escaped or xblock.scope_ids.block_type
messages = xblock.validate().to_json()
%>

View File

@@ -38,7 +38,7 @@
<span class="sr">${_("Current Course:")}</span>
<a class="course-link" href="${index_url}">
<span class="course-org">${context_course.display_org_with_default | h}</span><span class="course-number">${context_course.display_number_with_default | h}</span>
<span class="course-title" title="${context_course.display_name_with_default}">${context_course.display_name_with_default}</span>
<span class="course-title" title="${context_course.display_name_with_default_escaped}">${context_course.display_name_with_default_escaped}</span>
</a>
</h2>
@@ -141,7 +141,7 @@
<span class="sr">${_("Current Library:")}</span>
<a class="course-link" href="${index_url}">
<span class="course-org">${context_library.display_org_with_default | h}</span><span class="course-number">${context_library.display_number_with_default | h}</span>
<span class="course-title" title="${context_library.display_name_with_default}">${context_library.display_name_with_default}</span>
<span class="course-title" title="${context_library.display_name_with_default_escaped}">${context_library.display_name_with_default_escaped}</span>
</a>
</h2>

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_modes', '0004_auto_20151113_1457'),
]
operations = [
migrations.RemoveField(
model_name='coursemode',
name='expiration_datetime',
),
migrations.AddField(
model_name='coursemode',
name='_expiration_datetime',
field=models.DateTimeField(db_column=b'expiration_datetime', default=None, blank=True, help_text='OPTIONAL: After this date/time, users will no longer be able to enroll in this mode. Leave this blank if users can enroll in this mode until enrollment closes for the course.', null=True, verbose_name='Upgrade Deadline'),
),
]

View File

@@ -159,7 +159,9 @@ class CourseMode(models.Model):
@expiration_datetime.setter
def expiration_datetime(self, new_datetime):
""" Saves datetime to _expiration_datetime and sets the explicit flag. """
self.expiration_datetime_is_explicit = True
# Only set explicit flag if we are setting an actual date.
if new_datetime is not None:
self.expiration_datetime_is_explicit = True
self._expiration_datetime = new_datetime
@classmethod

View File

@@ -421,3 +421,12 @@ class CourseModeModelTest(TestCase):
self.assertFalse(verified_mode.expiration_datetime_is_explicit)
self.assertEqual(verified_mode.expiration_datetime, now)
def test_expiration_datetime_explicitly_set_to_none(self):
""" Verify that setting the _expiration_date property does not set the explicit flag. """
verified_mode, __ = self.create_mode('verified', 'Verified Certificate')
self.assertFalse(verified_mode.expiration_datetime_is_explicit)
verified_mode.expiration_datetime = None
self.assertFalse(verified_mode.expiration_datetime_is_explicit)
self.assertIsNone(verified_mode.expiration_datetime)

View File

@@ -120,7 +120,7 @@ class ChooseModeView(View):
"course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}),
"modes": modes,
"has_credit_upsell": has_credit_upsell,
"course_name": course.display_name_with_default,
"course_name": course.display_name_with_default_escaped,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"chosen_price": chosen_price,

View File

@@ -8,11 +8,12 @@ from django.contrib.auth.models import User
from opaque_keys.edx.keys import CourseKey
from enrollment.errors import (
CourseNotFoundError, CourseEnrollmentClosedError, CourseEnrollmentFullError,
CourseEnrollmentClosedError, CourseEnrollmentFullError,
CourseEnrollmentExistsError, UserNotFoundError, InvalidEnrollmentAttribute
)
from enrollment.serializers import CourseEnrollmentSerializer, CourseSerializer
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.exceptions import CourseNotFoundError
from student.models import (
CourseEnrollment, NonExistentCourseError, EnrollmentClosedError,
CourseFullError, AlreadyEnrolledError, CourseEnrollmentAttribute

View File

@@ -13,10 +13,6 @@ class CourseEnrollmentError(Exception):
self.data = data
class CourseNotFoundError(CourseEnrollmentError):
pass
class UserNotFoundError(CourseEnrollmentError):
pass

View File

@@ -11,9 +11,10 @@ from django.conf import settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from enrollment.errors import (
CourseNotFoundError, UserNotFoundError, CourseEnrollmentClosedError,
UserNotFoundError, CourseEnrollmentClosedError,
CourseEnrollmentFullError, CourseEnrollmentExistsError,
)
from openedx.core.lib.exceptions import CourseNotFoundError
from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, AlreadyEnrolledError
from enrollment import data

View File

@@ -11,24 +11,24 @@ from .views import (
EnrollmentCourseDetailView
)
USERNAME_PATTERN = '(?P<username>[\w.@+-]+)'
urlpatterns = patterns(
'enrollment.views',
url(
r'^enrollment/{username},{course_key}$'.format(username=USERNAME_PATTERN,
course_key=settings.COURSE_ID_PATTERN),
r'^enrollment/{username},{course_key}/$'.format(
username=settings.USERNAME_PATTERN, course_key=settings.COURSE_ID_PATTERN
),
EnrollmentView.as_view(),
name='courseenrollment'
),
url(
r'^enrollment/{course_key}$'.format(course_key=settings.COURSE_ID_PATTERN),
r'^enrollment/{course_key}/$'.format(course_key=settings.COURSE_ID_PATTERN),
EnrollmentView.as_view(),
name='courseenrollment'
),
url(r'^enrollment$', EnrollmentListView.as_view(), name='courseenrollments'),
url(
r'^course/{course_key}$'.format(course_key=settings.COURSE_ID_PATTERN),
r'^course/{course_key}/$'.format(course_key=settings.COURSE_ID_PATTERN),
EnrollmentCourseDetailView.as_view(),
name='courseenrollmentdetails'
),

View File

@@ -24,11 +24,13 @@ from openedx.core.lib.api.authentication import (
SessionAuthenticationAllowInactiveUser,
OAuth2AuthenticationAllowInactiveUser,
)
from openedx.core.lib.exceptions import CourseNotFoundError
from util.disable_rate_limit import can_disable_rate_limit
from enrollment import api
from enrollment.errors import (
CourseNotFoundError, CourseEnrollmentError,
CourseModeNotFoundError, CourseEnrollmentExistsError
CourseEnrollmentError,
CourseModeNotFoundError,
CourseEnrollmentExistsError
)
from student.auth import user_has_role
from student.models import User

View File

@@ -5,6 +5,7 @@ from django.utils.translation import get_language_bidi
from mako.exceptions import TemplateLookupException
from microsite_configuration import microsite
from certificates.api import get_asset_url_by_slug
%>
<%def name='url(file, raw=False)'><%
@@ -14,6 +15,13 @@ except:
url = file
%>${url}${"?raw" if raw else ""}</%def>
<%def name='certificate_asset_url(slug)'><%
try:
url = get_asset_url_by_slug(slug)
except:
url = ''
%>${url}</%def>
<%def name='css(group, raw=False)'>
<%
rtl_group = '{}-rtl'.format(group)

View File

@@ -504,8 +504,29 @@ class Registration(models.Model):
def activate(self):
self.user.is_active = True
self._track_activation()
self.user.save()
def _track_activation(self):
""" Update the isActive flag in mailchimp for activated users."""
has_segment_key = getattr(settings, 'LMS_SEGMENT_KEY', None)
has_mailchimp_id = hasattr(settings, 'MAILCHIMP_NEW_USER_LIST_ID')
if has_segment_key and has_mailchimp_id:
identity_args = [
self.user.id, # pylint: disable=no-member
{
'email': self.user.email,
'username': self.user.username,
'activated': 1,
},
{
"MailChimp": {
"listId": settings.MAILCHIMP_NEW_USER_LIST_ID
}
}
]
analytics.identify(*identity_args)
class PendingNameChange(models.Model):
user = models.OneToOneField(User, unique=True, db_index=True)

View File

@@ -0,0 +1,78 @@
"""Tests for account activation"""
from mock import patch
import unittest
from django.conf import settings
from django.contrib.auth.models import User
from django.test import TestCase, override_settings
from student.models import Registration
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestActivateAccount(TestCase):
"""Tests for account creation"""
def setUp(self):
super(TestActivateAccount, self).setUp()
self.username = "jack"
self.email = "jack@fake.edx.org"
self.user = User.objects.create(username=self.username, email=self.email, is_active=False)
# Set Up Registration
self.registration = Registration()
self.registration.register(self.user)
self.registration.save()
def assert_no_tracking(self, mock_segment_identify):
""" Assert that activate sets the flag but does not call segment. """
# Ensure that the user starts inactive
self.assertFalse(self.user.is_active)
# Until you explicitly activate it
self.registration.activate()
self.assertTrue(self.user.is_active)
self.assertFalse(mock_segment_identify.called)
@override_settings(
LMS_SEGMENT_KEY="testkey",
MAILCHIMP_NEW_USER_LIST_ID="listid"
)
@patch('student.models.analytics.identify')
def test_activation_with_keys(self, mock_segment_identify):
expected_segment_payload = {
'email': self.email,
'username': self.username,
'activated': 1,
}
expected_segment_mailchimp_list = {
"MailChimp": {
"listId": settings.MAILCHIMP_NEW_USER_LIST_ID
}
}
# Ensure that the user starts inactive
self.assertFalse(self.user.is_active)
# Until you explicitly activate it
self.registration.activate()
self.assertTrue(self.user.is_active)
mock_segment_identify.assert_called_with(
self.user.id,
expected_segment_payload,
expected_segment_mailchimp_list
)
@override_settings(LMS_SEGMENT_KEY="testkey")
@patch('student.models.analytics.identify')
def test_activation_without_mailchimp_key(self, mock_segment_identify):
self.assert_no_tracking(mock_segment_identify)
@override_settings(MAILCHIMP_NEW_USER_LIST_ID="listid")
@patch('student.models.analytics.identify')
def test_activation_without_segment_key(self, mock_segment_identify):
self.assert_no_tracking(mock_segment_identify)
@patch('student.models.analytics.identify')
def test_activation_without_keys(self, mock_segment_identify):
self.assert_no_tracking(mock_segment_identify)

View File

@@ -133,6 +133,9 @@ AUDIT_LOG = logging.getLogger("audit")
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display') # pylint: disable=invalid-name
SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated'
# Disable this warning because it doesn't make sense to completely refactor tests to appease Pylint
# pylint: disable=logging-format-interpolation
def csrf_token(context):
"""A csrf token that can be included in a form."""
@@ -156,13 +159,9 @@ def index(request, extra_context=None, user=AnonymousUser()):
"""
if extra_context is None:
extra_context = {}
# The course selection work is done in courseware.courses.
domain = settings.FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
# do explicit check, because domain=None is valid
if domain is False:
domain = request.META.get('HTTP_HOST')
courses = get_courses(user, domain=domain)
courses = get_courses(user)
if microsite.get_value("ENABLE_COURSE_SORTING_BY_START_DATE",
settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]):
courses = sort_by_start_date(courses)
@@ -300,12 +299,13 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
default_status = 'processing'
default_info = {'status': default_status,
'show_disabled_download_button': False,
'show_download_url': False,
'show_survey_button': False,
'can_unenroll': True
}
default_info = {
'status': default_status,
'show_disabled_download_button': False,
'show_download_url': False,
'show_survey_button': False,
'can_unenroll': True,
}
if cert_status is None:
return default_info
@@ -504,9 +504,14 @@ def is_course_blocked(request, redeemed_registration_codes, course_key):
u"User %s (%s) opted out of receiving emails from course %s",
request.user.username,
request.user.email,
course_key
course_key,
)
track.views.server_track(
request,
"change-email1-settings",
{"receive_emails": "no", "course": course_key.to_deprecated_string()},
page='dashboard',
)
track.views.server_track(request, "change-email1-settings", {"receive_emails": "no", "course": course_key.to_deprecated_string()}, page='dashboard')
break
return blocked
@@ -729,7 +734,7 @@ def _create_recent_enrollment_message(course_enrollments, course_modes): # pyli
recently_enrolled_courses = _get_recently_enrolled_courses(course_enrollments)
if recently_enrolled_courses:
messages = [
enroll_messages = [
{
"course_id": enrollment.course_overview.id,
"course_name": enrollment.course_overview.display_name,
@@ -742,7 +747,7 @@ def _create_recent_enrollment_message(course_enrollments, course_modes): # pyli
return render_to_string(
'enrollment/course_enrollment_message.html',
{'course_enrollment_messages': messages, 'platform_name': platform_name}
{'course_enrollment_messages': enroll_messages, 'platform_name': platform_name}
)
@@ -781,7 +786,11 @@ def _allow_donation(course_modes, course_id, enrollment):
"""
donations_enabled = DonationConfiguration.current().enabled
return donations_enabled and enrollment.mode in course_modes[course_id] and course_modes[course_id][enrollment.mode].min_price == 0
return (
donations_enabled and
enrollment.mode in course_modes[course_id] and
course_modes[course_id][enrollment.mode].min_price == 0
)
def _update_email_opt_in(request, org):
@@ -1015,7 +1024,7 @@ def change_enrollment(request, check_access=True):
enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes)
if enroll_mode:
CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode)
except Exception:
except Exception: # pylint: disable=broad-except
return HttpResponseBadRequest(_("Could not enroll"))
# If we have more than one course mode or professional ed is enabled,
@@ -1077,32 +1086,43 @@ def login_user(request, error=""): # pylint: disable=too-many-statements,unused
third_party_auth_successful = True
except User.DoesNotExist:
AUDIT_LOG.warning(
u'Login failed - user with username {username} has no social auth with backend_name {backend_name}'.format(
username=username, backend_name=backend_name))
return HttpResponse(
_("You've successfully logged into your {provider_name} account, but this account isn't linked with an {platform_name} account yet.").format(
platform_name=platform_name, provider_name=requested_provider.name
)
+ "<br/><br/>" +
_("Use your {platform_name} username and password to log into {platform_name} below, "
"and then link your {platform_name} account with {provider_name} from your dashboard.").format(
platform_name=platform_name, provider_name=requested_provider.name
)
+ "<br/><br/>" +
_("If you don't have an {platform_name} account yet, "
"click <strong>Register</strong> at the top of the page.").format(
platform_name=platform_name),
content_type="text/plain",
status=403
u"Login failed - user with username {username} has no social auth "
"with backend_name {backend_name}".format(
username=username, backend_name=backend_name)
)
message = _(
"You've successfully logged into your {provider_name} account, "
"but this account isn't linked with an {platform_name} account yet."
).format(
platform_name=platform_name,
provider_name=requested_provider.name,
)
message += "<br/><br/>"
message += _(
"Use your {platform_name} username and password to log into {platform_name} below, "
"and then link your {platform_name} account with {provider_name} from your dashboard."
).format(
platform_name=platform_name,
provider_name=requested_provider.name,
)
message += "<br/><br/>"
message += _(
"If you don't have an {platform_name} account yet, "
"click <strong>Register</strong> at the top of the page."
).format(
platform_name=platform_name
)
return HttpResponse(message, content_type="text/plain", status=403)
else:
if 'email' not in request.POST or 'password' not in request.POST:
return JsonResponse({
"success": False,
"value": _('There was an error receiving your login information. Please email us.'), # TODO: User error message
}) # TODO: this should be status code 400 # pylint: disable=fixme
# TODO: User error message
"value": _('There was an error receiving your login information. Please email us.'),
}) # TODO: this should be status code 400
email = request.POST['email']
password = request.POST['password']
@@ -1133,9 +1153,11 @@ def login_user(request, error=""): # pylint: disable=too-many-statements,unused
user_found_by_email_lookup = user
if user_found_by_email_lookup and LoginFailures.is_feature_enabled():
if LoginFailures.is_user_locked_out(user_found_by_email_lookup):
lockout_message = _('This account has been temporarily locked due '
'to excessive login failures. Try again later.')
return JsonResponse({
"success": False,
"value": _('This account has been temporarily locked due to excessive login failures. Try again later.'),
"value": lockout_message,
}) # TODO: this should be status code 429 # pylint: disable=fixme
# see if the user must reset his/her password due to any policy settings
@@ -1243,7 +1265,8 @@ def login_user(request, error=""): # pylint: disable=too-many-statements,unused
AUDIT_LOG.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))
reactivation_email_for_user(user)
not_activated_msg = _("This account has not been activated. We have sent another activation message. Please check your email for the activation instructions.")
not_activated_msg = _("This account has not been activated. We have sent another activation "
"message. Please check your email for the activation instructions.")
return JsonResponse({
"success": False,
"value": not_activated_msg,
@@ -1612,7 +1635,7 @@ def create_account_with_params(request, params):
if settings.FEATURES.get('ENABLE_DISCUSSION_EMAIL_DIGEST'):
try:
enable_notifications(user)
except Exception:
except Exception: # pylint: disable=broad-except
log.exception("Enable discussion notifications failed for user {id}.".format(id=user.id))
dog_stats_api.increment("common.student.account_created")
@@ -2014,9 +2037,9 @@ def password_reset(request):
def password_reset_confirm_wrapper(
request,
uidb36=None,
token=None,
request,
uidb36=None,
token=None,
):
""" A wrapper around django.contrib.auth.views.password_reset_confirm.
Needed because we want to set the user as active at this step.
@@ -2050,6 +2073,8 @@ def password_reset_confirm_wrapper(
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE']
else:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE']
# Because of how ngettext is, splitting the following into shorter lines would be ugly.
# pylint: disable=line-too-long
err_msg = ungettext(
"You are re-using a password that you have used recently. You must have {num} distinct password before reusing a previous password.",
"You are re-using a password that you have used recently. You must have {num} distinct passwords before reusing a previous password.",
@@ -2059,6 +2084,8 @@ def password_reset_confirm_wrapper(
# also, check to see if passwords are getting reset too frequent
if PasswordHistory.is_password_reset_too_soon(user):
num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
# Because of how ngettext is, splitting the following into shorter lines would be ugly.
# pylint: disable=line-too-long
err_msg = ungettext(
"You are resetting passwords too frequently. Due to security policies, {num} day must elapse between password resets.",
"You are resetting passwords too frequently. Due to security policies, {num} days must elapse between password resets.",
@@ -2291,18 +2318,28 @@ def change_email_settings(request):
u"User %s (%s) opted in to receive emails from course %s",
user.username,
user.email,
course_id
course_id,
)
track.views.server_track(
request,
"change-email-settings",
{"receive_emails": "yes", "course": course_id},
page='dashboard',
)
track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard')
else:
Optout.objects.get_or_create(user=user, course_id=course_key)
log.info(
u"User %s (%s) opted out of receiving emails from course %s",
user.username,
user.email,
course_id
course_id,
)
track.views.server_track(
request,
"change-email-settings",
{"receive_emails": "no", "course": course_id},
page='dashboard',
)
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
return JsonResponse({"success": True})

View File

@@ -38,8 +38,7 @@ class LoggerBackend(BaseBackend):
event_str = json.dumps(event, cls=DateTimeJSONEncoder)
except UnicodeDecodeError:
application_log.exception(
"UnicodeDecodeError Event_type: %r, Event_source: %r, Page: %r, Referer: %r",
event.get('event_type'), event.get('event_source'), event.get('page'), event.get('referer')
"UnicodeDecodeError Event_data: %r", event
)
raise

View File

@@ -0,0 +1,51 @@
"""
Utilities for use in Mako markup.
"""
from django.utils.translation import ugettext as django_ugettext
from django.utils.translation import ungettext as django_ungettext
import markupsafe
# So that we can use escape() imported from here.
escape = markupsafe.escape # pylint: disable=invalid-name
def ugettext(text):
"""Translate a string, and escape it as plain text.
Use like this in Mako::
<% from util.markup import ugettext as _ %>
<p>${_("Hello, world!")}</p>
Or with formatting::
<% from util.markup import HTML, ugettext as _ %>
${_("Write & send {start}email{end}").format(
start=HTML("<a href='mailto:ned@edx.org'>"),
end=HTML("</a>"),
)}
"""
return markupsafe.escape(django_ugettext(text))
def ungettext(text1, text2, num):
"""Translate a number-sensitive string, and escape it as plain text."""
return markupsafe.escape(django_ungettext(text1, text2, num))
def HTML(html): # pylint: disable=invalid-name
"""Mark a string as already HTML, so that it won't be escaped before output.
Use this when formatting HTML into other strings::
<% from util.markup import HTML, ugettext as _ %>
${_("Write & send {start}email{end}").format(
start=HTML("<a href='mailto:ned@edx.org'>"),
end=HTML("</a>"),
)}
"""
return markupsafe.Markup(html)

View File

@@ -186,7 +186,13 @@ def fulfill_course_milestone(course_key, user):
if not settings.FEATURES.get('MILESTONES_APP', False):
return None
from milestones import api as milestones_api
course_milestones = milestones_api.get_course_milestones(course_key=course_key, relationship="fulfills")
from milestones.exceptions import InvalidMilestoneRelationshipTypeException
try:
course_milestones = milestones_api.get_course_milestones(course_key=course_key, relationship="fulfills")
except InvalidMilestoneRelationshipTypeException:
# we have not seeded milestone relationship types
seed_milestone_relationship_types()
course_milestones = milestones_api.get_course_milestones(course_key=course_key, relationship="fulfills")
for milestone in course_milestones:
milestones_api.add_user_milestone({'id': user.id}, milestone)

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
"""
Tests for util.markup
"""
import unittest
import ddt
from edxmako.template import Template
from util.markup import escape, HTML, ugettext as _, ungettext
@ddt.ddt
class FormatHtmlTest(unittest.TestCase):
"""Test that we can format plain strings and HTML into them properly."""
@ddt.data(
(u"hello", u"hello"),
(u"<hello>", u"&lt;hello&gt;"),
(u"It's cool", u"It&#39;s cool"),
(u'"cool," she said.', u'&#34;cool,&#34; she said.'),
(u"Stop & Shop", u"Stop &amp; Shop"),
(u"<a>нтмℓ-єѕ¢αρє∂</a>", u"&lt;a&gt;нтмℓ-єѕ¢αρє∂&lt;/a&gt;"),
)
def test_simple(self, (before, after)):
self.assertEqual(unicode(_(before)), after) # pylint: disable=translation-of-non-string
self.assertEqual(unicode(escape(before)), after)
def test_formatting(self):
# The whole point of this function is to make sure this works:
out = _(u"Point & click {start}here{end}!").format(
start=HTML("<a href='http://edx.org'>"),
end=HTML("</a>"),
)
self.assertEqual(
unicode(out),
u"Point &amp; click <a href='http://edx.org'>here</a>!",
)
def test_nested_formatting(self):
# Sometimes, you have plain text, with html inserted, and the html has
# plain text inserted. It gets twisty...
out = _(u"Send {start}email{end}").format(
start=HTML("<a href='mailto:{email}'>").format(email="A&B"),
end=HTML("</a>"),
)
self.assertEqual(
unicode(out),
u"Send <a href='mailto:A&amp;B'>email</a>",
)
def test_mako(self):
# The default_filters used here have to match the ones in edxmako.
template = Template(
"""
<%! from util.markup import HTML, ugettext as _ %>
${_(u"A & {BC}").format(BC=HTML("B & C"))}
""",
default_filters=['decode.utf8', 'h'],
)
out = template.render({})
self.assertEqual(out.strip(), u"A &amp; B & C")
def test_ungettext(self):
for i in [1, 2]:
out = ungettext("1 & {}", "2 & {}", i).format(HTML("<>"))
self.assertEqual(out, "{} &amp; <>".format(i))

View File

@@ -7,7 +7,8 @@ of a variety of types.
Used by capa_problem.py
"""
# TODO: Refactor this code and fix this issue.
# pylint: disable=attribute-defined-outside-init
# standard library imports
import abc
import cgi
@@ -541,7 +542,7 @@ class LoncapaResponse(object):
# If we can't do that, create the <div> and set the message
# as the text of the <div>
except:
except Exception: # pylint: disable=broad-except
response_msg_div = etree.Element('div')
response_msg_div.text = str(response_msg)
@@ -1225,7 +1226,6 @@ class MultipleChoiceResponse(LoncapaResponse):
i = 0
for response in self.xml.xpath("choicegroup"):
# Is Masking enabled? -- check for shuffle or answer-pool features
ans_str = response.get("answer-pool")
# Masking (self._has_mask) is off, to be re-enabled with a future PR.
rtype = response.get('type')
if rtype not in ["MultipleChoice"]:
@@ -1240,12 +1240,15 @@ class MultipleChoiceResponse(LoncapaResponse):
i += 1
# If using the masked name, e.g. mask_0, save the regular name
# to support unmasking later (for the logs).
if self.has_mask():
mask_name = "mask_" + str(mask_ids.pop())
self._mask_dict[mask_name] = name
choice.set("name", mask_name)
else:
choice.set("name", name)
# Masking is currently disabled so this code is commented, as
# the variable `mask_ids` is not defined. (the feature appears to not be fully implemented)
# The original work for masking was done by Nick Parlante as part of the OLI Hinting feature.
# if self.has_mask():
# mask_name = "mask_" + str(mask_ids.pop())
# self._mask_dict[mask_name] = name
# choice.set("name", mask_name)
# else:
choice.set("name", name)
def late_transforms(self, problem):
"""
@@ -1338,12 +1341,13 @@ class MultipleChoiceResponse(LoncapaResponse):
Given a masked name, e.g. mask_2, returns the regular name, e.g. choice_0.
Fails with LoncapaProblemError if called on a response that is not masking.
"""
if not self.has_mask():
_ = self.capa_system.i18n.ugettext
# Translators: 'unmask_name' is a method name and should not be translated.
msg = _("unmask_name called on response that is not masked")
raise LoncapaProblemError(msg)
return self._mask_dict[name]
# if not self.has_mask():
# _ = self.capa_system.i18n.ugettext
# # Translators: 'unmask_name' is a method name and should not be translated.
# msg = "unmask_name called on response that is not masked"
# raise LoncapaProblemError(msg)
# return self._mask_dict[name] # TODO: this is not defined
raise NotImplementedError()
def unmask_order(self):
"""
@@ -1750,7 +1754,9 @@ class NumericalResponse(LoncapaResponse):
student_float = evaluator({}, {}, student_answer)
except UndefinedVariable as undef_var:
raise StudentInputError(
_(u"You may not use variables ({bad_variables}) in numerical problems.").format(bad_variables=undef_var.message)
_(u"You may not use variables ({bad_variables}) in numerical problems.").format(
bad_variables=undef_var.message,
)
)
except ValueError as val_err:
if 'factorial' in val_err.message:
@@ -1802,13 +1808,17 @@ class NumericalResponse(LoncapaResponse):
for inclusion, answer in zip(self.inclusion, self.answer_range):
boundary = self.get_staff_ans(answer)
if boundary.imag != 0:
# Translators: This is an error message for a math problem. If the instructor provided a boundary
# (end limit) for a variable that is a complex number (a + bi), this message displays.
raise StudentInputError(_("There was a problem with the staff answer to this problem: complex boundary."))
raise StudentInputError(
# Translators: This is an error message for a math problem. If the instructor provided a
# boundary (end limit) for a variable that is a complex number (a + bi), this message displays.
_("There was a problem with the staff answer to this problem: complex boundary.")
)
if isnan(boundary):
# Translators: This is an error message for a math problem. If the instructor did not provide
# a boundary (end limit) for a variable, this message displays.
raise StudentInputError(_("There was a problem with the staff answer to this problem: empty boundary."))
raise StudentInputError(
# Translators: This is an error message for a math problem. If the instructor did not
# provide a boundary (end limit) for a variable, this message displays.
_("There was a problem with the staff answer to this problem: empty boundary.")
)
boundaries.append(boundary.real)
if compare_with_tolerance(
student_float,
@@ -2164,7 +2174,8 @@ class StringResponse(LoncapaResponse):
def get_answers(self):
_ = self.capa_system.i18n.ugettext
# Translators: Separator used in StringResponse to display multiple answers. Example: "Answer: Answer_1 or Answer_2 or Answer_3".
# Translators: Separator used in StringResponse to display multiple answers.
# Example: "Answer: Answer_1 or Answer_2 or Answer_3".
separator = u' <b>{}</b> '.format(_('or'))
return {self.answer_id: separator.join(self.correct_answer)}
@@ -2280,7 +2291,9 @@ class CustomResponse(LoncapaResponse):
submission = [student_answers[k] for k in idset]
except Exception as err:
msg = u"[courseware.capa.responsetypes.customresponse] {message}\n idset = {idset}, error = {err}".format(
message=_("error getting student answer from {student_answers}").format(student_answers=student_answers),
message=_("error getting student answer from {student_answers}").format(
student_answers=student_answers,
),
idset=idset,
err=err
)
@@ -2392,20 +2405,20 @@ class CustomResponse(LoncapaResponse):
random_seed=self.context['seed'],
unsafely=self.capa_system.can_execute_unsafe_code(),
)
except Exception as err:
except Exception as err: # pylint: disable=broad-except
self._handle_exec_exception(err)
else:
# self.code is not a string; it's a function we created earlier.
# this is an interface to the Tutor2 check functions
fn = self.code
tutor_cfn = self.code
answer_given = submission[0] if (len(idset) == 1) else submission
kwnames = self.xml.get("cfn_extra_args", "").split()
kwargs = {n: self.context.get(n) for n in kwnames}
log.debug(" submission = %s", submission)
try:
ret = fn(self.expect, answer_given, **kwargs)
ret = tutor_cfn(self.expect, answer_given, **kwargs)
except Exception as err: # pylint: disable=broad-except
self._handle_exec_exception(err)
log.debug(
@@ -2928,15 +2941,17 @@ class CodeResponse(LoncapaResponse):
# Next, we need to check that the contents of the external grader message is safe for the LMS.
# 1) Make sure that the message is valid XML (proper opening/closing tags)
# 2) If it is not valid XML, make sure it is valid HTML. Note: html5lib parser will try to repair any broken HTML
# For example: <aaa></bbb> will become <aaa/>.
# 2) If it is not valid XML, make sure it is valid HTML.
# Note: html5lib parser will try to repair any broken HTML
# For example: <aaa></bbb> will become <aaa/>.
msg = score_result['msg']
try:
etree.fromstring(msg)
except etree.XMLSyntaxError as _err:
# If `html` contains attrs with no values, like `controls` in <audio controls src='smth'/>,
# XML parser will raise exception, so wee fallback to html5parser, which will set empty "" values for such attrs.
# XML parser will raise exception, so wee fallback to html5parser,
# which will set empty "" values for such attrs.
try:
parsed = html5lib.parseFragment(msg, treebuilder='lxml', namespaceHTMLElements=False)
except ValueError:
@@ -3612,11 +3627,13 @@ class AnnotationResponse(LoncapaResponse):
def _find_options(self, inputfield):
"""Returns an array of dicts where each dict represents an option. """
elements = inputfield.findall('./options/option')
return [{
return [
{
'id': index,
'description': option.text,
'choice': option.get('choice')
} for (index, option) in enumerate(elements)]
} for (index, option) in enumerate(elements)
]
def _find_option_with_choice(self, inputfield, choice):
"""Returns the option with the given choice value, otherwise None. """
@@ -3663,10 +3680,11 @@ class ChoiceTextResponse(LoncapaResponse):
human_name = _('Checkboxes With Text Input')
tags = ['choicetextresponse']
max_inputfields = 1
allowed_inputfields = ['choicetextgroup',
'checkboxtextgroup',
'radiotextgroup'
]
allowed_inputfields = [
'choicetextgroup',
'checkboxtextgroup',
'radiotextgroup',
]
def __init__(self, *args, **kwargs):
self.correct_inputs = {}
@@ -3771,9 +3789,8 @@ class ChoiceTextResponse(LoncapaResponse):
</radiotextgroup>
"""
for index, choice in enumerate(
self.xml.xpath('//*[@id=$id]//choice', id=self.xml.get('id'))
):
choices = self.xml.xpath('//*[@id=$id]//choice', id=self.xml.get('id'))
for index, choice in enumerate(choices):
# Set the name attribute for <choices>
# "bc" is appended at the end to indicate that this is a
# binary choice as opposed to a numtolerance_input, this convention

View File

@@ -9,16 +9,17 @@ from .chemcalc import (
chemical_equations_equal,
)
import miller
import chem.miller
local_debug = None
LOCAL_DEBUG = None
def log(s, output_type=None):
if local_debug:
print s
def log(msg, output_type=None):
"""Logging function for tests"""
if LOCAL_DEBUG:
print msg
if output_type == 'html':
f.write(s + '\n<br>\n')
f.write(msg + '\n<br>\n')
class Test_Compare_Equations(unittest.TestCase):
@@ -132,10 +133,6 @@ class Test_Compare_Expressions(unittest.TestCase):
self.assertFalse(compare_chemical_expression(
"H2O(s) + CO2", "H2O+CO2"))
def test_compare_phases_not_ignored_explicitly(self):
self.assertTrue(compare_chemical_expression(
"H2O(s) + CO2", "H2O(s)+CO2", ignore_state=False))
# all in one cases
def test_complex_additivity(self):
self.assertTrue(compare_chemical_expression(
@@ -223,247 +220,250 @@ class Test_Divide_Expressions(unittest.TestCase):
class Test_Render_Equations(unittest.TestCase):
"""
Tests to validate the HTML rendering of plaintext (input) equations
"""
# pylint: disable=line-too-long
def test_render1(self):
s = "H2O + CO2"
out = render_to_html(s)
test_string = "H2O + CO2"
out = render_to_html(test_string)
correct = u'<span class="math">H<sub>2</sub>O+CO<sub>2</sub></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render_uncorrect_reaction(self):
s = "O2C + OH2"
out = render_to_html(s)
test_string = "O2C + OH2"
out = render_to_html(test_string)
correct = u'<span class="math">O<sub>2</sub>C+OH<sub>2</sub></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render2(self):
s = "CO2 + H2O + Fe(OH)3"
out = render_to_html(s)
test_string = "CO2 + H2O + Fe(OH)3"
out = render_to_html(test_string)
correct = u'<span class="math">CO<sub>2</sub>+H<sub>2</sub>O+Fe(OH)<sub>3</sub></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render3(self):
s = "3H2O + 2CO2"
out = render_to_html(s)
test_string = "3H2O + 2CO2"
out = render_to_html(test_string)
correct = u'<span class="math">3H<sub>2</sub>O+2CO<sub>2</sub></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render4(self):
s = "H^+ + OH^-"
out = render_to_html(s)
test_string = "H^+ + OH^-"
out = render_to_html(test_string)
correct = u'<span class="math">H<sup>+</sup>+OH<sup>-</sup></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render5(self):
s = "Fe(OH)^2- + (OH)^-"
out = render_to_html(s)
test_string = "Fe(OH)^2- + (OH)^-"
out = render_to_html(test_string)
correct = u'<span class="math">Fe(OH)<sup>2-</sup>+(OH)<sup>-</sup></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render6(self):
s = "7/2H^+ + 3/5OH^-"
out = render_to_html(s)
test_string = "7/2H^+ + 3/5OH^-"
out = render_to_html(test_string)
correct = u'<span class="math"><sup>7</sup>&frasl;<sub>2</sub>H<sup>+</sup>+<sup>3</sup>&frasl;<sub>5</sub>OH<sup>-</sup></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render7(self):
s = "5(H1H212)^70010- + 2H2O + 7/2HCl + H2O"
out = render_to_html(s)
test_string = "5(H1H212)^70010- + 2H2O + 7/2HCl + H2O"
out = render_to_html(test_string)
correct = u'<span class="math">5(H<sub>1</sub>H<sub>212</sub>)<sup>70010-</sup>+2H<sub>2</sub>O+<sup>7</sup>&frasl;<sub>2</sub>HCl+H<sub>2</sub>O</span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render8(self):
s = "H2O(s) + CO2"
out = render_to_html(s)
test_string = "H2O(s) + CO2"
out = render_to_html(test_string)
correct = u'<span class="math">H<sub>2</sub>O(s)+CO<sub>2</sub></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render9(self):
s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-"
out = render_to_html(s)
test_string = "5[Ni(NH3)4]^2+ + 5/2SO4^2-"
out = render_to_html(test_string)
correct = u'<span class="math">5[Ni(NH<sub>3</sub>)<sub>4</sub>]<sup>2+</sup>+<sup>5</sup>&frasl;<sub>2</sub>SO<sub>4</sub><sup>2-</sup></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render_error(self):
s = "5.2H20"
out = render_to_html(s)
test_string = "5.2H20"
out = render_to_html(test_string)
correct = u'<span class="math"><span class="inline-error inline">5.2H20</span></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render_simple_brackets(self):
s = "(Ar)"
out = render_to_html(s)
test_string = "(Ar)"
out = render_to_html(test_string)
correct = u'<span class="math">(Ar)</span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render_eq1(self):
s = "H^+ + OH^- -> H2O"
out = render_to_html(s)
test_string = "H^+ + OH^- -> H2O"
out = render_to_html(test_string)
correct = u'<span class="math">H<sup>+</sup>+OH<sup>-</sup>\u2192H<sub>2</sub>O</span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render_eq2(self):
s = "H^+ + OH^- <-> H2O"
out = render_to_html(s)
test_string = "H^+ + OH^- <-> H2O"
out = render_to_html(test_string)
correct = u'<span class="math">H<sup>+</sup>+OH<sup>-</sup>\u2194H<sub>2</sub>O</span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
def test_render_eq3(self):
s = "H^+ + OH^- <= H2O" # unsupported arrow
out = render_to_html(s)
test_string = "H^+ + OH^- <= H2O" # unsupported arrow
out = render_to_html(test_string)
correct = u'<span class="math"><span class="inline-error inline">H^+ + OH^- <= H2O</span></span>'
log(out + ' ------- ' + correct, 'html')
self.assertEqual(out, correct)
class Test_Crystallography_Miller(unittest.TestCase):
''' Tests for crystallography grade function.'''
"""Tests for crystallography grade function."""
# pylint: disable=line-too-long
def test_empty_points(self):
user_input = '{"lattice": "bcc", "points": []}'
self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
self.assertFalse(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
def test_only_one_point(self):
user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"]]}'
self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
self.assertFalse(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
def test_only_two_points(self):
user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"]]}'
self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
self.assertFalse(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
def test_1(self):
user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
def test_2(self):
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(1,1,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,1,1)', 'lattice': 'bcc'}))
def test_3(self):
user_input = '{"lattice": "bcc", "points": [["1.00", "0.50", "1.00"], ["1.00", "1.00", "0.50"], ["0.50", "1.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'}))
def test_4(self):
user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.664", "0.00"], ["0.00", "1.00", "0.33"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(-3, 3, -3)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(-3, 3, -3)', 'lattice': 'bcc'}))
def test_5(self):
""" return true only in case points coordinates are exact.
But if they transform to closest 0.05 value it is not true"""
user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.33", "0.00"], ["0.00", "1.00", "0.33"]]}'
self.assertFalse(miller.grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'}))
self.assertFalse(chem.miller.grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'}))
def test_6(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.25", "0.00"], ["0.25", "0.00", "0.00"], ["0.00", "0.00", "0.25"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(4,4,4)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(4,4,4)', 'lattice': 'bcc'}))
def test_7(self): # goes throug origin
user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "0.00", "0.00"], ["0.50", "1.00", "0.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(0,0,-1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,0,-1)', 'lattice': 'bcc'}))
def test_8(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.50"], ["1.00", "0.00", "0.50"], ["0.50", "1.00", "0.50"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(0,0,2)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,0,2)', 'lattice': 'bcc'}))
def test_9(self):
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "1.00", "1.00"], ["1.00", "0.00", "0.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(1,1,0)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,1,0)', 'lattice': 'bcc'}))
def test_10(self):
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "0.00", "0.00"], ["0.00", "1.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'}))
def test_11(self):
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "0.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(0,1,2)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,1,2)', 'lattice': 'bcc'}))
def test_12(self):
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["0.00", "0.00", "0.50"], ["1.00", "1.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(0,1,-2)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,1,-2)', 'lattice': 'bcc'}))
def test_13(self):
user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.50", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(2,0,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(2,0,1)', 'lattice': 'bcc'}))
def test_14(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "0.00", "1.00"], ["0.50", "1.00", "0.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(2,-1,0)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(2,-1,0)', 'lattice': 'bcc'}))
def test_15(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'}))
def test_16(self):
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "1.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'}))
def test_17(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "1.00"], ["1.00", "1.00", "0.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(-1,1,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(-1,1,1)', 'lattice': 'bcc'}))
def test_18(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'}))
def test_19(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(-1,1,0)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(-1,1,0)', 'lattice': 'bcc'}))
def test_20(self):
user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(1,0,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,0,1)', 'lattice': 'bcc'}))
def test_21(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "0.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(-1,0,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(-1,0,1)', 'lattice': 'bcc'}))
def test_22(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(0,1,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,1,1)', 'lattice': 'bcc'}))
def test_23(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'}))
def test_24(self):
user_input = '{"lattice": "bcc", "points": [["0.66", "0.00", "0.00"], ["0.00", "0.66", "0.00"], ["0.00", "0.00", "0.66"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'bcc'}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'bcc'}))
def test_25(self):
user_input = u'{"lattice":"","points":[["0.00","0.00","0.01"],["1.00","1.00","0.01"],["0.00","1.00","1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': ''}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': ''}))
def test_26(self):
user_input = u'{"lattice":"","points":[["0.00","0.01","0.00"],["1.00","0.00","0.00"],["0.00","0.00","1.00"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(0,-1,0)', 'lattice': ''}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,-1,0)', 'lattice': ''}))
def test_27(self):
""" rounding to 0.35"""
user_input = u'{"lattice":"","points":[["0.33","0.00","0.00"],["0.00","0.33","0.00"],["0.00","0.00","0.33"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': ''}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': ''}))
def test_28(self):
""" rounding to 0.30"""
user_input = u'{"lattice":"","points":[["0.30","0.00","0.00"],["0.00","0.30","0.00"],["0.00","0.00","0.30"]]}'
self.assertTrue(miller.grade(user_input, {'miller': '(10,10,10)', 'lattice': ''}))
self.assertTrue(chem.miller.grade(user_input, {'miller': '(10,10,10)', 'lattice': ''}))
def test_wrong_lattice(self):
user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}'
self.assertFalse(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'}))
self.assertFalse(chem.miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'}))
def suite():
@@ -478,7 +478,7 @@ def suite():
return unittest.TestSuite(suites)
if __name__ == "__main__":
local_debug = True
LOCAL_DEBUG = True
with codecs.open('render.html', 'w', encoding='utf-8') as f:
unittest.TextTestRunner(verbosity=2).run(suite())
# open render.html to look at rendered equations

View File

@@ -28,7 +28,6 @@ XMODULES = [
"static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"textannotation = xmodule.textannotation_module:TextAnnotationDescriptor",
"videoannotation = xmodule.videoannotation_module:VideoAnnotationDescriptor",

View File

@@ -150,7 +150,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
def get_html(self):
""" Renders parameters to template. """
context = {
'display_name': self.display_name_with_default,
'display_name': self.display_name_with_default_escaped,
'element_id': self.element_id,
'instructions_html': self.instructions,
'content_html': self._render_content()

View File

@@ -658,7 +658,7 @@ class CapaMixin(CapaFields):
check_button_checking = False
content = {
'name': self.display_name_with_default,
'name': self.display_name_with_default_escaped,
'html': html,
'weight': self.weight,
}
@@ -1116,10 +1116,11 @@ class CapaMixin(CapaFields):
if dog_stats_api:
dog_stats_api.increment(metric_name('checks'), tags=[u'result:success'])
dog_stats_api.histogram(
metric_name('correct_pct'),
float(published_grade['grade']) / published_grade['max_grade'],
)
if published_grade['max_grade'] != 0:
dog_stats_api.histogram(
metric_name('correct_pct'),
float(published_grade['grade']) / published_grade['max_grade'],
)
dog_stats_api.histogram(
metric_name('attempts'),
self.attempts,

View File

@@ -39,13 +39,23 @@ class StaticContent(object):
return self.location.category == 'thumbnail'
@staticmethod
def generate_thumbnail_name(original_name):
def generate_thumbnail_name(original_name, dimensions=None):
"""
- original_name: Name of the asset (typically its location.name)
- dimensions: `None` or a tuple of (width, height) in pixels
"""
name_root, ext = os.path.splitext(original_name)
if not ext == XASSET_THUMBNAIL_TAIL_NAME:
name_root = name_root + ext.replace(u'.', u'-')
if dimensions:
width, height = dimensions # pylint: disable=unpacking-non-sequence
name_root += "-{}x{}".format(width, height)
return u"{name_root}{extension}".format(
name_root=name_root,
extension=XASSET_THUMBNAIL_TAIL_NAME,)
extension=XASSET_THUMBNAIL_TAIL_NAME,
)
@staticmethod
def compute_location(course_key, path, revision=None, is_thumbnail=False):
@@ -248,11 +258,25 @@ class ContentStore(object):
"""
raise NotImplementedError
def generate_thumbnail(self, content, tempfile_path=None):
def generate_thumbnail(self, content, tempfile_path=None, dimensions=None):
"""Create a thumbnail for a given image.
Returns a tuple of (StaticContent, AssetKey)
`content` is the StaticContent representing the image you want to make a
thumbnail out of.
`tempfile_path` is a string path to the location of a file to read from
in order to grab the image data, instead of relying on `content.data`
`dimensions` is an optional param that represents (width, height) in
pixels. It defaults to None.
"""
thumbnail_content = None
# use a naming convention to associate originals with the thumbnail
thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name)
thumbnail_name = StaticContent.generate_thumbnail_name(
content.location.name, dimensions=dimensions
)
thumbnail_file_location = StaticContent.compute_location(
content.location.course_key, thumbnail_name, is_thumbnail=True
)
@@ -273,8 +297,11 @@ class ContentStore(object):
# I've seen some exceptions from the PIL library when trying to save palletted
# PNG files to JPEG. Per the google-universe, they suggest converting to RGB first.
im = im.convert('RGB')
size = 128, 128
im.thumbnail(size, Image.ANTIALIAS)
if not dimensions:
dimensions = (128, 128)
im.thumbnail(dimensions, Image.ANTIALIAS)
thumbnail_file = StringIO.StringIO()
im.save(thumbnail_file, 'JPEG')
thumbnail_file.seek(0)

View File

@@ -57,15 +57,51 @@ def display_name_with_default(course):
like to just pass course.display_name and course.url_name as arguments to
this function, we can't do so without breaking those tests.
Note: This method no longer escapes as it once did, so the caller must
ensure it is properly escaped where necessary.
Arguments:
course (CourseDescriptor|CourseOverview): descriptor or overview of
said course.
"""
# TODO: Consider changing this to use something like xml.sax.saxutils.escape
return (
course.display_name if course.display_name is not None
else course.url_name.replace('_', ' ')
).replace('<', '&lt;').replace('>', '&gt;')
)
def display_name_with_default_escaped(course):
"""
DEPRECATED: use display_name_with_default
Calculates the display name for a course with some HTML escaping.
This follows the same logic as display_name_with_default, with
the addition of the escaping.
Here is an example of how to move away from this method in Mako html:
Before:
<span class="course-name">${course.display_name_with_default_escaped}</span>
After:
<span class="course-name">${course.display_name_with_default | h}</span>
If the context is Javascript in Mako, you'll need to follow other best practices.
Note: Switch to display_name_with_default, and ensure the caller
properly escapes where necessary.
Note: This newly introduced method should not be used. It was only
introduced to enable a quick search/replace and the ability to slowly
migrate and test switching to display_name_with_default, which is no
longer escaped.
Arguments:
course (CourseDescriptor|CourseOverview): descriptor or overview of
said course.
"""
# This escaping is incomplete. However, rather than switching this to use
# markupsafe.escape() and fixing issues, better to put that energy toward
# migrating away from this method altogether.
return course.display_name_with_default.replace('<', '&lt;').replace('>', '&gt;')
def number_for_course_location(location):

View File

@@ -1367,3 +1367,56 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
bool: False if the course has already started, True otherwise.
"""
return datetime.now(UTC()) <= self.start
class CourseSummary(object):
"""
A lightweight course summary class, which constructs split/mongo course summary without loading
the course. It is used at cms for listing courses to global staff user.
"""
course_info_fields = ['display_name', 'display_coursenumber', 'display_organization']
def __init__(self, course_locator, display_name=u"Empty", display_coursenumber=None, display_organization=None):
"""
Initialize and construct course summary
Arguments:
course_locator (CourseLocator): CourseLocator object of the course.
display_name (unicode): display name of the course. When you create a course from console, display_name
isn't set (course block has no key `display_name`). "Empty" name is returned when we load the course.
If `display_name` isn't present in the course block, use the `Empty` as default display name.
We can set None as a display_name in Course Advance Settings; Do not use "Empty" when display_name is
set to None.
display_coursenumber (unicode|None): Course number that is specified & appears in the courseware
display_organization (unicode|None): Course organization that is specified & appears in the courseware
"""
self.display_coursenumber = display_coursenumber
self.display_organization = display_organization
self.display_name = display_name
self.id = course_locator # pylint: disable=invalid-name
self.location = course_locator.make_usage_key('course', 'course')
@property
def display_org_with_default(self):
"""
Return a display organization if it has been specified, otherwise return the 'org' that
is in the location
"""
if self.display_organization:
return self.display_organization
return self.location.org
@property
def display_number_with_default(self):
"""
Return a display course number if it has been specified, otherwise return the 'course' that
is in the location
"""
if self.display_coursenumber:
return self.display_coursenumber
return self.location.course

View File

@@ -1,44 +0,0 @@
// In the LMS sliders use built-in styles from jquery-ui-1.8.22.custom.css.
// CMS uses its own sliders styles.
// These styles we use only to sure, that slider in GST module
// will be render correctly (just like a duplication some from jquery-ui-1.8.22.custom.css).
// Cause, for example, CMS overwrites many jquery-ui-1.8.22.custom.css styles,
// and we must overwrite them again.
.ui-widget-content {
border: 1px solid #dddddd;
color: #333333;
}
.ui-widget {
font-family: Trebuchet MS, Tahoma, Verdana, Arial, sans-serif;
font-size: 1.1em;
}
.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl {
-moz-border-radius-topleft: 4px;
-webkit-border-top-left-radius: 4px;
-khtml-border-top-left-radius: 4px;
border-top-left-radius: 4px;
}
.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr {
-moz-border-radius-topright: 4px;
-webkit-border-top-right-radius: 4px;
-khtml-border-top-right-radius: 4px;
border-top-right-radius: 4px;
}
.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl {
-moz-border-radius-bottomleft: 4px;
-webkit-border-bottom-left-radius: 4px;
-khtml-border-bottom-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br {
-moz-border-radius-bottomright: 4px;
-webkit-border-bottom-right-radius: 4px;
-khtml-border-bottom-right-radius: 4px;
border-bottom-right-radius: 4px;
}

View File

@@ -1,5 +1,5 @@
$sequence--border-color: #C8C8C8;
$link-color: rgb(26, 161, 222);
// repeated extends - needed since LMS styling was referenced
.block-link {
border-left: 1px solid lighten($sequence--border-color, 10%);
@@ -36,7 +36,7 @@ $sequence--border-color: #C8C8C8;
// TODO (cpennington): This doesn't work anymore. XModules aren't able to
// import from external sources.
@extend .topbar;
margin: -4px 0 ($baseline*1.5);
margin: -4px 0 $baseline;
position: relative;
border-bottom: none;
z-index: 0;
@@ -119,6 +119,10 @@ $sequence--border-color: #C8C8C8;
-webkit-font-smoothing: antialiased; // Clear up the lines on the icons
}
i.fa-bookmark {
color: $link-color;
}
&.inactive {
.icon {
@@ -142,6 +146,10 @@ $sequence--border-color: #C8C8C8;
.icon {
color: rgb(10, 10, 10);
}
i.fa-bookmark {
color: $link-color;
}
}
}
@@ -295,3 +303,4 @@ nav.sequence-bottom {
outline: none;
}
}

View File

@@ -247,6 +247,22 @@ html:not('.afontgarde') .icon-fallback-img {
}
}
.closed-captions {
position: absolute;
width: 85%;
left: 5%;
top: 70%;
text-align: center;
}
.closed-captions.is-visible {
max-height: ($baseline * 3);
border-radius: ($baseline / 5);
padding: 8px ($baseline / 2) 8px ($baseline * 1.5);
background: rgba(0, 0, 0, .75);
color: $yellow;
}
.video-player {
overflow: hidden;
min-height: 300px;
@@ -701,39 +717,44 @@ html:not('.afontgarde') .icon-fallback-img {
.subtitles {
@include float(left);
overflow: auto;
margin: 0;
max-height: 460px;
width: flex-grid(3, 9);
padding: 0;
font-size: 14px;
list-style: none;
visibility: visible;
li {
@extend %ui-fake-link;
margin-bottom: 8px;
border: 0;
.subtitles-menu {
height: 100%;
margin: 0;
padding: 0;
color: #0074b5; // AA compliant
line-height: lh();
list-style: none;
&.current {
color: #333;
font-weight: 700;
}
li {
@extend %ui-fake-link;
margin-bottom: 8px;
border: 0;
padding: 0;
color: #0074b5; // AA compliant
line-height: lh();
&.focused {
outline: #000 dotted thin;
outline-offset: -1px;
}
&.current {
color: #333;
font-weight: 700;
}
&:hover,
&:focus {
text-decoration: underline;
}
&.focused {
outline: #000 dotted thin;
outline-offset: -1px;
}
&:empty {
margin-bottom: 0;
&:hover,
&:focus {
text-decoration: underline;
}
&:empty {
margin-bottom: 0;
}
}
}
}

View File

@@ -1,241 +0,0 @@
"""
Graphical slider tool module is ungraded xmodule used by students to
understand functional dependencies.
"""
import json
import logging
from lxml import etree
from lxml import html
import xmltodict
from xmodule.editing_module import XMLEditingDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.stringify import stringify_children
from pkg_resources import resource_string
from xblock.fields import String, Scope
log = logging.getLogger(__name__)
DEFAULT_RENDER = """
<h2>Graphic slider tool: Dynamic range and implicit functions.</h2>
<p>You can make the range of the x axis (but not ticks of x axis) of
functions depend on a parameter value. This can be useful when the
function domain needs to be variable.</p>
<p>Implicit functions like a circle can be plotted as 2 separate
functions of the same color.</p>
<div style="height:50px;">
<slider var='r' style="width:400px;float:left;"/>
<textbox var='r' style="float:left;width:60px;margin-left:15px;"/>
</div>
<plot style="margin-top:15px;margin-bottom:15px;"/>
"""
DEFAULT_CONFIGURATION = """
<parameters>
<param var="r" min="5" max="25" step="0.5" initial="12.5" />
</parameters>
<functions>
<function color="red">Math.sqrt(r * r - x * x)</function>
<function color="red">-Math.sqrt(r * r - x * x)</function>
<function color="red">Math.sqrt(r * r / 20 - Math.pow(x-r/2.5, 2)) + r/8</function>
<function color="red">-Math.sqrt(r * r / 20 - Math.pow(x-r/2.5, 2)) + r/5.5</function>
<function color="red">Math.sqrt(r * r / 20 - Math.pow(x+r/2.5, 2)) + r/8</function>
<function color="red">-Math.sqrt(r * r / 20 - Math.pow(x+r/2.5, 2)) + r/5.5</function>
<function color="red">-Math.sqrt(r * r / 5 - x * x) - r/5.5</function>
</functions>
<plot>
<xrange>
<!-- dynamic range -->
<min>-r</min>
<max>r</max>
</xrange>
<num_points>1000</num_points>
<xticks>-30, 6, 30</xticks>
<yticks>-30, 6, 30</yticks>
</plot>
"""
class GraphicalSliderToolFields(object):
data = String(
help="Html contents to display for this module",
default='<render>{}</render><configuration>{}</configuration>'.format(
DEFAULT_RENDER, DEFAULT_CONFIGURATION),
scope=Scope.content
)
class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule):
''' Graphical-Slider-Tool Module
'''
js = {
'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')],
'js': [
# 3rd party libraries used by graphic slider tool.
# TODO - where to store them - outside xmodule?
resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/state.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/el_output.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/g_label_el_output.js'),
resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')
]
}
css = {'scss': [resource_string(__name__, 'css/gst/display.scss')]}
js_module_name = "GraphicalSliderTool"
@property
def configuration(self):
return stringify_children(
html.fromstring(self.data).xpath('configuration')[0]
)
@property
def render(self):
return stringify_children(
html.fromstring(self.data).xpath('render')[0]
)
def get_html(self):
""" Renders parameters to template. """
# these 3 will be used in class methods
self.html_id = self.location.html_id()
self.html_class = self.location.category
self.configuration_json = self.build_configuration_json()
params = {
'gst_html': self.substitute_controls(self.render),
'element_id': self.html_id,
'element_class': self.html_class,
'configuration_json': self.configuration_json
}
content = self.system.render_template(
'graphical_slider_tool.html', params
)
return content
def substitute_controls(self, html_string):
""" Substitutes control elements (slider, textbox and plot) in
html_string with their divs. Html_string is content of <render> tag
inside <graphical_slider_tool> tag. Documentation on how information in
<render> tag is organized and processed is located in:
edx-platform/docs/build/html/graphical_slider_tool.html.
Args:
html_string: content of <render> tag, with controls as xml tags,
e.g. <slider var="a"/>.
Returns:
html_string with control tags replaced by proper divs
(<slider var="a"/> -> <div class="....slider" > </div>)
"""
xml = html.fromstring(html_string)
# substitute plot, if presented
plot_div = '<div class="{element_class}_plot" id="{element_id}_plot" \
style="{style}"></div>'
plot_el = xml.xpath('//plot')
if plot_el:
plot_el = plot_el[0]
plot_el.getparent().replace(plot_el, html.fromstring(
plot_div.format(
element_class=self.html_class,
element_id=self.html_id,
style=plot_el.get('style', ""))))
# substitute sliders
slider_div = '<div class="{element_class}_slider" \
id="{element_id}_slider_{var}" \
data-var="{var}" \
style="{style}">\
</div>'
slider_els = xml.xpath('//slider')
for slider_el in slider_els:
slider_el.getparent().replace(slider_el, html.fromstring(
slider_div.format(
element_class=self.html_class,
element_id=self.html_id,
var=slider_el.get('var', ""),
style=slider_el.get('style', ""))))
# substitute inputs aka textboxes
input_div = '<input class="{element_class}_input" \
id="{element_id}_input_{var}_{input_index}" \
data-var="{var}" style="{style}"/>'
input_els = xml.xpath('//textbox')
for input_index, input_el in enumerate(input_els):
input_el.getparent().replace(input_el, html.fromstring(
input_div.format(
element_class=self.html_class,
element_id=self.html_id,
var=input_el.get('var', ""),
style=input_el.get('style', ""),
input_index=input_index)))
return html.tostring(xml)
def build_configuration_json(self):
"""Creates json element from xml element (with aim to transfer later
directly to javascript via hidden field in template). Steps:
1. Convert xml tree to python dict.
2. Dump dict to json.
"""
# <root> added for interface compatibility with xmltodict.parse
# class added for javascript's part purposes
root = '<root class="{}">{}</root>'.format(
self.html_class,
self.configuration)
return json.dumps(xmltodict.parse(root))
class GraphicalSliderToolDescriptor(GraphicalSliderToolFields, XMLEditingDescriptor, XmlDescriptor):
module_class = GraphicalSliderToolModule
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Pull out the data into dictionary.
Args:
xml_object: xml from file.
Returns:
dict
"""
# check for presense of required tags in xml
expected_children_level_0 = ['render', 'configuration']
for child in expected_children_level_0:
if len(xml_object.xpath(child)) != 1:
raise ValueError(u"Graphical Slider Tool definition must include \
exactly one '{0}' tag".format(child))
expected_children_level_1 = ['functions']
for child in expected_children_level_1:
if len(xml_object.xpath('configuration')[0].xpath(child)) != 1:
raise ValueError(u"Graphical Slider Tool definition must include \
exactly one '{0}' tag".format(child))
# finished
return {
'data': stringify_children(xml_object)
}, []
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
data = u'<{tag}>{body}</{tag}>'.format(
tag='graphical_slider_tool',
body=self.data)
xml_object = etree.fromstring(data)
return xml_object

View File

@@ -127,7 +127,7 @@ class ImageAnnotationModule(AnnotatableFields, XModule):
def student_view(self, context):
""" Renders parameters to template. """
context = {
'display_name': self.display_name_with_default,
'display_name': self.display_name_with_default_escaped,
'instructions_html': self.instructions,
'token': retrieve_token(self.user_email, self.annotation_token_secret),
'tag': self.instructor_tags,

View File

@@ -12,7 +12,7 @@
<span id="display_example_1"></span>
<span id="input_example_1_dynamath"></span>
<button class="check">Check</button>
<button class="check Check" data-checking="Checking..." data-value="Check"><span class="check-label">Check</span><span class="sr"> your answer</span></button>
<button class="reset">Reset</button>
<button class="save">Save</button>
<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>

View File

@@ -17,6 +17,7 @@
<div id="id"></div>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>

View File

@@ -47,7 +47,7 @@ lib_paths:
- common_static/js/vendor/json2.js
- common_static/js/vendor/underscore-min.js
- common_static/js/vendor/backbone-min.js
- common_static/js/vendor/jquery.leanModal.min.js
- common_static/js/vendor/jquery.leanModal.js
- common_static/js/vendor/CodeMirror/codemirror.js
- common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce.min.js
- common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min.js

View File

@@ -198,17 +198,55 @@ describe 'Problem', ->
expect(@problem.el.html()).toEqual 'Incorrect!'
expect(window.SR.readElts).toHaveBeenCalled()
it 'tests if all the capa buttons are disabled while checking', ->
runs ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'incorrect', contents: 'Incorrect!')
promise =
always: (callable) -> callable()
done: (callable) -> callable()
spyOn @problem, 'enableAllButtons'
@problem.check()
expect(@problem.enableAllButtons).toHaveBeenCalledWith false, true
waitsFor (->
return jQuery.active == 0
), "jQuery requests finished", 1000
runs ->
expect(@problem.enableAllButtons).toHaveBeenCalledWith true, true
it 'tests the expected change in text of check button', ->
runs ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
promise =
always: (callable) -> callable()
done: (callable) -> callable()
spyOn @problem.checkButtonLabel, 'text'
@problem.check()
expect(@problem.checkButtonLabel.text).toHaveBeenCalledWith 'Checking...'
waitsFor (->
return jQuery.active == 0
), "jQuery requests finished", 1000
runs ->
expect(@problem.checkButtonLabel.text).toHaveBeenCalledWith 'Check'
describe 'reset', ->
beforeEach ->
@problem = new Problem($('.xblock-student_view'))
it 'log the problem_reset event', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
promise =
always: (callable) -> callable()
@problem.answers = 'foo=1&bar=2'
@problem.reset()
expect(Logger.log).toHaveBeenCalledWith 'problem_reset', 'foo=1&bar=2'
it 'POST to the problem reset page', ->
spyOn $, 'postWithPrefix'
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
promise =
always: (callable) -> callable()
@problem.reset()
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_reset',
{ id: 'i4x://edX/101/problem/Problem1' }, jasmine.any(Function)
@@ -216,9 +254,28 @@ describe 'Problem', ->
it 'render the returned content', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback html: "Reset!"
promise =
always: (callable) -> callable()
@problem.reset()
expect(@problem.el.html()).toEqual 'Reset!'
it 'tests if all the buttons are disabled and the text of check button remains same while resetting', ->
runs ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
promise =
always: (callable) -> callable()
spyOn @problem, 'enableAllButtons'
@problem.reset()
expect(@problem.enableAllButtons).toHaveBeenCalledWith false, false
expect(@problem.checkButtonLabel).toHaveText 'Check'
waitsFor (->
return jQuery.active == 0
), "jQuery requests finished", 1000
runs ->
expect(@problem.enableAllButtons).toHaveBeenCalledWith true, false
expect(@problem.checkButtonLabel).toHaveText 'Check'
describe 'show', ->
beforeEach ->
@problem = new Problem($('.xblock-student_view'))
@@ -518,18 +575,26 @@ describe 'Problem', ->
@problem.answers = 'foo=1&bar=2'
it 'log the problem_save event', ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
promise =
always: (callable) -> callable()
@problem.save()
expect(Logger.log).toHaveBeenCalledWith 'problem_save', 'foo=1&bar=2'
it 'POST to save problem', ->
spyOn $, 'postWithPrefix'
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
promise =
always: (callable) -> callable()
@problem.save()
expect($.postWithPrefix).toHaveBeenCalledWith '/problem/Problem1/problem_save',
'foo=1&bar=2', jasmine.any(Function)
it 'reads the save message', ->
runs ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'OK')
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'OK')
promise =
always: (callable) -> callable()
@problem.save()
waitsFor (->
return jQuery.active == 0
@@ -538,6 +603,24 @@ describe 'Problem', ->
runs ->
expect(window.SR.readElts).toHaveBeenCalled()
it 'tests if all the buttons are disabled and the text of check button does not change while saving.', ->
runs ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'OK')
promise =
always: (callable) -> callable()
spyOn @problem, 'enableAllButtons'
@problem.save()
expect(@problem.enableAllButtons).toHaveBeenCalledWith false, false
expect(@problem.checkButtonLabel).toHaveText 'Check'
waitsFor (->
return jQuery.active == 0
), "jQuery requests finished", 1000
runs ->
expect(@problem.enableAllButtons).toHaveBeenCalledWith true, false
expect(@problem.checkButtonLabel).toHaveText 'Check'
describe 'refreshMath', ->
beforeEach ->
@problem = new Problem($('.xblock-student_view'))

View File

@@ -48,6 +48,12 @@
});
});
it('adds the captioning control to the video player', function() {
state = jasmine.initializePlayer();
expect($('.video')).toContain('.toggle-captions');
expect($('.video')).toContain('.closed-captions');
});
it('fetch the transcript in HTML5 mode', function () {
runs(function () {
state = jasmine.initializePlayer();
@@ -122,16 +128,16 @@
it('bind the mouse movement', function () {
state = jasmine.initializePlayer();
expect($('.subtitles')).toHandle('mouseover');
expect($('.subtitles')).toHandle('mouseout');
expect($('.subtitles')).toHandle('mousemove');
expect($('.subtitles')).toHandle('mousewheel');
expect($('.subtitles')).toHandle('DOMMouseScroll');
expect($('.subtitles-menu')).toHandle('mouseover');
expect($('.subtitles-menu')).toHandle('mouseout');
expect($('.subtitles-menu')).toHandle('mousemove');
expect($('.subtitles-menu')).toHandle('mousewheel');
expect($('.subtitles-menu')).toHandle('DOMMouseScroll');
});
it('bind the scroll', function () {
state = jasmine.initializePlayer();
expect($('.subtitles'))
expect($('.subtitles-menu'))
.toHandleWith('scroll', state.videoControl.showControls);
});
@@ -158,14 +164,61 @@
});
});
describe('renderCaptions', function() {
describe('is rendered', function() {
var KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', { keyCode: key });
};
it('toggles the captions on control click', function() {
state = jasmine.initializePlayer();
$('.toggle-captions').click();
expect($('.toggle-captions')).toHaveClass('is-active');
expect($('.closed-captions')).toHaveClass('is-visible');
$('.toggle-captions').click();
expect($('.toggle-captions')).not.toHaveClass('is-active');
expect($('.closed-captions')).not.toHaveClass('is-visible');
});
it('toggles the captions on keypress ENTER', function() {
state = jasmine.initializePlayer();
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.ENTER));
expect($('.toggle-captions')).toHaveClass('is-active');
expect($('.closed-captions')).toHaveClass('is-visible');
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.ENTER));
expect($('.toggle-captions')).not.toHaveClass('is-active');
expect($('.closed-captions')).not.toHaveClass('is-visible');
});
it('toggles the captions on keypress SPACE', function() {
state = jasmine.initializePlayer();
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.SPACE));
expect($('.toggle-captions')).toHaveClass('is-active');
expect($('.closed-captions')).toHaveClass('is-visible');
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.SPACE));
expect($('.toggle-captions')).not.toHaveClass('is-active');
expect($('.closed-captions')).not.toHaveClass('is-visible');
});
});
});
describe('renderLanguageMenu', function () {
describe('is rendered', function () {
var KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', { keyCode: key });
};
keyPressEvent = function(key) {
return $.Event('keydown', { keyCode: key });
};
it('if languages more than 1', function () {
state = jasmine.initializePlayer();
@@ -364,9 +417,8 @@
});
it('show explanation message', function () {
expect($('.subtitles li')).toHaveHtml(
'Caption will be displayed when you start playing ' +
'the video.'
expect($('.subtitles-menu li')).toHaveHtml(
'Transcript will be displayed when you start playing the video.'
);
});
@@ -444,7 +496,7 @@
runs(function () {
$(window).trigger(jQuery.Event('mousemove'));
jasmine.Clock.tick(state.config.captionsFreezeTime);
$('.subtitles').trigger(jQuery.Event('mouseenter'));
$('.subtitles-menu').trigger(jQuery.Event('mouseenter'));
jasmine.Clock.tick(state.config.captionsFreezeTime);
});
});
@@ -459,7 +511,7 @@
describe('when the cursor is moving', function () {
it('reset the freezing timeout', function () {
runs(function () {
$('.subtitles').trigger(jQuery.Event('mousemove'));
$('.subtitles-menu').trigger(jQuery.Event('mousemove'));
expect(window.clearTimeout).toHaveBeenCalled();
});
});
@@ -468,7 +520,7 @@
describe('when the mouse is scrolling', function () {
it('reset the freezing timeout', function () {
runs(function () {
$('.subtitles').trigger(jQuery.Event('mousewheel'));
$('.subtitles-menu').trigger(jQuery.Event('mousewheel'));
expect(window.clearTimeout).toHaveBeenCalled();
});
});
@@ -486,7 +538,7 @@
describe('always', function () {
beforeEach(function () {
$('.subtitles').trigger(jQuery.Event('mouseout'));
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
});
it('reset the freezing timeout', function () {
@@ -501,9 +553,9 @@
describe('when the player is playing', function () {
beforeEach(function () {
state.videoCaption.playing = true;
$('.subtitles li[data-index]:first')
$('.subtitles-menu li[data-index]:first')
.addClass('current');
$('.subtitles').trigger(jQuery.Event('mouseout'));
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
});
it('scroll the transcript', function () {
@@ -514,7 +566,7 @@
describe('when the player is not playing', function () {
beforeEach(function () {
state.videoCaption.playing = false;
$('.subtitles').trigger(jQuery.Event('mouseout'));
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
});
it('does not scroll the transcript', function () {

View File

@@ -92,7 +92,8 @@ function (VideoPlayer) {
showinfo: 0,
enablejsapi: 1,
modestbranding: 1,
html5: 1
html5: 1,
cc_load_policy: 0
},
videoId: 'cogebirgzzM',
events: events
@@ -118,7 +119,8 @@ function (VideoPlayer) {
rel: 0,
showinfo: 0,
enablejsapi: 1,
modestbranding: 1
modestbranding: 1,
cc_load_policy: 0
},
videoId: 'abcdefghijkl',
events: jasmine.any(Object)

View File

@@ -32,11 +32,15 @@ class @Problem
@checkButtonCheckText = @checkButtonLabel.text()
@checkButtonCheckingText = @checkButton.data('checking')
@checkButton.click @check_fd
@hintButton = @$('div.action button.hint-button')
@hintButton.click @hint_button
@resetButton = @$('div.action button.reset')
@resetButton.click @reset
@showButton = @$('div.action button.show')
@showButton.click @show
@saveButton = @$('div.action button.save')
@saveButton.click @save
@$('div.action button.hint-button').click @hint_button
@$('div.action button.reset').click @reset
@$('div.action button.show').click @show
@$('div.action button.save').click @save
# Accessibility helper for sighted keyboard users to show <clarification> tooltips on focus:
@$('.clarification').focus (ev) =>
icon = $(ev.target).children "i"
@@ -301,16 +305,11 @@ class @Problem
check: =>
if not @check_save_waitfor(@check_internal)
@check_internal()
@disableAllButtonsWhileRunning @check_internal, true
check_internal: =>
@enableCheckButton false
timeout_id = @enableCheckButtonAfterTimeout()
Logger.log 'problem_check', @answers
$.postWithPrefix("#{@url}/problem_check", @answers, (response) =>
$.postWithPrefix "#{@url}/problem_check", @answers, (response) =>
switch response.success
when 'incorrect', 'correct'
window.SR.readElts($(response.contents).find('.status'))
@@ -322,9 +321,11 @@ class @Problem
else
@gentle_alert response.success
Logger.log 'problem_graded', [@answers, response.contents], @id
).always(@enableCheckButtonAfterResponse)
reset: =>
@disableAllButtonsWhileRunning @reset_internal, false
reset_internal: =>
Logger.log 'problem_reset', @answers
$.postWithPrefix "#{@url}/problem_reset", id: @id, (response) =>
@render(response.html)
@@ -409,7 +410,7 @@ class @Problem
save: =>
if not @check_save_waitfor(@save_internal)
@save_internal()
@disableAllButtonsWhileRunning @save_internal, false
save_internal: =>
Logger.log 'problem_save', @answers
@@ -674,16 +675,56 @@ class @Problem
element = $(element)
element.find("section[id^='forinput']").removeClass('choicetextgroup_show_correct')
enableCheckButton: (enable) =>
disableAllButtonsWhileRunning: (operationCallback, isFromCheckOperation) =>
# Used to keep the buttons disabled while operationCallback is running.
# params:
# 'operationCallback' is an operation to be run.
# 'isFromCheckOperation' is a boolean to keep track if 'operationCallback' was
# @check, if so then text of check button will be changed as well.
@enableAllButtons false, isFromCheckOperation
operationCallback().always =>
@enableAllButtons true, isFromCheckOperation
enableAllButtons: (enable, isFromCheckOperation) =>
# Used to enable/disable all buttons in problem.
# params:
# 'enable' is a boolean to determine enabling/disabling of buttons.
# 'isFromCheckOperation' is a boolean to keep track if operation was initiated
# from @check so that text of check button will also be changed while disabling/enabling
# the check button.
if enable
@resetButton
.add(@saveButton)
.add(@hintButton)
.add(@showButton)
.removeClass('is-disabled')
.attr({'aria-disabled': 'false'})
else
@resetButton
.add(@saveButton)
.add(@hintButton)
.add(@showButton)
.addClass('is-disabled')
.attr({'aria-disabled': 'true'})
@enableCheckButton enable, isFromCheckOperation
enableCheckButton: (enable, changeText = true) =>
# Used to disable check button to reduce chance of accidental double-submissions.
# params:
# 'enable' is a boolean to determine enabling/disabling of check button.
# 'changeText' is a boolean to determine if there is need to change the
# text of check button as well.
if enable
@checkButton.removeClass 'is-disabled'
@checkButton.attr({'aria-disabled': 'false'})
@checkButtonLabel.text(@checkButtonCheckText)
if changeText
@checkButtonLabel.text(@checkButtonCheckText)
else
@checkButton.addClass 'is-disabled'
@checkButton.attr({'aria-disabled': 'true'})
@checkButtonLabel.text(@checkButtonCheckingText)
if changeText
@checkButtonLabel.text(@checkButtonCheckingText)
enableCheckButtonAfterResponse: =>
@has_response = true

View File

@@ -1 +0,0 @@
!*.js

View File

@@ -1,141 +0,0 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('ElOutput', [], function () {
return ElOutput;
function ElOutput(config, state) {
if ($.isPlainObject(config.functions["function"])) {
processFuncObj(config.functions["function"]);
} else if ($.isArray(config.functions["function"])) {
(function (c1) {
while (c1 < config.functions["function"].length) {
if ($.isPlainObject(config.functions["function"][c1])) {
processFuncObj(config.functions["function"][c1]);
}
c1 += 1;
}
}(0));
}
return;
function processFuncObj(obj) {
var paramNames, funcString, func, el, disableAutoReturn, updateOnEvent;
// We are only interested in functions that are meant for output to an
// element.
if (
(typeof obj['@output'] !== 'string') ||
((obj['@output'].toLowerCase() !== 'element') && (obj['@output'].toLowerCase() !== 'none'))
) {
return;
}
if (typeof obj['@el_id'] !== 'string') {
console.log('ERROR: You specified "output" as "element", but did not spify "el_id".');
return;
}
if (typeof obj['#text'] !== 'string') {
console.log('ERROR: Function body is not defined.');
return;
}
updateOnEvent = 'slide';
if (
(obj.hasOwnProperty('@update_on') === true) &&
(typeof obj['@update_on'] === 'string') &&
((obj['@update_on'].toLowerCase() === 'slide') || (obj['@update_on'].toLowerCase() === 'change'))
) {
updateOnEvent = obj['@update_on'].toLowerCase();
}
disableAutoReturn = obj['@disable_auto_return'];
funcString = obj['#text'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
console.log(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
// Make sure that all HTML entities are converted to their proper
// ASCII text equivalents.
funcString = $('<div>').html(funcString).text();
paramNames = state.getAllParameterNames();
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
console.log(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
console.log('Error message: "' + err.message + '".');
if (state.showDebugInfo) {
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
paramNames.pop();
return;
}
paramNames.pop();
if (obj['@output'].toLowerCase() !== 'none') {
el = $('#' + obj['@el_id']);
if (el.length !== 1) {
console.log(
'ERROR: DOM element with ID "' + obj['@el_id'] + '" ' +
'not found. Dynamic element not created.'
);
return;
}
el.html(func.apply(window, state.getAllParameterValues()));
} else {
el = null;
func.apply(window, state.getAllParameterValues());
}
state.addDynamicEl(el, func, obj['@el_id'], updateOnEvent);
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -1,115 +0,0 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('GLabelElOutput', [], function () {
return GLabelElOutput;
function GLabelElOutput(config, state) {
if ($.isPlainObject(config.functions["function"])) {
processFuncObj(config.functions["function"]);
} else if ($.isArray(config.functions["function"])) {
(function (c1) {
while (c1 < config.functions["function"].length) {
if ($.isPlainObject(config.functions["function"][c1])) {
processFuncObj(config.functions["function"][c1]);
}
c1 += 1;
}
}(0));
}
return;
function processFuncObj(obj) {
var paramNames, funcString, func, disableAutoReturn;
// We are only interested in functions that are meant for output to an
// element.
if (
(typeof obj['@output'] !== 'string') ||
(obj['@output'].toLowerCase() !== 'plot_label')
) {
return;
}
if (typeof obj['@el_id'] !== 'string') {
console.log('ERROR: You specified "output" as "plot_label", but did not spify "el_id".');
return;
}
if (typeof obj['#text'] !== 'string') {
console.log('ERROR: Function body is not defined.');
return;
}
disableAutoReturn = obj['@disable_auto_return'];
funcString = obj['#text'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
console.log(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
// Make sure that all HTML entities are converted to their proper
// ASCII text equivalents.
funcString = $('<div>').html(funcString).text();
paramNames = state.getAllParameterNames();
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
console.log(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
console.log('Error message: "' + err.message + '".');
if (state.showDebugInfo) {
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
paramNames.pop();
return;
}
paramNames.pop();
state.plde.push({
'elId': obj['@el_id'],
'func': func
});
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -1,23 +0,0 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('GeneralMethods', [], function () {
if (!String.prototype.trim) {
// http://blog.stevenlevithan.com/archives/faster-trim-javascript
String.prototype.trim = function trim(str) {
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};
}
return {
'module_name': 'GeneralMethods',
'module_status': 'OK'
};
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -1,1512 +0,0 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('Graph', [], function () {
return Graph;
function Graph(gstId, config, state) {
var plotDiv, dataSeries, functions, xaxis, yaxis, numPoints, xrange,
asymptotes, movingLabels, xTicksNames, yTicksNames, graphBarWidth, graphBarAlign;
// We need plot configuration settings. Without them we can't continue.
if ($.isPlainObject(config.plot) === false) {
return;
}
// We must have a graph container DIV element available in order to
// proceed.
plotDiv = $('#' + gstId + '_plot');
if (plotDiv.length === 0) {
console.log('ERROR: Could not find the plot DIV with ID "' + gstId + '_plot".');
return;
}
if (plotDiv.width() === 0) {
plotDiv.width(300);
}
// Sometimes, when height is not explicitly set via CSS (or by some
// other means), it is 0 pixels by default. When Flot will try to plot
// a graph in this DIV with 0 height, then it will raise an error. To
// prevent this, we will set it to be equal to the width.
if (plotDiv.height() === 0) {
plotDiv.height(plotDiv.width());
}
plotDiv.css('position', 'relative');
// Configure some settings for the graph.
if (setGraphXRange() === false) {
console.log('ERROR: Could not configure the xrange. Will not continue.');
return;
}
if (setGraphAxes() === false) {
console.log('ERROR: Could not process configuration for the axes.');
return;
}
graphBarWidth = 1;
graphBarAlign = null;
getBarWidth();
getBarAlign();
// Get the user defined functions. If there aren't any, don't do
// anything else.
createFunctions();
if (functions.length === 0) {
console.log('ERROR: No functions were specified, or something went wrong.');
return;
}
if (createMarkingsFunctions() === false) {
return;
}
if (createMovingLabelFunctions() === false) {
return;
}
// Create the initial graph and plot it for the user to see.
if (generateData() === true) {
updatePlot();
}
// Bind an event. Whenever some constant changes, the graph will be
// redrawn
state.bindUpdatePlotEvent(plotDiv, onUpdatePlot);
return;
function getBarWidth() {
if (config.plot.hasOwnProperty('bar_width') === false) {
return;
}
if (typeof config.plot.bar_width !== 'string') {
console.log('ERROR: The parameter config.plot.bar_width must be a string.');
return;
}
if (isFinite(graphBarWidth = parseFloat(config.plot.bar_width)) === false) {
console.log('ERROR: The parameter config.plot.bar_width is not a valid floating number.');
graphBarWidth = 1;
return;
}
return;
}
function getBarAlign() {
if (config.plot.hasOwnProperty('bar_align') === false) {
return;
}
if (typeof config.plot.bar_align !== 'string') {
console.log('ERROR: The parameter config.plot.bar_align must be a string.');
return;
}
if (
(config.plot.bar_align.toLowerCase() !== 'left') &&
(config.plot.bar_align.toLowerCase() !== 'center')
) {
console.log('ERROR: Property config.plot.bar_align can be one of "left", or "center".');
return;
}
graphBarAlign = config.plot.bar_align.toLowerCase();
return;
}
function createMovingLabelFunctions() {
var c1, returnStatus;
returnStatus = true;
movingLabels = [];
if (config.plot.hasOwnProperty('moving_label') !== true) {
returnStatus = true;
} else if ($.isPlainObject(config.plot.moving_label) === true) {
if (processMovingLabel(config.plot.moving_label) === false) {
returnStatus = false;
}
} else if ($.isArray(config.plot.moving_label) === true) {
for (c1 = 0; c1 < config.plot.moving_label.length; c1++) {
if (processMovingLabel(config.plot.moving_label[c1]) === false) {
returnStatus = false;
}
}
}
return returnStatus;
}
function processMovingLabel(obj) {
var labelText, funcString, disableAutoReturn, paramNames, func,
fontWeight, fontColor;
if (obj.hasOwnProperty('@text') === false) {
console.log('ERROR: You did not define a "text" attribute for the moving_label.');
return false;
}
if (typeof obj['@text'] !== 'string') {
console.log('ERROR: "text" attribute is not a string.');
return false;
}
labelText = obj['@text'];
if (obj.hasOwnProperty('#text') === false) {
console.log('ERROR: moving_label is missing function declaration.');
return false;
}
if (typeof obj['#text'] !== 'string') {
console.log('ERROR: Function declaration is not a string.');
return false;
}
funcString = obj['#text'];
fontColor = 'black';
if (
(obj.hasOwnProperty('@color') === true) &&
(typeof obj['@color'] === 'string')
) {
fontColor = obj['@color'];
}
fontWeight = 'normal';
if (
(obj.hasOwnProperty('@weight') === true) &&
(typeof obj['@weight'] === 'string')
) {
if (
(obj['@weight'].toLowerCase() === 'normal') ||
(obj['@weight'].toLowerCase() === 'bold')
) {
fontWeight = obj['@weight'];
} else {
console.log('ERROR: Moving label can have a weight property of "normal" or "bold".');
}
}
disableAutoReturn = obj['@disable_auto_return'];
funcString = $('<div>').html(funcString).text();
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
console.log(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
paramNames = state.getAllParameterNames();
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
console.log(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
console.log('Error message: "' + err.message + '"');
if (state.showDebugInfo) {
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
paramNames.pop();
return false;
}
paramNames.pop();
movingLabels.push({
'labelText': labelText,
'func': func,
'el': null,
'fontColor': fontColor,
'fontWeight': fontWeight
});
return true;
}
function createMarkingsFunctions() {
var c1, paramNames, returnStatus;
returnStatus = true;
asymptotes = [];
paramNames = state.getAllParameterNames();
if ($.isPlainObject(config.plot.asymptote)) {
if (processAsymptote(config.plot.asymptote) === false) {
returnStatus = false;
}
} else if ($.isArray(config.plot.asymptote)) {
for (c1 = 0; c1 < config.plot.asymptote.length; c1 += 1) {
if (processAsymptote(config.plot.asymptote[c1]) === false) {
returnStatus = false;
}
}
}
return returnStatus;
// Read configuration options for asymptotes, and store them as
// an array of objects. Each object will have 3 properties:
//
// - color: the color of the asymptote line
// - type: 'x' (vertical), or 'y' (horizontal)
// - func: the function that will generate the value at which
// the asymptote will be plotted; i.e. x = func(), or
// y = func(); for now only horizontal and vertical
// asymptotes are supported
//
// Since each asymptote can have a variable function - function
// that relies on some parameter specified in the config - we will
// generate each asymptote just before we draw the graph. See:
//
// function updatePlot()
// function generateMarkings()
//
// Asymptotes are really thin rectangles implemented via the Flot's
// markings option.
function processAsymptote(asyObj) {
var newAsyObj, funcString, func;
newAsyObj = {};
if (typeof asyObj['@type'] === 'string') {
if (asyObj['@type'].toLowerCase() === 'x') {
newAsyObj.type = 'x';
} else if (asyObj['@type'].toLowerCase() === 'y') {
newAsyObj.type = 'y';
} else {
console.log('ERROR: Attribute "type" for asymptote can be "x" or "y".');
return false;
}
} else {
console.log('ERROR: Attribute "type" for asymptote is not specified.');
return false;
}
if (typeof asyObj['#text'] === 'string') {
funcString = asyObj['#text'];
} else {
console.log('ERROR: Function body for asymptote is not specified.');
return false;
}
newAsyObj.color = '#000';
if (typeof asyObj['@color'] === 'string') {
newAsyObj.color = asyObj['@color'];
}
newAsyObj.label = false;
if (
(asyObj.hasOwnProperty('@label') === true) &&
(typeof asyObj['@label'] === 'string')
) {
newAsyObj.label = asyObj['@label'];
}
funcString = $('<div>').html(funcString).text();
disableAutoReturn = asyObj['@disable_auto_return'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
console.log(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
paramNames.push(funcString);
try {
func = Function.apply(null, paramNames);
} catch (err) {
console.log('ERROR: Asymptote function body could not be converted to function object.');
console.log('Error message: "".' + err.message);
return false;
}
paramNames.pop();
newAsyObj.func = func;
asymptotes.push(newAsyObj);
return true;
}
}
function setGraphAxes() {
xaxis = {
'tickFormatter': null
};
if (typeof config.plot['xticks'] === 'string') {
if (processTicks(config.plot['xticks'], xaxis, 'xunits') === false) {
console.log('ERROR: Could not process the ticks for x-axis.');
return false;
}
} else {
// console.log('MESSAGE: "xticks" were not specified. Using defaults.');
return false;
}
yaxis = {
'tickFormatter': null
};
if (typeof config.plot['yticks'] === 'string') {
if (processTicks(config.plot['yticks'], yaxis, 'yunits') === false) {
console.log('ERROR: Could not process the ticks for y-axis.');
return false;
}
} else {
// console.log('MESSAGE: "yticks" were not specified. Using defaults.');
return false;
}
xTicksNames = null;
yTicksNames = null;
if (checkForTicksNames('x') === false) {
return false;
}
if (checkForTicksNames('y') === false) {
return false;
}
return true;
//
// function checkForTicksNames(axisName)
//
// The parameter "axisName" can be either "x" or "y" (string). Depending on it, the function
// will set "xTicksNames" or "yTicksNames" private variable.
//
// This function does not return anything. It sets the private variable "xTicksNames" ("yTicksNames")
// to the object converted by JSON.parse from the XML parameter "plot.xticks_names" ("plot.yticks_names").
// If the "plot.xticks_names" ("plot.yticks_names") is missing or it is not a valid JSON string, then
// "xTicksNames" ("yTicksNames") will be set to "null".
//
// Depending on the "xTicksNames" ("yTicksNames") being "null" or an object, the plot will either draw
// number ticks, or use the names specified by the opbject.
//
function checkForTicksNames(axisName) {
var tmpObj;
if ((axisName !== 'x') && (axisName !== 'y')) {
// This is not an error. This funcion should simply stop executing.
return true;
}
if (
(config.plot.hasOwnProperty(axisName + 'ticks_names') === true) ||
(typeof config.plot[axisName + 'ticks_names'] === 'string')
) {
try {
tmpObj = JSON.parse(config.plot[axisName + 'ticks_names']);
} catch (err) {
console.log(
'ERROR: plot.' + axisName + 'ticks_names is not a valid JSON string.',
'Error message: "' + err.message + '".'
);
return false;
}
if (axisName === 'x') {
xTicksNames = tmpObj;
xaxis.tickFormatter = xAxisTickFormatter;
}
// At this point, we are certain that axisName = 'y'.
else {
yTicksNames = tmpObj;
yaxis.tickFormatter = yAxisTickFormatter;
}
}
}
function processTicks(ticksStr, ticksObj, unitsType) {
var ticksBlobs, tempFloat, tempTicks, c1, c2;
// The 'ticks' setting is a string containing 3 floating-point
// numbers.
ticksBlobs = ticksStr.split(',');
if (ticksBlobs.length !== 3) {
console.log('ERROR: Did not get 3 blobs from ticksStr = "' + ticksStr + '".');
return false;
}
tempFloat = parseFloat(ticksBlobs[0]);
if (isNaN(tempFloat) === false) {
ticksObj.min = tempFloat;
} else {
console.log('ERROR: Invalid "min". ticksBlobs[0] = ', ticksBlobs[0]);
return false;
}
tempFloat = parseFloat(ticksBlobs[1]);
if (isNaN(tempFloat) === false) {
ticksObj.tickSize = tempFloat;
} else {
console.log('ERROR: Invalid "tickSize". ticksBlobs[1] = ', ticksBlobs[1]);
return false;
}
tempFloat = parseFloat(ticksBlobs[2]);
if (isNaN(tempFloat) === false) {
ticksObj.max = tempFloat;
} else {
console.log('ERROR: Invalid "max". ticksBlobs[2] = ', ticksBlobs[2]);
return false;
}
// Is the starting tick to the left of the ending tick (on the
// x-axis)? If not, set default starting and ending tick.
if (ticksObj.min >= ticksObj.max) {
console.log('ERROR: Ticks min >= max.');
return false;
}
// Make sure the range makes sense - i.e. that there are at
// least 3 ticks. If not, set a tickSize which will produce
// 11 ticks. tickSize is the spacing between the ticks.
if (ticksObj.tickSize > ticksObj.max - ticksObj.min) {
console.log('ERROR: tickSize > max - min.');
return false;
}
// units: change last tick to units
if (typeof config.plot[unitsType] === 'string') {
tempTicks = [];
for (c1 = ticksObj.min; c1 <= ticksObj.max; c1 += ticksObj.tickSize) {
c2 = roundToPrec(c1, ticksObj.tickSize);
tempTicks.push([c2, c2]);
}
tempTicks.pop();
tempTicks.push([
roundToPrec(ticksObj.max, ticksObj.tickSize),
config.plot[unitsType]
]);
ticksObj.tickSize = null;
ticksObj.ticks = tempTicks;
}
return true;
function roundToPrec(num, prec) {
var c1, tn1, tn2, digitsBefore, digitsAfter;
tn1 = Math.abs(num);
tn2 = Math.abs(prec);
// Find out number of digits BEFORE the decimal point.
c1 = 0;
tn1 = Math.abs(num);
while (tn1 >= 1) {
c1 += 1;
tn1 /= 10;
}
digitsBefore = c1;
// Find out number of digits AFTER the decimal point.
c1 = 0;
tn1 = Math.abs(num);
while (Math.round(tn1) !== tn1) {
c1 += 1;
tn1 *= 10;
}
digitsAfter = c1;
// For precision, find out number of digits AFTER the
// decimal point.
c1 = 0;
while (Math.round(tn2) !== tn2) {
c1 += 1;
tn2 *= 10;
}
// If precision is more than 1 (no digits after decimal
// points).
if (c1 === 0) {
return num;
}
// If the precision contains digits after the decimal
// point, we apply special rules.
else {
tn1 = Math.abs(num);
// if (digitsAfter > c1) {
tn1 = tn1.toFixed(c1);
// } else {
// tn1 = tn1.toPrecision(digitsBefore + digitsAfter);
// }
}
if (num < 0) {
return -tn1;
}
return tn1;
}
}
}
function setGraphXRange() {
var xRangeStr, xRangeBlobs, tempNum, allParamNames, funcString,
disableAutoReturn;
xrange = {};
if ($.isPlainObject(config.plot.xrange) === false) {
console.log(
'ERROR: Expected config.plot.xrange to be an object. ' +
'It is not.'
);
console.log('config.plot.xrange = ', config.plot.xrange);
return false;
}
if (config.plot.xrange.hasOwnProperty('min') === false) {
console.log(
'ERROR: Expected config.plot.xrange.min to be ' +
'present. It is not.'
);
return false;
}
disableAutoReturn = false;
if (typeof config.plot.xrange.min === 'string') {
funcString = config.plot.xrange.min;
} else if (
($.isPlainObject(config.plot.xrange.min) === true) &&
(config.plot.xrange.min.hasOwnProperty('#text') === true) &&
(typeof config.plot.xrange.min['#text'] === 'string')
) {
funcString = config.plot.xrange.min['#text'];
disableAutoReturn =
config.plot.xrange.min['@disable_auto_return'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
disableAutoReturn = false;
} else {
disableAutoReturn = true;
}
} else {
console.log(
'ERROR: Could not get a function definition for ' +
'xrange.min property.'
);
return false;
}
funcString = $('<div>').html(funcString).text();
if (disableAutoReturn === false) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
console.log(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
allParamNames = state.getAllParameterNames();
allParamNames.push(funcString);
try {
xrange.min = Function.apply(null, allParamNames);
} catch (err) {
console.log(
'ERROR: could not create a function from the string "' +
funcString + '" for xrange.min.'
);
console.log('Error message: "' + err.message + '"');
if (state.showDebugInfo) {
$('#' + gstId).html(
'<div style="color: red;">' + 'ERROR IN ' +
'XML: Could not create a function from the string "' +
funcString + '" for xrange.min.' + '</div>'
);
$('#' + gstId).append(
'<div style="color: red;">' + 'Error ' +
'message: "' + err.message + '".' + '</div>'
);
}
return false;
}
allParamNames.pop();
if (config.plot.xrange.hasOwnProperty('max') === false) {
console.log(
'ERROR: Expected config.plot.xrange.max to be ' +
'present. It is not.'
);
return false;
}
disableAutoReturn = false;
if (typeof config.plot.xrange.max === 'string') {
funcString = config.plot.xrange.max;
} else if (
($.isPlainObject(config.plot.xrange.max) === true) &&
(config.plot.xrange.max.hasOwnProperty('#text') === true) &&
(typeof config.plot.xrange.max['#text'] === 'string')
) {
funcString = config.plot.xrange.max['#text'];
disableAutoReturn =
config.plot.xrange.max['@disable_auto_return'];
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
disableAutoReturn = false;
} else {
disableAutoReturn = true;
}
} else {
console.log(
'ERROR: Could not get a function definition for ' +
'xrange.max property.'
);
return false;
}
funcString = $('<div>').html(funcString).text();
if (disableAutoReturn === false) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
console.log(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
allParamNames.push(funcString);
try {
xrange.max = Function.apply(null, allParamNames);
} catch (err) {
console.log(
'ERROR: could not create a function from the string "' +
funcString + '" for xrange.max.'
);
console.log('Error message: "' + err.message + '"');
if (state.showDebugInfo) {
$('#' + gstId).html(
'<div style="color: red;">' + 'ERROR IN ' +
'XML: Could not create a function from the string "' +
funcString + '" for xrange.max.' + '</div>'
);
$('#' + gstId).append(
'<div style="color: red;">' + 'Error message: "' +
err.message + '".' + '</div>'
);
}
return false;
}
allParamNames.pop();
tempNum = parseInt(config.plot.num_points, 10);
if (isFinite(tempNum) === false) {
tempNum = plotDiv.width() / 5.0;
}
if (
(tempNum < 2) &&
(tempNum > 1000)
) {
console.log(
'ERROR: Number of points is outside the allowed range ' +
'[2, 1000]'
);
console.log('config.plot.num_points = ' + tempNum);
return false;
}
numPoints = tempNum;
return true;
}
function createFunctions() {
var c1;
functions = [];
if (typeof config.functions === 'undefined') {
console.log('ERROR: config.functions is undefined.');
return;
}
if (typeof config.functions["function"] === 'string') {
// If just one function string is present.
addFunction(config.functions["function"]);
} else if ($.isPlainObject(config.functions["function"]) === true) {
// If a function is present, but it also has properties
// defined.
callAddFunction(config.functions["function"]);
} else if ($.isArray(config.functions["function"])) {
// If more than one function is defined.
for (c1 = 0; c1 < config.functions["function"].length; c1 += 1) {
// For each definition, we must check if it is a simple
// string definition, or a complex one with properties.
if (typeof config.functions["function"][c1] === 'string') {
// Simple string.
addFunction(config.functions["function"][c1]);
} else if ($.isPlainObject(config.functions["function"][c1])) {
// Properties are present.
callAddFunction(config.functions["function"][c1]);
}
}
} else {
console.log('ERROR: config.functions.function is of an unsupported type.');
return;
}
return;
// This function will reduce code duplication. We have to call
// the function addFunction() several times passing object
// properties as parameters. Rather than writing them out every
// time, we will have a single place where it is done.
function callAddFunction(obj) {
if (
(obj.hasOwnProperty('@output')) &&
(typeof obj['@output'] === 'string')
) {
// If this function is meant to be calculated for an
// element then skip it.
if ((obj['@output'].toLowerCase() === 'element') ||
(obj['@output'].toLowerCase() === 'none')) {
return;
}
// If this function is meant to be calculated for a
// dynamic element in a label then skip it.
else if (obj['@output'].toLowerCase() === 'plot_label') {
return;
}
// It is an error if '@output' is not 'element',
// 'plot_label', or 'graph'. However, if the '@output'
// attribute is omitted, we will not have reached this.
else if (obj['@output'].toLowerCase() !== 'graph') {
console.log(
'ERROR: Function "output" attribute can be ' +
'either "element", "plot_label", "none" or "graph".'
);
return;
}
}
// The user did not specify an "output" attribute, or it is
// "graph".
addFunction(
obj['#text'],
obj['@color'],
obj['@line'],
obj['@dot'],
obj['@label'],
obj['@point_size'],
obj['@fill_area'],
obj['@bar'],
obj['@disable_auto_return']
);
}
function addFunction(funcString, color, line, dot, label,
pointSize, fillArea, bar, disableAutoReturn) {
var newFunctionObject, func, paramNames, c1, rgxp;
// The main requirement is function string. Without it we can't
// create a function, and the series cannot be calculated.
if (typeof funcString !== 'string') {
return;
}
// Make sure that any HTML entities that were escaped will be
// unescaped. This is done because if a string with escaped
// HTML entities is passed to the Function() constructor, it
// will break.
funcString = $('<div>').html(funcString).text();
// If the user did not specifically turn off this feature,
// check if the function string contains a 'return', and
// prepend a 'return ' to the string if one, or more, is not
// found.
if (
(disableAutoReturn === undefined) ||
(
(typeof disableAutoReturn === 'string') &&
(disableAutoReturn.toLowerCase() !== 'true')
)
) {
if (funcString.search(/return/i) === -1) {
funcString = 'return ' + funcString;
}
} else {
if (funcString.search(/return/i) === -1) {
console.log(
'ERROR: You have specified a JavaScript ' +
'function without a "return" statemnt. Your ' +
'function will return "undefined" by default.'
);
}
}
// Some defaults. If no options are set for the graph, we will
// make sure that at least a line is drawn for a function.
newFunctionObject = {
'line': true,
'dot': false,
'bars': false
};
// Get all of the parameter names defined by the user in the
// XML.
paramNames = state.getAllParameterNames();
// The 'x' is always one of the function parameters.
paramNames.push('x');
// Must make sure that the function body also gets passed to
// the Function constructor.
paramNames.push(funcString);
// Create the function from the function string, and all of the
// available parameters AND the 'x' variable as it's parameters.
// For this we will use the built-in Function object
// constructor.
//
// If something goes wrong during this step, most
// likely the user supplied an invalid JavaScript function body
// string. In this case we will not proceed.
try {
func = Function.apply(null, paramNames);
} catch (err) {
console.log(
'ERROR: The function body "' +
funcString +
'" was not converted by the Function constructor.'
);
console.log('Error message: "' + err.message + '"');
if (state.showDebugInfo) {
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not create a function from the string "' + funcString + '".' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
paramNames.pop();
paramNames.pop();
return;
}
// Return the array back to original state. Remember that it is
// a pointer to original array which is stored in state object.
paramNames.pop();
paramNames.pop();
newFunctionObject['func'] = func;
if (typeof color === 'string') {
newFunctionObject['color'] = color;
}
if (typeof line === 'string') {
if (line.toLowerCase() === 'true') {
newFunctionObject['line'] = true;
} else if (line.toLowerCase() === 'false') {
newFunctionObject['line'] = false;
}
}
if (typeof dot === 'string') {
if (dot.toLowerCase() === 'true') {
newFunctionObject['dot'] = true;
} else if (dot.toLowerCase() === 'false') {
newFunctionObject['dot'] = false;
}
}
if (typeof pointSize === 'string') {
newFunctionObject['pointSize'] = pointSize;
}
if (typeof bar === 'string') {
if (bar.toLowerCase() === 'true') {
newFunctionObject['bars'] = true;
} else if (bar.toLowerCase() === 'false') {
newFunctionObject['bars'] = false;
}
}
if (newFunctionObject['bars'] === true) {
newFunctionObject['line'] = false;
newFunctionObject['dot'] = false;
// To do: See if need to do anything here.
} else if (
(newFunctionObject['dot'] === false) &&
(newFunctionObject['line'] === false)
) {
newFunctionObject['line'] = true;
}
if (newFunctionObject['line'] === true) {
if (typeof fillArea === 'string') {
if (fillArea.toLowerCase() === 'true') {
newFunctionObject['fillArea'] = true;
} else if (fillArea.toLowerCase() === 'false') {
newFunctionObject['fillArea'] = false;
} else {
console.log('ERROR: The attribute fill_area should be either "true" or "false".');
console.log('fill_area = "' + fillArea + '".');
return;
}
}
}
if (typeof label === 'string') {
newFunctionObject.specialLabel = false;
newFunctionObject.pldeHash = [];
// Let's check the label against all of the plde objects.
// plde is an abbreviation for Plot Label Dynamic Elements.
for (c1 = 0; c1 < state.plde.length; c1 += 1) {
rgxp = new RegExp(state.plde[c1].elId, 'g');
// If we find a dynamic element in the label, we will
// hash the current plde object, and indicate that this
// is a special label.
if (rgxp.test(label) === true) {
newFunctionObject.specialLabel = true;
newFunctionObject.pldeHash.push(state.plde[c1]);
}
}
newFunctionObject.label = label;
} else {
newFunctionObject.label = false;
}
functions.push(newFunctionObject);
}
}
// The callback that will be called whenever a constant changes (gets
// updated via a slider or a text input).
function onUpdatePlot(event) {
if (generateData() === true) {
updatePlot();
}
}
function generateData() {
var c0, c1, c3, functionObj, seriesObj, dataPoints, paramValues, x, y,
start, end, step, numNotUndefined;
paramValues = state.getAllParameterValues();
dataSeries = [];
for (c0 = 0; c0 < functions.length; c0 += 1) {
functionObj = functions[c0];
try {
start = xrange.min.apply(window, paramValues);
} catch (err) {
console.log('ERROR: Could not determine xrange start.');
console.log('Error message: "' + err.message + '".');
if (state.showDebugInfo) {
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange start from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
return false;
}
try {
end = xrange.max.apply(window, paramValues);
} catch (err) {
console.log('ERROR: Could not determine xrange end.');
console.log('Error message: "' + err.message + '".');
if (state.showDebugInfo) {
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not determine xrange end from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
return false;
}
seriesObj = {};
dataPoints = [];
// For counting number of points added. In the end we will
// compare this number to 'numPoints' specified in the config
// JSON.
c1 = 0;
step = (end - start) / (numPoints - 1);
// Generate the data points.
for (x = start; x <= end; x += step) {
// Push the 'x' variable to the end of the parameter array.
paramValues.push(x);
// We call the user defined function, passing all of the
// available parameter values. Inside this function they
// will be accessible by their names.
try {
y = functionObj.func.apply(window, paramValues);
} catch (err) {
console.log('ERROR: Could not generate data.');
console.log('Error message: "' + err.message + '".');
if (state.showDebugInfo) {
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from defined function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
return false;
}
// Return the paramValues array to how it was before we
// added 'x' variable to the end of it.
paramValues.pop();
// Add the generated point to the data points set.
dataPoints.push([x, y]);
c1 += 1;
}
// If the last point did not get included because of rounding
// of floating-point number addition, then we will include it
// manually.
if (c1 != numPoints) {
x = end;
paramValues.push(x);
try {
y = functionObj.func.apply(window, paramValues);
} catch (err) {
console.log('ERROR: Could not generate data.');
console.log('Error message: "' + err.message + '".');
if (state.showDebugInfo) {
$('#' + gstId).html('<div style="color: red;">' + 'ERROR IN XML: Could not generate data from function.' + '</div>');
$('#' + gstId).append('<div style="color: red;">' + 'Error message: "' + err.message + '".' + '</div>');
}
return false;
}
paramValues.pop();
dataPoints.push([x, y]);
}
// Put the entire data points set into the series object.
seriesObj.data = dataPoints;
// See if user defined a specific color for this function.
if (functionObj.hasOwnProperty('color') === true) {
seriesObj.color = functionObj.color;
}
// See if a user defined a label for this function.
if (functionObj.label !== false) {
if (functionObj.specialLabel === true) {
(function (c1) {
var tempLabel;
tempLabel = functionObj.label;
while (c1 < functionObj.pldeHash.length) {
tempLabel = tempLabel.replace(
functionObj.pldeHash[c1].elId,
functionObj.pldeHash[c1].func.apply(
window,
state.getAllParameterValues()
)
);
c1 += 1;
}
seriesObj.label = tempLabel;
}(0));
} else {
seriesObj.label = functionObj.label;
}
}
// Should the data points be connected by a line?
seriesObj.lines = {
'show': functionObj.line
};
if (functionObj.hasOwnProperty('fillArea') === true) {
seriesObj.lines.fill = functionObj.fillArea;
}
// Should each data point be represented by a point on the
// graph?
seriesObj.points = {
'show': functionObj.dot
};
seriesObj.bars = {
'show': functionObj.bars,
'barWidth': graphBarWidth
};
if (graphBarAlign !== null) {
seriesObj.bars.align = graphBarAlign;
}
if (functionObj.hasOwnProperty('pointSize')) {
seriesObj.points.radius = functionObj.pointSize;
}
// Add the newly created series object to the series set which
// will be plotted by Flot.
dataSeries.push(seriesObj);
}
if (graphBarAlign === null) {
for (c0 = 0; c0 < numPoints; c0 += 1) {
// Number of points that have a value other than 'undefined' (undefined).
numNotUndefined = 0;
for (c1 = 0; c1 < dataSeries.length; c1 += 1) {
if (dataSeries[c1].bars.show === false) {
continue;
}
if (isFinite(parseInt(dataSeries[c1].data[c0][1])) === true) {
numNotUndefined += 1;
}
}
c3 = 0;
for (c1 = 0; c1 < dataSeries.length; c1 += 1) {
if (dataSeries[c1].bars.show === false) {
continue;
}
dataSeries[c1].data[c0][0] -= graphBarWidth * (0.5 * numNotUndefined - c3);
if (isFinite(parseInt(dataSeries[c1].data[c0][1])) === true) {
c3 += 1;
}
}
}
}
for (c0 = 0; c0 < asymptotes.length; c0 += 1) {
// If the user defined a label for this asympote, then the
// property 'label' will be a string (in the other case it is
// a boolean value 'false'). We will create an empty data set,
// and add to it a label. This solution is a bit _wrong_ , but
// it will have to do for now. Flot JS does not provide a way
// to add labels to markings, and we use markings to generate
// asymptotes.
if (asymptotes[c0].label !== false) {
dataSeries.push({
'data': [],
'label': asymptotes[c0].label,
'color': asymptotes[c0].color
});
}
}
return true;
} // End-of: function generateData
function updatePlot() {
var paramValues, plotObj;
paramValues = state.getAllParameterValues();
if (xaxis.tickFormatter !== null) {
xaxis.ticks = null;
}
if (yaxis.tickFormatter !== null) {
yaxis.ticks = null;
}
// Tell Flot to draw the graph to our specification.
plotObj = $.plot(
plotDiv,
dataSeries,
{
'xaxis': xaxis,
'yaxis': yaxis,
'legend': {
// To show the legend or not. Note, even if 'show' is
// 'true', the legend will only show if labels are
// provided for at least one of the series that are
// going to be plotted.
'show': true,
// A floating point number in the range [0, 1]. The
// smaller the number, the more transparent will the
// legend background become.
'backgroundOpacity': 0
},
'grid': {
'markings': generateMarkings()
}
}
);
updateMovingLabels();
// The first time that the graph gets added to the page, the legend
// is created from scratch. When it appears, MathJax works some
// magic, and all of the specially marked TeX gets rendered nicely.
// The next time when we update the graph, no such thing happens.
// We must ask MathJax to typeset the legend again (well, we will
// ask it to look at our entire graph DIV), the next time it's
// worker queue is available.
MathJax.Hub.Queue([
'Typeset',
MathJax.Hub,
plotDiv.attr('id')
]);
return;
function updateMovingLabels() {
var c1, labelCoord, pointOffset;
for (c1 = 0; c1 < movingLabels.length; c1 += 1) {
if (movingLabels[c1].el === null) {
movingLabels[c1].el = $(
'<div>' +
movingLabels[c1].labelText +
'</div>'
);
movingLabels[c1].el.css('position', 'absolute');
movingLabels[c1].el.css('color', movingLabels[c1].fontColor);
movingLabels[c1].el.css('font-weight', movingLabels[c1].fontWeight);
movingLabels[c1].el.appendTo(plotDiv);
movingLabels[c1].elWidth = movingLabels[c1].el.width();
movingLabels[c1].elHeight = movingLabels[c1].el.height();
} else {
movingLabels[c1].el.detach();
movingLabels[c1].el.appendTo(plotDiv);
}
labelCoord = movingLabels[c1].func.apply(window, paramValues);
pointOffset = plotObj.pointOffset({'x': labelCoord.x, 'y': labelCoord.y});
movingLabels[c1].el.css('left', pointOffset.left - 0.5 * movingLabels[c1].elWidth);
movingLabels[c1].el.css('top', pointOffset.top - 0.5 * movingLabels[c1].elHeight);
}
}
// Generate markings to represent asymptotes defined by the user.
// See the following function for more details:
//
// function processAsymptote()
//
function generateMarkings() {
var c1, asymptote, markings, val;
markings = [];
for (c1 = 0; c1 < asymptotes.length; c1 += 1) {
asymptote = asymptotes[c1];
try {
val = asymptote.func.apply(window, paramValues);
} catch (err) {
console.log('ERROR: Could not generate value from asymptote function.');
console.log('Error message: ', err.message);
continue;
}
if (asymptote.type === 'x') {
markings.push({
'color': asymptote.color,
'lineWidth': 2,
'xaxis': {
'from': val,
'to': val
}
});
} else {
markings.push({
'color': asymptote.color,
'lineWidth': 2,
'yaxis': {
'from': val,
'to': val
}
});
}
}
return markings;
}
}
function xAxisTickFormatter(val, axis) {
if (xTicksNames.hasOwnProperty(val.toFixed(axis.tickDecimals)) === true) {
return xTicksNames[val.toFixed(axis.tickDecimals)];
}
return '';
}
function yAxisTickFormatter(val, axis) {
if (yTicksNames.hasOwnProperty(val.toFixed(axis.tickDecimals)) === true) {
return yTicksNames[val.toFixed(axis.tickDecimals)];
}
return '';
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -1,22 +0,0 @@
/*
* We will add a function that will be called for all GraphicalSliderTool
* xmodule module instances. It must be available globally by design of
* xmodule.
*/
window.GraphicalSliderTool = function (el) {
// All the work will be performed by the GstMain module. We will get access
// to it, and all it's dependencies, via Require JS. Currently Require JS
// is namespaced and is available via a global object RequireJS.
RequireJS.require(['GstMain'], function (GstMain) {
// The GstMain module expects the DOM ID of a Graphical Slider Tool
// element. Since we are given a <section> element which might in
// theory contain multiple graphical_slider_tool <div> elements (each
// with a unique DOM ID), we will iterate over all children, and for
// each match, we will call GstMain module.
$(el).children('.graphical_slider_tool').each(function (index, value) {
JavascriptLoader.executeModuleScripts($(value), function(){
GstMain($(value).attr('id'));
});
});
});
};

View File

@@ -1,86 +0,0 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define(
'GstMain',
// Even though it is not explicitly in this module, we have to specify
// 'GeneralMethods' as a dependency. It expands some of the core JS objects
// with additional useful methods that are used in other modules.
['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph', 'ElOutput', 'GLabelElOutput'],
function (State, GeneralMethods, Sliders, Inputs, Graph, ElOutput, GLabelElOutput) {
return GstMain;
function GstMain(gstId) {
var config, gstClass, state;
if ($('#' + gstId).attr('data-processed') !== 'processed') {
$('#' + gstId).attr('data-processed', 'processed');
} else {
// console.log('MESSAGE: Already processed GST with ID ' + gstId + '. Skipping.');
return;
}
// Get the JSON configuration, parse it, and store as an object.
try {
config = JSON.parse($('#' + gstId + '_json').html()).root;
} catch (err) {
console.log('ERROR: could not parse config JSON.');
console.log('$("#" + gstId + "_json").html() = ', $('#' + gstId + '_json').html());
console.log('JSON.parse(...) = ', JSON.parse($('#' + gstId + '_json').html()));
console.log('config = ', config);
return;
}
// Get the class name of the GST. All elements are assigned a class
// name that is based on the class name of the GST. For example, inputs
// are assigned a class name '{GST class name}_input'.
if (typeof config['@class'] !== 'string') {
console.log('ERROR: Could not get the class name of GST.');
console.log('config["@class"] = ', config['@class']);
return;
}
gstClass = config['@class'];
// Parse the configuration settings for parameters, and store them in a
// state object.
state = State(gstId, config);
state.showDebugInfo = false;
// It is possible that something goes wrong while extracting parameters
// from the JSON config object. In this case, we will not continue.
if (state === undefined) {
console.log('ERROR: The state object was not initialized properly.');
return;
}
// Create the sliders and the text inputs, attaching them to
// appropriate parameters.
Sliders(gstId, state);
Inputs(gstId, gstClass, state);
// Configure functions that output to an element instead of the graph.
ElOutput(config, state);
// Configure functions that output to an element instead of the graph
// label.
GLabelElOutput(config, state);
// Configure and display the graph. Attach event for the graph to be
// updated on any change of a slider or a text input.
Graph(gstId, config, state);
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -1,88 +0,0 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('Inputs', [], function () {
return Inputs;
function Inputs(gstId, gstClass, state) {
var c1, paramName, allParamNames;
allParamNames = state.getAllParameterNames();
for (c1 = 0; c1 < allParamNames.length; c1 += 1) {
$('#' + gstId).find('.' + gstClass + '_input').each(function (index, value) {
var inputDiv, paramName;
paramName = allParamNames[c1];
inputDiv = $(value);
if (paramName === inputDiv.data('var')) {
createInput(inputDiv, paramName);
}
});
}
return;
function createInput(inputDiv, paramName) {
var paramObj;
paramObj = state.getParamObj(paramName);
// Check that the retrieval went OK.
if (paramObj === undefined) {
console.log('ERROR: Could not get a paramObj for parameter "' + paramName + '".');
return;
}
// Bind a function to the 'change' event. Whenever the user changes
// the value of this text input, and presses 'enter' (or clicks
// somewhere else on the page), this event will be triggered, and
// our callback will be called.
inputDiv.bind('change', inputOnChange);
inputDiv.val(paramObj.value);
// Lets style the input element nicely. We will use the button()
// widget for this since there is no native widget for the text
// input.
inputDiv.button().css({
'font': 'inherit',
'color': 'inherit',
'text-align': 'left',
'outline': 'none',
'cursor': 'text',
'height': '15px'
});
// Tell the parameter object from state that we are attaching a
// text input to it. Next time the parameter will be updated with
// a new value, tis input will also be updated.
paramObj.inputDivs.push(inputDiv);
return;
// Update the 'state' - i.e. set the value of the parameter this
// input is attached to to a new value.
//
// This will cause the plot to be redrawn each time after the user
// changes the value in the input. Note that he has to either press
// 'Enter', or click somewhere else on the page in order for the
// 'change' event to be tiggered.
function inputOnChange(event) {
var inputDiv;
inputDiv = $(this);
state.setParameterValue(paramName, inputDiv.val(), inputDiv);
}
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -1,236 +0,0 @@
function jstat(){}
j=jstat;(function(){var initializing=false,fnTest=/xyz/.test(function(){xyz;})?/\b_super\b/:/.*/;this.Class=function(){};Class.extend=function(prop){var _super=this.prototype;initializing=true;var prototype=new this();initializing=false;for(var name in prop){prototype[name]=typeof prop[name]=="function"&&typeof _super[name]=="function"&&fnTest.test(prop[name])?(function(name,fn){return function(){var tmp=this._super;this._super=_super[name];var ret=fn.apply(this,arguments);this._super=tmp;return ret;};})(name,prop[name]):prop[name];}
function Class(){if(!initializing&&this.init)
this.init.apply(this,arguments);}
Class.prototype=prototype;Class.constructor=Class;Class.extend=arguments.callee;return Class;};})();jstat.ONE_SQRT_2PI=0.3989422804014327;jstat.LN_SQRT_2PI=0.9189385332046727417803297;jstat.LN_SQRT_PId2=0.225791352644727432363097614947;jstat.DBL_MIN=2.22507e-308;jstat.DBL_EPSILON=2.220446049250313e-16;jstat.SQRT_32=5.656854249492380195206754896838;jstat.TWO_PI=6.283185307179586;jstat.DBL_MIN_EXP=-999;jstat.SQRT_2dPI=0.79788456080287;jstat.LN_SQRT_PI=0.5723649429247;jstat.seq=function(min,max,length){var r=new Range(min,max,length);return r.getPoints();}
jstat.dnorm=function(x,mean,sd,log){if(mean==null)mean=0;if(sd==null)sd=1;if(log==null)log=false;var n=new NormalDistribution(mean,sd);if(!isNaN(x)){return n._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(n._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.pnorm=function(q,mean,sd,lower_tail,log){if(mean==null)mean=0;if(sd==null)sd=1;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var n=new NormalDistribution(mean,sd);if(!isNaN(q)){return n._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(n._cdf(q[i],lower_tail,log));}
return res;}else{throw"Illegal argument: x";}}
jstat.dlnorm=function(x,meanlog,sdlog,log){if(meanlog==null)meanlog=0;if(sdlog==null)sdlog=1;if(log==null)log=false;var n=new LogNormalDistribution(meanlog,sdlog);if(!isNaN(x)){return n._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(n._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.plnorm=function(q,meanlog,sdlog,lower_tail,log){if(meanlog==null)meanlog=0;if(sdlog==null)sdlog=1;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var n=new LogNormalDistribution(meanlog,sdlog);if(!isNaN(q)){return n._cdf(q,lower_tail,log);}
else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(n._cdf(q[i],lower_tail,log));}
return res;}else{throw"Illegal argument: x";}}
jstat.dbeta=function(x,alpha,beta,ncp,log){if(ncp==null)ncp=0;if(log==null)log=false;var b=new BetaDistribution(alpha,beta);if(!isNaN(x)){return b._pdf(x,log);}
else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(b._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.pbeta=function(q,alpha,beta,ncp,lower_tail,log){if(ncp==null)ncp=0;if(log==null)log=false;if(lower_tail==null)lower_tail=true;var b=new BetaDistribution(alpha,beta);if(!isNaN(q)){return b._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(b._cdf(q[i],lower_tail,log));}
return res;}
else{throw"Illegal argument: x";}}
jstat.dgamma=function(x,shape,rate,scale,log){if(rate==null)rate=1;if(scale==null)scale=1/rate;if(log==null)log=false;var g=new GammaDistribution(shape,scale);if(!isNaN(x)){return g._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(g._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.pgamma=function(q,shape,rate,scale,lower_tail,log){if(rate==null)rate=1;if(scale==null)scale=1/rate;if(lower_tail==null)lower_tail=true;if(log==null)log=false;var g=new GammaDistribution(shape,scale);if(!isNaN(q)){return g._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(g._cdf(q[i],lower_tail,log));}
return res;}else{throw"Illegal argument: x";}}
jstat.dt=function(x,df,ncp,log){if(log==null)log=false;var t=new StudentTDistribution(df,ncp);if(!isNaN(x)){return t._pdf(x,log);}else if(x.length){var res=[];for(var i=0;i<x.length;i++){res.push(t._pdf(x[i],log));}
return res;}else{throw"Illegal argument: x";}}
jstat.pt=function(q,df,ncp,lower_tail,log){if(lower_tail==null)lower_tail=true;if(log==null)log=false;var t=new StudentTDistribution(df,ncp);if(!isNaN(q)){return t._cdf(q,lower_tail,log);}else if(q.length){var res=[];for(var i=0;i<q.length;i++){res.push(t._cdf(q[i],lower_tail,log));}
return res;}else{throw"Illegal argument: x";}}
jstat.plot=function(x,y,options){if(x==null){throw"x is undefined in jstat.plot";}
if(y==null){throw"y is undefined in jstat.plot";}
if(x.length!=y.length){throw"x and y lengths differ in jstat.plot";}
var flotOpt={series:{lines:{},points:{}}};var series=[];if(x.length==undefined){series.push([x,y]);flotOpt.series.points.show=true;}else{for(var i=0;i<x.length;i++){series.push([x[i],y[i]]);}}
var title='jstat graph';if(options!=null){if(options.type!=null){if(options.type=='l'){flotOpt.series.lines.show=true;}else if(options.type=='p'){flotOpt.series.lines.show=false;flotOpt.series.points.show=true;}}
if(options.hover!=null){flotOpt.grid={hoverable:options.hover}}
if(options.main!=null){title=options.main;}}
var now=new Date();var hash=now.getMilliseconds()*now.getMinutes()+now.getSeconds();$('body').append('<div title="'+title+'" style="display: none;" id="'+hash+'"><div id="graph-'+hash+'" style="width:95%; height: 95%"></div></div>');$('#'+hash).dialog({modal:false,width:475,height:475,resizable:true,resize:function(){$.plot($('#graph-'+hash),[series],flotOpt);},open:function(event,ui){var id='#graph-'+hash;$.plot($('#graph-'+hash),[series],flotOpt);}})}
jstat.log10=function(arg){return Math.log(arg)/Math.LN10;}
jstat.toSigFig=function(num,n){if(num==0){return 0;}
var d=Math.ceil(jstat.log10(num<0?-num:num));var power=n-parseInt(d);var magnitude=Math.pow(10,power);var shifted=Math.round(num*magnitude);return shifted/magnitude;}
jstat.trunc=function(x){return(x>0)?Math.floor(x):Math.ceil(x);}
jstat.isFinite=function(x){return(!isNaN(x)&&(x!=Number.POSITIVE_INFINITY)&&(x!=Number.NEGATIVE_INFINITY));}
jstat.dopois_raw=function(x,lambda,give_log){if(lambda==0){if(x==0){return(give_log)?0.0:1.0;}
return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(!jstat.isFinite(lambda))return(give_log)?Number.NEGATIVE_INFINITY:0.0;if(x<0)return(give_log)?Number.NEGATIVE_INFINITY:0.0;if(x<=lambda*jstat.DBL_MIN){return(give_log)?-lambda:Math.exp(-lambda);}
if(lambda<x*jstat.DBL_MIN){var param=-lambda+x*Math.log(lambda)-jstat.lgamma(x+1);return(give_log)?param:Math.exp(param);}
var param1=jstat.TWO_PI*x;var param2=-jstat.stirlerr(x)-jstat.bd0(x,lambda);return(give_log)?-0.5*Math.log(param1)+param2:Math.exp(param2)/Math.sqrt(param1);}
jstat.bd0=function(x,np){var ej,s,s1,v,j;if(!jstat.isFinite(x)||!jstat.isFinite(np)||np==0.0)throw"illegal parameter in jstat.bd0";if(Math.abs(x-np)>0.1*(x+np)){v=(x-np)/(x+np);s=(x-np)*v;ej=2*x*v;v=v*v;for(j=1;;j++){ej*=v;s1=s+ej/((j<<1)+1);if(s1==s)
return(s1);s=s1;}}
return(x*Math.log(x/np)+np-x);}
jstat.stirlerr=function(n){var S0=0.083333333333333333333;var S1=0.00277777777777777777778;var S2=0.00079365079365079365079365;var S3=0.000595238095238095238095238;var S4=0.0008417508417508417508417508;var sferr_halves=[0.0,0.1534264097200273452913848,0.0810614667953272582196702,0.0548141210519176538961390,0.0413406959554092940938221,0.03316287351993628748511048,0.02767792568499833914878929,0.02374616365629749597132920,0.02079067210376509311152277,0.01848845053267318523077934,0.01664469118982119216319487,0.01513497322191737887351255,0.01387612882307074799874573,0.01281046524292022692424986,0.01189670994589177009505572,0.01110455975820691732662991,0.010411265261972096497478567,0.009799416126158803298389475,0.009255462182712732917728637,0.008768700134139385462952823,0.008330563433362871256469318,0.007934114564314020547248100,0.007573675487951840794972024,0.007244554301320383179543912,0.006942840107209529865664152,0.006665247032707682442354394,0.006408994188004207068439631,0.006171712263039457647532867,0.005951370112758847735624416,0.005746216513010115682023589,0.005554733551962801371038690];var nn;if(n<=15.0){nn=n+n;if(nn==parseInt(nn))return(sferr_halves[parseInt(nn)]);return(jstat.lgamma(n+1.0)-(n+0.5)*Math.log(n)+n-jstat.LN_SQRT_2PI);}
nn=n*n;if(n>500)return((S0-S1/nn)/n);if(n>80)return((S0-(S1-S2/nn)/nn)/n);if(n>35)return((S0-(S1-(S2-S3/nn)/nn)/nn)/n);return((S0-(S1-(S2-(S3-S4/nn)/nn)/nn)/nn)/n);}
jstat.lgamma=function(x){function lgammafn_sign(x,sgn){var ans,y,sinpiy;var xmax=2.5327372760800758e+305;var dxrel=1.490116119384765696e-8;if(sgn!=null)sgn=1;if(isNaN(x))return x;if(x<0&&(Math.floor(-x)%2.0)==0)
if(sgn!=null)sgn=-1;if(x<=0&&x==jstat.trunc(x)){console.warn("Negative integer argument in lgammafn_sign");return Number.POSITIVE_INFINITY;}
y=Math.abs(x);if(y<=10)return Math.log(Math.abs(jstat.gamma(x)));if(y>xmax){console.warn("Illegal arguement passed to lgammafn_sign");return Number.POSITIVE_INFINITY;}
if(x>0){if(x>1e17){return(x*(Math.log(x)-1.0));}else if(x>4934720.0){return(jstat.LN_SQRT_2PI+(x-0.5)*Math.log(x)-x);}else{return jstat.LN_SQRT_2PI+(x-0.5)*Math.log(x)-x+jstat.lgammacor(x);}}
sinpiy=Math.abs(Math.sin(Math.PI*y));if(sinpiy==0){throw"Should never happen!!";}
ans=jstat.LN_SQRT_PId2+(x-0.5)*Math.log(y)-x-Math.log(sinpiy)-jstat.lgammacor(y);if(Math.abs((x-jstat.trunc(x-0.5))*ans/x)<dxrel){throw"The answer is less than half the precision argument too close to a negative integer";}
return ans;}
return lgammafn_sign(x,null);}
jstat.gamma=function(x){var xbig=171.624;var p=[-1.71618513886549492533811,24.7656508055759199108314,-379.804256470945635097577,629.331155312818442661052,866.966202790413211295064,-31451.2729688483675254357,-36144.4134186911729807069,66456.1438202405440627855];var q=[-30.8402300119738975254353,315.350626979604161529144,-1015.15636749021914166146,-3107.77167157231109440444,22538.1184209801510330112,4755.84627752788110767815,-134659.959864969306392456,-115132.259675553483497211];var c=[-.001910444077728,8.4171387781295e-4,-5.952379913043012e-4,7.93650793500350248e-4,-.002777777777777681622553,.08333333333333333331554247,.0057083835261];var i,n,parity,fact,xden,xnum,y,z,yi,res,sum,ysq;parity=(0);fact=1.0;n=0;y=x;if(y<=0.0){y=-x;yi=jstat.trunc(y);res=y-yi;if(res!=0.0){if(yi!=jstat.trunc(yi*0.5)*2.0)
parity=(1);fact=-Math.PI/Math.sin(Math.PI*res);y+=1.0;}else{return(Number.POSITIVE_INFINITY);}}
if(y<jstat.DBL_EPSILON){if(y>=jstat.DBL_MIN){res=1.0/y;}else{return(Number.POSITIVE_INFINITY);}}else if(y<12.0){yi=y;if(y<1.0){z=y;y+=1.0;}else{n=parseInt(y)-1;y-=parseFloat(n);z=y-1.0;}
xnum=0.0;xden=1.0;for(i=0;i<8;++i){xnum=(xnum+p[i])*z;xden=xden*z+q[i];}
res=xnum/xden+1.0;if(yi<y){res/=yi;}else if(yi>y){for(i=0;i<n;++i){res*=y;y+=1.0;}}}else{if(y<=xbig){ysq=y*y;sum=c[6];for(i=0;i<6;++i){sum=sum/ysq+c[i];}
sum=sum/y-y+jstat.LN_SQRT_2PI;sum+=(y-0.5)*Math.log(y);res=Math.exp(sum);}else{return(Number.POSITIVE_INFINITY);}}
if(parity)
res=-res;if(fact!=1.0)
res=fact/res;return res;}
jstat.lgammacor=function(x){var algmcs=[+.1666389480451863247205729650822e+0,-.1384948176067563840732986059135e-4,+.9810825646924729426157171547487e-8,-.1809129475572494194263306266719e-10,+.6221098041892605227126015543416e-13,-.3399615005417721944303330599666e-15,+.2683181998482698748957538846666e-17,-.2868042435334643284144622399999e-19,+.3962837061046434803679306666666e-21,-.6831888753985766870111999999999e-23,+.1429227355942498147573333333333e-24,-.3547598158101070547199999999999e-26,+.1025680058010470912000000000000e-27,-.3401102254316748799999999999999e-29,+.1276642195630062933333333333333e-30];var tmp;var nalgm=5;var xbig=94906265.62425156;var xmax=3.745194030963158e306;if(x<10){return Number.NaN;}else if(x>=xmax){throw"Underflow error in lgammacor";}else if(x<xbig){tmp=10/x;return jstat.chebyshev(tmp*tmp*2-1,algmcs,nalgm)/x;}
return 1/(x*12);}
jstat.incompleteBeta=function(a,b,x){function betacf(a,b,x){var MAXIT=100;var EPS=3.0e-12;var FPMIN=1.0e-30;var m,m2,aa,c,d,del,h,qab,qam,qap;qab=a+b;qap=a+1.0;qam=a-1.0;c=1.0;d=1.0-qab*x/qap;if(Math.abs(d)<FPMIN){d=FPMIN;}
d=1.0/d;h=d;for(m=1;m<=MAXIT;m++){m2=2*m;aa=m*(b-m)*x/((qam+m2)*(a+m2));d=1.0+aa*d;if(Math.abs(d)<FPMIN){d=FPMIN;}
c=1.0+aa/c;if(Math.abs(c)<FPMIN){c=FPMIN;}
d=1.0/d;h*=d*c;aa=-(a+m)*(qab+m)*x/((a+m2)*(qap+m2));d=1.0+aa*d;if(Math.abs(d)<FPMIN){d=FPMIN;}
c=1.0+aa/c;if(Math.abs(c)<FPMIN){c=FPMIN;}
d=1.0/d;del=d*c;h*=del;if(Math.abs(del-1.0)<EPS){break;}}
if(m>MAXIT){console.warn("a or b too big, or MAXIT too small in betacf: "+a+", "+b+", "+x+", "+h);return h;}
if(isNaN(h)){console.warn(a+", "+b+", "+x);}
return h;}
var bt;if(x<0.0||x>1.0){throw"bad x in routine incompleteBeta";}
if(x==0.0||x==1.0){bt=0.0;}else{bt=Math.exp(jstat.lgamma(a+b)-jstat.lgamma(a)-jstat.lgamma(b)+a*Math.log(x)+b*Math.log(1.0-x));}
if(x<(a+1.0)/(a+b+2.0)){return bt*betacf(a,b,x)/a;}else{return 1.0-bt*betacf(b,a,1.0-x)/b;}}
jstat.chebyshev=function(x,a,n){var b0,b1,b2,twox;var i;if(n<1||n>1000)return Number.NaN;if(x<-1.1||x>1.1)return Number.NaN;twox=x*2;b2=b1=0;b0=0;for(i=1;i<=n;i++){b2=b1;b1=b0;b0=twox*b1-b2+a[n-i];}
return(b0-b2)*0.5;}
jstat.fmin2=function(x,y){return(x<y)?x:y;}
jstat.log1p=function(x){var ret=0,n=50;if(x<=-1){return Number.NEGATIVE_INFINITY;}
if(x<0||x>1){return Math.log(1+x);}
for(var i=1;i<n;i++){if((i%2)===0){ret-=Math.pow(x,i)/i;}else{ret+=Math.pow(x,i)/i;}}
return ret;}
jstat.expm1=function(x){var y,a=Math.abs(x);if(a<jstat.DBL_EPSILON)return x;if(a>0.697)return Math.exp(x)-1;if(a>1e-8){y=Math.exp(x)-1;}else{y=(x/2+1)*x;}
y-=(1+y)*(jstat.log1p(y)-x);return y;}
jstat.logBeta=function(a,b){var corr,p,q;p=q=a;if(b<p)p=b;if(b>q)q=b;if(p<0){console.warn('Both arguements must be >= 0');return Number.NaN;}
else if(p==0){return Number.POSITIVE_INFINITY;}
else if(!jstat.isFinite(q)){return Number.NEGATIVE_INFINITY;}
if(p>=10){corr=jstat.lgammacor(p)+jstat.lgammacor(q)-jstat.lgammacor(p+q);return Math.log(q)*-0.5+jstat.LN_SQRT_2PI+corr
+(p-0.5)*Math.log(p/(p+q))+q*jstat.log1p(-p/(p+q));}
else if(q>=10){corr=jstat.lgammacor(q)-jstat.lgammacor(p+q);return jstat.lgamma(p)+corr+p-p*Math.log(p+q)
+(q-0.5)*jstat.log1p(-p/(p+q));}
else
return Math.log(jstat.gamma(p)*(jstat.gamma(q)/jstat.gamma(p+q)));}
jstat.dbinom_raw=function(x,n,p,q,give_log){if(give_log==null)give_log=false;var lf,lc;if(p==0){if(x==0){return(give_log)?0.0:1.0;}else{return(give_log)?Number.NEGATIVE_INFINITY:0.0;}}
if(q==0){if(x==n){return(give_log)?0.0:1.0;}else{return(give_log)?Number.NEGATIVE_INFINITY:0.0;}}
if(x==0){if(n==0)return(give_log)?0.0:1.0;lc=(p<0.1)?-jstat.bd0(n,n*q)-n*p:n*Math.log(q);return(give_log)?lc:Math.exp(lc);}
if(x==n){lc=(q<0.1)?-jstat.bd0(n,n*p)-n*q:n*Math.log(p);return(give_log)?lc:Math.exp(lc);}
if(x<0||x>n)return(give_log)?Number.NEGATIVE_INFINITY:0.0;lc=jstat.stirlerr(n)-jstat.stirlerr(x)-jstat.stirlerr(n-x)-jstat.bd0(x,n*p)-jstat.bd0(n-x,n*q);lf=Math.log(jstat.TWO_PI)+Math.log(x)+jstat.log1p(-x/n);return(give_log)?lc-0.5*lf:Math.exp(lc-0.5*lf);}
jstat.max=function(values){var max=Number.NEGATIVE_INFINITY;for(var i=0;i<values.length;i++){if(values[i]>max){max=values[i];}}
return max;}
var Range=Class.extend({init:function(min,max,numPoints){this._minimum=parseFloat(min);this._maximum=parseFloat(max);this._numPoints=parseFloat(numPoints);},getMinimum:function(){return this._minimum;},getMaximum:function(){return this._maximum;},getNumPoints:function(){return this._numPoints;},getPoints:function(){var results=[];var x=this._minimum;var step=(this._maximum-this._minimum)/(this._numPoints-1);for(var i=0;i<this._numPoints;i++){results[i]=parseFloat(x.toFixed(6));x+=step;}
return results;}});Range.validate=function(range){if(!range instanceof Range){return false;}
if(isNaN(range.getMinimum())||isNaN(range.getMaximum())||isNaN(range.getNumPoints())||range.getMaximum()<range.getMinimum()||range.getNumPoints()<=0){return false;}
return true;}
var ContinuousDistribution=Class.extend({init:function(name){this._name=name;},toString:function(){return this._string;},getName:function(){return this._name;},getClassName:function(){return this._name+'Distribution';},density:function(valueOrRange){if(!isNaN(valueOrRange)){return parseFloat(this._pdf(valueOrRange).toFixed(15));}else if(Range.validate(valueOrRange)){var points=valueOrRange.getPoints();var result=[];for(var i=0;i<points.length;i++){result[i]=parseFloat(this._pdf(points[i]));}
return result;}else{throw"Invalid parameter supplied to "+this.getClassName()+".density()";}},cumulativeDensity:function(valueOrRange){if(!isNaN(valueOrRange)){return parseFloat(this._cdf(valueOrRange).toFixed(15));}else if(Range.validate(valueOrRange)){var points=valueOrRange.getPoints();var result=[];for(var i=0;i<points.length;i++){result[i]=parseFloat(this._cdf(points[i]));}
return result;}else{throw"Invalid parameter supplied to "+this.getClassName()+".cumulativeDensity()";}},getRange:function(standardDeviations,numPoints){if(standardDeviations==null){standardDeviations=5;}
if(numPoints==null){numPoints=100;}
var min=this.getMean()-standardDeviations*Math.sqrt(this.getVariance());var max=this.getMean()+standardDeviations*Math.sqrt(this.getVariance());if(this.getClassName()=='GammaDistribution'||this.getClassName()=='LogNormalDistribution'){min=0.0;max=this.getMean()+standardDeviations*Math.sqrt(this.getVariance());}else if(this.getClassName()=='BetaDistribution'){min=0.0;max=1.0;}
var range=new Range(min,max,numPoints);return range;},getVariance:function(){},getMean:function(){},getQuantile:function(p){var self=this;function findClosestMatch(range,p){var ERR=1.0e-5;var xs=range.getPoints();var closestIndex=0;var closestDistance=999;for(var i=0;i<xs.length;i++){var pp=self.cumulativeDensity(xs[i]);var distance=Math.abs(pp-p);if(distance<closestDistance){closestIndex=i;closestDistance=distance;}}
if(closestDistance<=ERR){return xs[closestIndex];}else{var newRange=new Range(xs[closestIndex-1],xs[closestIndex+1],20);return findClosestMatch(newRange,p);}}
var range=this.getRange(5,20);return findClosestMatch(range,p);}});var NormalDistribution=ContinuousDistribution.extend({init:function(mean,sigma){this._super('Normal');this._mean=parseFloat(mean);this._sigma=parseFloat(sigma);this._string="Normal ("+this._mean.toFixed(2)+", "+this._sigma.toFixed(2)+")";},_pdf:function(x,give_log){if(give_log==null){give_log=false;}
var sigma=this._sigma;var mu=this._mean;if(!jstat.isFinite(sigma)){return(give_log)?Number.NEGATIVE_INFINITY:0.0}
if(!jstat.isFinite(x)&&mu==x){return Number.NaN;}
if(sigma<=0){if(sigma<0){throw"invalid sigma in _pdf";}
return(x==mu)?Number.POSITIVE_INFINITY:(give_log)?Number.NEGATIVE_INFINITY:0.0;}
x=(x-mu)/sigma;if(!jstat.isFinite(x)){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
return(give_log?-(jstat.LN_SQRT_2PI+0.5*x*x+Math.log(sigma)):jstat.ONE_SQRT_2PI*Math.exp(-0.5*x*x)/sigma);},_cdf:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;function pnorm_both(x,cum,ccum,i_tail,log_p){var a=[2.2352520354606839287,161.02823106855587881,1067.6894854603709582,18154.981253343561249,0.065682337918207449113];var b=[47.20258190468824187,976.09855173777669322,10260.932208618978205,45507.789335026729956];var c=[0.39894151208813466764,8.8831497943883759412,93.506656132177855979,597.27027639480026226,2494.5375852903726711,6848.1904505362823326,11602.651437647350124,9842.7148383839780218,1.0765576773720192317e-8];var d=[22.266688044328115691,235.38790178262499861,1519.377599407554805,6485.558298266760755,18615.571640885098091,34900.952721145977266,38912.003286093271411,19685.429676859990727];var p=[0.21589853405795699,0.1274011611602473639,0.022235277870649807,0.001421619193227893466,2.9112874951168792e-5,0.02307344176494017303];var q=[1.28426009614491121,0.468238212480865118,0.0659881378689285515,0.00378239633202758244,7.29751555083966205e-5];var xden,xnum,temp,del,eps,xsq,y,i,lower,upper;eps=jstat.DBL_EPSILON*0.5;lower=i_tail!=1;upper=i_tail!=0;y=Math.abs(x);if(y<=0.67448975){if(y>eps){xsq=x*x;xnum=a[4]*xsq;xden=xsq;for(i=0;i<3;++i){xnum=(xnum+a[i])*xsq;xden=(xden+b[i])*xsq;}}else{xnum=xden=0.0;}
temp=x*(xnum+a[3])/(xden+b[3]);if(lower)cum=0.5+temp;if(upper)ccum=0.5-temp;if(log_p){if(lower)cum=Math.log(cum);if(upper)ccum=Math.log(ccum);}}else if(y<=jstat.SQRT_32){xnum=c[8]*y;xden=y;for(i=0;i<7;++i){xnum=(xnum+c[i])*y;xden=(xden+d[i])*y;}
temp=(xnum+c[7])/(xden+d[7]);xsq=jstat.trunc(x*16)/16;del=(x-xsq)*(x+xsq);if(log_p){cum=(-xsq*xsq*0.5)+(-del*0.5)+Math.log(temp);if((lower&&x>0.)||(upper&&x<=0.))
ccum=jstat.log1p(-Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp);}
else{cum=Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp;ccum=1.0-cum;}
if(x>0.0){temp=cum;if(lower){cum=ccum;}
ccum=temp;}}
else if((log_p&&y<1e170)||(lower&&-37.5193<x&&x<8.2924)||(upper&&-8.2924<x&&x<37.5193)){xsq=1.0/(x*x);xnum=p[5]*xsq;xden=xsq;for(i=0;i<4;++i){xnum=(xnum+p[i])*xsq;xden=(xden+q[i])*xsq;}
temp=xsq*(xnum+p[4])/(xden+q[4]);temp=(jstat.ONE_SQRT_2PI-temp)/y;xsq=jstat.trunc(x*16)/16;del=(x-xsq)*(x+xsq);if(log_p){cum=(-xsq*xsq*0.5)+(-del*0.5)+Math.log(temp);if((lower&&x>0.)||(upper&&x<=0.))
ccum=jstat.log1p(-Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp);}
else{cum=Math.exp(-xsq*xsq*0.5)*Math.exp(-del*0.5)*temp;ccum=1.0-cum;}
if(x>0.0){temp=cum;if(lower){cum=ccum;}
ccum=temp;}}else{if(x>0){cum=(log_p)?0.0:1.0;ccum=(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{cum=(log_p)?Number.NEGATIVE_INFINITY:0.0;ccum=(log_p)?0.0:1.0;}}
return[cum,ccum];}
var p,cp;var mu=this._mean;var sigma=this._sigma;var R_DT_0,R_DT_1;if(lower_tail){if(log_p){R_DT_0=Number.NEGATIVE_INFINITY;R_DT_1=0.0;}else{R_DT_0=0.0;R_DT_1=1.0;}}else{if(log_p){R_DT_0=0.0;R_DT_1=Number.NEGATIVE_INFINITY;}else{R_DT_0=1.0;R_DT_1=0.0;}}
if(!jstat.isFinite(x)&&mu==x)return Number.NaN;if(sigma<=0){if(sigma<0){console.warn("Sigma is less than 0");return Number.NaN;}
return(x<mu)?R_DT_0:R_DT_1;}
p=(x-mu)/sigma;if(!jstat.isFinite(p)){return(x<mu)?R_DT_0:R_DT_1;}
x=p;var result=pnorm_both(x,p,cp,(lower_tail?false:true),log_p);return(lower_tail?result[0]:result[1]);},getMean:function(){return this._mean;},getSigma:function(){return this._sigma;},getVariance:function(){return this._sigma*this._sigma;}});var LogNormalDistribution=ContinuousDistribution.extend({init:function(location,scale){this._super('LogNormal')
this._location=parseFloat(location);this._scale=parseFloat(scale);this._string="LogNormal ("+this._location.toFixed(2)+", "+this._scale.toFixed(2)+")";},_pdf:function(x,give_log){var y;var sdlog=this._scale;var meanlog=this._location;if(give_log==null){give_log=false;}
if(sdlog<=0)throw"Illegal parameter in _pdf";if(x<=0){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
y=(Math.log(x)-meanlog)/sdlog;return(give_log?-(jstat.LN_SQRT_2PI+0.5*y*y+Math.log(x*sdlog)):jstat.ONE_SQRT_2PI*Math.exp(-0.5*y*y)/(x*sdlog));},_cdf:function(x,lower_tail,log_p){var sdlog=this._scale;var meanlog=this._location;if(lower_tail==null){lower_tail=true;}
if(log_p==null){log_p=false;}
if(sdlog<=0){throw"illegal std in _cdf";}
if(x>0){var nd=new NormalDistribution(meanlog,sdlog);return nd._cdf(Math.log(x),lower_tail,log_p);}
if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.0:1.0;}},getLocation:function(){return this._location;},getScale:function(){return this._scale;},getMean:function(){return Math.exp((this._location+this._scale)/2);},getVariance:function(){var ans=(Math.exp(this._scale)-1)*Math.exp(2*this._location+this._scale);return ans;}});var GammaDistribution=ContinuousDistribution.extend({init:function(shape,scale){this._super('Gamma');this._shape=parseFloat(shape);this._scale=parseFloat(scale);this._string="Gamma ("+this._shape.toFixed(2)+", "+this._scale.toFixed(2)+")";},_pdf:function(x,give_log){var pr;var shape=this._shape;var scale=this._scale;if(give_log==null){give_log=false;}
if(shape<0||scale<=0){throw"Illegal argument in _pdf";}
if(x<0){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(shape==0){return(x==0)?Number.POSITIVE_INFINITY:(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(x==0){if(shape<1)return Number.POSITIVE_INFINITY;if(shape>1)return(give_log)?Number.NEGATIVE_INFINITY:0.0;return(give_log)?-Math.log(scale):1/scale;}
if(shape<1){pr=jstat.dopois_raw(shape,x/scale,give_log);return give_log?pr+Math.log(shape/x):pr*shape/x;}
pr=jstat.dopois_raw(shape-1,x/scale,give_log);return give_log?pr-Math.log(scale):pr/scale;},_cdf:function(x,lower_tail,log_p){function USE_PNORM(){pn1=Math.sqrt(alph)*3.0*(Math.pow(x/alph,1.0/3.0)+1.0/(9.0*alph)-1.0);var norm_dist=new NormalDistribution(0.0,1.0);return norm_dist._cdf(pn1,lower_tail,log_p);}
if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var alph=this._shape;var scale=this._scale;var xbig=1.0e+8;var xlarge=1.0e+37;var alphlimit=1e5;var pn1,pn2,pn3,pn4,pn5,pn6,arg,a,b,c,an,osum,sum,n,pearson;if(alph<=0.||scale<=0.){console.warn('Invalid gamma params in _cdf');return Number.NaN;}
x/=scale;if(isNaN(x))return x;if(x<=0.0){if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.0:1.0;}}
if(alph>alphlimit){return USE_PNORM();}
if(x>xbig*alph){if(x>jstat.DBL_MAX*alph){if(lower_tail){return(log_p)?0.0:1.0;}else{return(log_p)?Number.NEGATIVE_INFINITY:0.0;}}else{return USE_PNORM();}}
if(x<=1.0||x<alph){pearson=1;arg=alph*Math.log(x)-x-jstat.lgamma(alph+1.0);c=1.0;sum=1.0;a=alph;do{a+=1.0;c*=x/a;sum+=c;}while(c>jstat.DBL_EPSILON*sum);}else{pearson=0;arg=alph*Math.log(x)-x-jstat.lgamma(alph);a=1.-alph;b=a+x+1.;pn1=1.;pn2=x;pn3=x+1.;pn4=x*b;sum=pn3/pn4;for(n=1;;n++){a+=1.;b+=2.;an=a*n;pn5=b*pn3-an*pn1;pn6=b*pn4-an*pn2;if(Math.abs(pn6)>0.){osum=sum;sum=pn5/pn6;if(Math.abs(osum-sum)<=jstat.DBL_EPSILON*jstat.fmin2(1.0,sum))
break;}
pn1=pn3;pn2=pn4;pn3=pn5;pn4=pn6;if(Math.abs(pn5)>=xlarge){pn1/=xlarge;pn2/=xlarge;pn3/=xlarge;pn4/=xlarge;}}}
arg+=Math.log(sum);lower_tail=(lower_tail==pearson);if(log_p&&lower_tail)
return(arg);if(lower_tail){return Math.exp(arg);}else{if(log_p){return(arg>-Math.LN2)?Math.log(-jstat.expm1(arg)):jstat.log1p(-Math.exp(arg));}else{return-jstat.expm1(arg);}}},getShape:function(){return this._shape;},getScale:function(){return this._scale;},getMean:function(){return this._shape*this._scale;},getVariance:function(){return this._shape*Math.pow(this._scale,2);}});var BetaDistribution=ContinuousDistribution.extend({init:function(alpha,beta){this._super('Beta');this._alpha=parseFloat(alpha);this._beta=parseFloat(beta);this._string="Beta ("+this._alpha.toFixed(2)+", "+this._beta.toFixed(2)+")";},_pdf:function(x,give_log){if(give_log==null)give_log=false;var a=this._alpha;var b=this._beta;var lval;if(a<=0||b<=0){console.warn('Illegal arguments in _pdf');return Number.NaN;}
if(x<0||x>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(x==0){if(a>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(a<1){return Number.POSITIVE_INFINITY;}
return(give_log)?Math.log(b):b;}
if(x==1){if(b>1){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(b<1){return Number.POSITIVE_INFINITY;}
return(give_log)?Math.log(a):a;}
if(a<=2||b<=2){lval=(a-1)*Math.log(x)+(b-1)*jstat.log1p(-x)-jstat.logBeta(a,b);}else{lval=Math.log(a+b-1)+jstat.dbinom_raw(a-1,a+b-2,x,1-x,true);}
return(give_log)?lval:Math.exp(lval);},_cdf:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var pin=this._alpha;var qin=this._beta;if(pin<=0||qin<=0){console.warn('Invalid argument in _cdf');return Number.NaN;}
if(x<=0){if(lower_tail){return(log_p)?Number.NEGATIVE_INFINITY:0.0;}else{return(log_p)?0.1:1.0;}}
if(x>=1){if(lower_tail){return(log_p)?0.1:1.0;}else{return(log_p)?Number.NEGATIVE_INFINITY:0.0;}}
return jstat.incompleteBeta(pin,qin,x);},getAlpha:function(){return this._alpha;},getBeta:function(){return this._beta;},getMean:function(){return this._alpha/(this._alpha+this._beta);},getVariance:function(){var ans=(this._alpha*this._beta)/(Math.pow(this._alpha+this._beta,2)*(this._alpha+this._beta+1));return ans;}});var StudentTDistribution=ContinuousDistribution.extend({init:function(degreesOfFreedom,mu){this._super('StudentT');this._dof=parseFloat(degreesOfFreedom);if(mu!=null){this._mu=parseFloat(mu);this._string="StudentT ("+this._dof.toFixed(2)+", "+this._mu.toFixed(2)+")";}else{this._mu=0.0;this._string="StudentT ("+this._dof.toFixed(2)+")";}},_pdf:function(x,give_log){if(give_log==null)give_log=false;if(this._mu==null){return this._dt(x,give_log);}else{var y=this._dnt(x,give_log);if(y>1){console.warn('x:'+x+', y: '+y);}
return y;}},_cdf:function(x,lower_tail,give_log){if(lower_tail==null)lower_tail=true;if(give_log==null)give_log=false;if(this._mu==null){return this._pt(x,lower_tail,give_log);}else{return this._pnt(x,lower_tail,give_log);}},_dt:function(x,give_log){var t,u;var n=this._dof;if(n<=0){console.warn('Invalid parameters in _dt');return Number.NaN;}
if(!jstat.isFinite(x)){return(give_log)?Number.NEGATIVE_INFINITY:0.0;}
if(!jstat.isFinite(n)){var norm=new NormalDistribution(0.0,1.0);return norm.density(x,give_log);}
t=-jstat.bd0(n/2.0,(n+1)/2.0)+jstat.stirlerr((n+1)/2.0)-jstat.stirlerr(n/2.0);if(x*x>0.2*n)
u=Math.log(1+x*x/n)*n/2;else
u=-jstat.bd0(n/2.0,(n+x*x)/2.0)+x*x/2.0;var p1=jstat.TWO_PI*(1+x*x/n);var p2=t-u;return(give_log)?-0.5*Math.log(p1)+p2:Math.exp(p2)/Math.sqrt(p1);},_dnt:function(x,give_log){if(give_log==null)give_log=false;var df=this._dof;var ncp=this._mu;var u;if(df<=0.0){console.warn("Illegal arguments _dnf");return Number.NaN;}
if(ncp==0.0){return this._dt(x,give_log);}
if(!jstat.isFinite(x)){if(give_log){return Number.NEGATIVE_INFINITY;}else{return 0.0;}}
if(!isFinite(df)||df>1e8){var dist=new NormalDistribution(ncp,1.);return dist.density(x,give_log);}
if(Math.abs(x)>Math.sqrt(df*jstat.DBL_EPSILON)){var newT=new StudentTDistribution(df+2,ncp);u=Math.log(df)-Math.log(Math.abs(x))+
Math.log(Math.abs(newT._pnt(x*Math.sqrt((df+2)/df),true,false)-
this._pnt(x,true,false)));}
else{u=jstat.lgamma((df+1)/2)-jstat.lgamma(df/2)
-.5*(Math.log(Math.PI)+Math.log(df)+ncp*ncp);}
return(give_log?u:Math.exp(u));},_pt:function(x,lower_tail,log_p){if(lower_tail==null)lower_tail=true;if(log_p==null)log_p=false;var val,nx;var n=this._dof;var DT_0,DT_1;if(lower_tail){if(log_p){DT_0=Number.NEGATIVE_INFINITY;DT_1=1.;}else{DT_0=0.;DT_1=1.;}}else{if(log_p){DT_0=0.;DT_1=Number.NEGATIVE_INFINITY;}else{DT_0=1.;DT_1=0.;}}
if(n<=0.0){console.warn("Invalid T distribution _pt");return Number.NaN;}
var norm=new NormalDistribution(0,1);if(!jstat.isFinite(x)){return(x<0)?DT_0:DT_1;}
if(!jstat.isFinite(n)){return norm._cdf(x,lower_tail,log_p);}
if(n>4e5){val=1./(4.*n);return norm._cdf(x*(1.-val)/sqrt(1.+x*x*2.*val),lower_tail,log_p);}
nx=1+(x/n)*x;if(nx>1e100){var lval;lval=-0.5*n*(2*Math.log(Math.abs(x))-Math.log(n))
-jstat.logBeta(0.5*n,0.5)-Math.log(0.5*n);val=log_p?lval:Math.exp(lval);}else{if(n>x*x){var beta=new BetaDistribution(0.5,n/2.);return beta._cdf(x*x/(n+x*x),false,log_p);}else{beta=new BetaDistribution(n/2.,0.5);return beta._cdf(1./nx,true,log_p);}}
if(x<=0.)
lower_tail=!lower_tail;if(log_p){if(lower_tail)return jstat.log1p(-0.5*Math.exp(val));else return val-M_LN2;}
else{val/=2.;if(lower_tail){return(0.5-val+0.5);}else{return val;}}},_pnt:function(t,lower_tail,log_p){var dof=this._dof;var ncp=this._mu;var DT_0,DT_1;if(lower_tail){if(log_p){DT_0=Number.NEGATIVE_INFINITY;DT_1=1.;}else{DT_0=0.;DT_1=1.;}}else{if(log_p){DT_0=0.;DT_1=Number.NEGATIVE_INFINITY;}else{DT_0=1.;DT_1=0.;}}
var albeta,a,b,del,errbd,lambda,rxb,tt,x;var geven,godd,p,q,s,tnc,xeven,xodd;var it,negdel;var ITRMAX=1000;var ERRMAX=1.e-7;if(dof<=0.0){return Number.NaN;}else if(dof==0.0){return this._pt(t);}
if(!jstat.isFinite(t)){return(t<0)?DT_0:DT_1;}
if(t>=0.){negdel=false;tt=t;del=ncp;}else{if(ncp>=40&&(!log_p||!lower_tail)){return DT_0;}
negdel=true;tt=-t;del=-ncp;}
if(dof>4e5||del*del>2*Math.LN2*(-(jstat.DBL_MIN_EXP))){s=1./(4.*dof);var norm=new NormalDistribution(del,Math.sqrt(1.+tt*tt*2.*s));var result=norm._cdf(tt*(1.-s),lower_tail!=negdel,log_p);return result;}
x=t*t;rxb=dof/(x+dof);x=x/(x+dof);if(x>0.){lambda=del*del;p=.5*Math.exp(-.5*lambda);if(p==0.){console.warn("underflow in _pnt");return DT_0;}
q=jstat.SQRT_2dPI*p*del;s=.5-p;if(s<1e-7){s=-0.5*jstat.expm1(-0.5*lambda);}
a=.5;b=.5*dof;rxb=Math.pow(rxb,b);albeta=jstat.LN_SQRT_PI+jstat.lgamma(b)-jstat.lgamma(.5+b);xodd=jstat.incompleteBeta(a,b,x);godd=2.*rxb*Math.exp(a*Math.log(x)-albeta);tnc=b*x;xeven=(tnc<jstat.DBL_EPSILON)?tnc:1.-rxb;geven=tnc*rxb;tnc=p*xodd+q*xeven;for(it=1;it<=ITRMAX;it++){a+=1.;xodd-=godd;xeven-=geven;godd*=x*(a+b-1.)/a;geven*=x*(a+b-.5)/(a+.5);p*=lambda/(2*it);q*=lambda/(2*it+1);tnc+=p*xodd+q*xeven;s-=p;if(s<-1.e-10){console.write("precision error _pnt");break;}
if(s<=0&&it>1)break;errbd=2.*s*(xodd-godd);if(Math.abs(errbd)<ERRMAX)break;}
if(it==ITRMAX){throw"Non-convergence _pnt";}}else{tnc=0.;}
norm=new NormalDistribution(0,1);tnc+=norm._cdf(-del,true,false);lower_tail=lower_tail!=negdel;if(tnc>1-1e-10&&lower_tail){console.warn("precision error _pnt");}
var res=jstat.fmin2(tnc,1.);if(lower_tail){if(log_p){return Math.log(res);}else{return res;}}else{if(log_p){return jstat.log1p(-(res));}else{return(0.5-(res)+0.5);}}},getDegreesOfFreedom:function(){return this._dof;},getNonCentralityParameter:function(){return this._mu;},getMean:function(){if(this._dof>1){var ans=(1/2)*Math.log(this._dof/2)+jstat.lgamma((this._dof-1)/2)-jstat.lgamma(this._dof/2)
return Math.exp(ans)*this._mu;}else{return Number.NaN;}},getVariance:function(){if(this._dof>2){var ans=this._dof*(1+this._mu*this._mu)/(this._dof-2)-(((this._mu*this._mu*this._dof)/2)*Math.pow(Math.exp(jstat.lgamma((this._dof-1)/2)-jstat.lgamma(this._dof/2)),2));return ans;}else{return Number.NaN;}}});var Plot=Class.extend({init:function(id,options){this._container='#'+String(id);this._plots=[];this._flotObj=null;this._locked=false;if(options!=null){this._options=options;}else{this._options={};}},getContainer:function(){return this._container;},getGraph:function(){return this._flotObj;},setData:function(data){this._plots=data;},clear:function(){this._plots=[];},showLegend:function(){this._options.legend={show:true}
this.render();},hideLegend:function(){this._options.legend={show:false}
this.render();},render:function(){this._flotObj=null;this._flotObj=$.plot($(this._container),this._plots,this._options);}});var DistributionPlot=Plot.extend({init:function(id,distribution,range,options){this._super(id,options);this._showPDF=true;this._showCDF=false;this._pdfValues=[];this._cdfValues=[];this._maxY=1;this._plotType='line';this._fill=false;this._distribution=distribution;if(range!=null&&Range.validate(range)){this._range=range;}else{this._range=this._distribution.getRange();}
if(this._distribution!=null){this._maxY=this._generateValues();}else{this._options.xaxis={min:range.getMinimum(),max:range.getMaximum()}
this._options.yaxis={max:1}}
this.render();},setHover:function(bool){if(bool){if(this._options.grid==null){this._options.grid={hoverable:true,mouseActiveRadius:25}}else{this._options.grid.hoverable=true,this._options.grid.mouseActiveRadius=25}
function showTooltip(x,y,contents,color){$('<div id="jstat_tooltip">'+contents+'</div>').css({position:'absolute',display:'none',top:y+15,'font-size':'small',left:x+5,border:'1px solid '+color[1],color:color[2],padding:'5px','background-color':color[0],opacity:0.80}).appendTo("body").show();}
var previousPoint=null;$(this._container).bind("plothover",function(event,pos,item){$("#x").text(pos.x.toFixed(2));$("#y").text(pos.y.toFixed(2));if(item){if(previousPoint!=item.datapoint){previousPoint=item.datapoint;$("#jstat_tooltip").remove();var x=jstat.toSigFig(item.datapoint[0],2),y=jstat.toSigFig(item.datapoint[1],2);var text=null;var color=item.series.color;if(item.series.label=='PDF'){text="P("+x+") = "+y;color=["#fee","#fdd","#C05F5F"];}else{text="F("+x+") = "+y;color=["#eef","#ddf","#4A4AC0"];}
showTooltip(item.pageX,item.pageY,text,color);}}
else{$("#jstat_tooltip").remove();previousPoint=null;}});$(this._container).bind("mouseleave",function(){if($('#jstat_tooltip').is(':visible')){$('#jstat_tooltip').remove();previousPoint=null;}});}else{if(this._options.grid==null){this._options.grid={hoverable:false}}else{this._options.grid.hoverable=false}
$(this._container).unbind("plothover");}
this.render();},setType:function(type){this._plotType=type;var lines={};var points={};if(this._plotType=='line'){lines.show=true;points.show=false;}else if(this._plotType=='points'){lines.show=false;points.show=true;}else if(this._plotType=='both'){lines.show=true;points.show=true;}
if(this._options.series==null){this._options.series={lines:lines,points:points}}else{if(this._options.series.lines==null){this._options.series.lines=lines;}else{this._options.series.lines.show=lines.show;}
if(this._options.series.points==null){this._options.series.points=points;}else{this._options.series.points.show=points.show;}}
this.render();},setFill:function(bool){this._fill=bool;if(this._options.series==null){this._options.series={lines:{fill:bool}}}else{if(this._options.series.lines==null){this._options.series.lines={fill:bool}}else{this._options.series.lines.fill=bool;}}
this.render();},clear:function(){this._super();this._distribution=null;this._pdfValues=[];this._cdfValues=[];this.render();},_generateValues:function(){this._cdfValues=[];this._pdfValues=[];var xs=this._range.getPoints();this._options.xaxis={min:xs[0],max:xs[xs.length-1]}
var pdfs=this._distribution.density(this._range);var cdfs=this._distribution.cumulativeDensity(this._range);for(var i=0;i<xs.length;i++){if(pdfs[i]==Number.POSITIVE_INFINITY||pdfs[i]==Number.NEGATIVE_INFINITY){pdfs[i]=null;}
if(cdfs[i]==Number.POSITIVE_INFINITY||cdfs[i]==Number.NEGATIVE_INFINITY){cdfs[i]=null;}
this._pdfValues.push([xs[i],pdfs[i]]);this._cdfValues.push([xs[i],cdfs[i]]);}
return jstat.max(pdfs);},showPDF:function(){this._showPDF=true;this.render();},hidePDF:function(){this._showPDF=false;this.render();},showCDF:function(){this._showCDF=true;this.render();},hideCDF:function(){this._showCDF=false;this.render();},setDistribution:function(distribution,range){this._distribution=distribution;if(range!=null){this._range=range;}else{this._range=distribution.getRange();}
this._maxY=this._generateValues();this._options.yaxis={max:this._maxY*1.1}
this.render();},getDistribution:function(){return this._distribution;},getRange:function(){return this._range;},setRange:function(range){this._range=range;this._generateValues();this.render();},render:function(){if(this._distribution!=null){if(this._showPDF&&this._showCDF){this.setData([{yaxis:1,data:this._pdfValues,color:'rgb(237,194,64)',clickable:false,hoverable:true,label:"PDF"},{yaxis:2,data:this._cdfValues,clickable:false,color:'rgb(175,216,248)',hoverable:true,label:"CDF"}]);this._options.yaxis={max:this._maxY*1.1}}else if(this._showPDF){this.setData([{data:this._pdfValues,hoverable:true,color:'rgb(237,194,64)',clickable:false,label:"PDF"}]);this._options.yaxis={max:this._maxY*1.1}}else if(this._showCDF){this.setData([{data:this._cdfValues,hoverable:true,color:'rgb(175,216,248)',clickable:false,label:"CDF"}]);this._options.yaxis={max:1.1}}}else{this.setData([]);}
this._super();}});var DistributionFactory={};DistributionFactory.build=function(json){if(json.NormalDistribution){if(json.NormalDistribution.mean!=null&&json.NormalDistribution.standardDeviation!=null){return new NormalDistribution(json.NormalDistribution.mean[0],json.NormalDistribution.standardDeviation[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.LogNormalDistribution){if(json.LogNormalDistribution.location!=null&&json.LogNormalDistribution.scale!=null){return new LogNormalDistribution(json.LogNormalDistribution.location[0],json.LogNormalDistribution.scale[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.BetaDistribution){if(json.BetaDistribution.alpha!=null&&json.BetaDistribution.beta!=null){return new BetaDistribution(json.BetaDistribution.alpha[0],json.BetaDistribution.beta[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.GammaDistribution){if(json.GammaDistribution.shape!=null&&json.GammaDistribution.scale!=null){return new GammaDistribution(json.GammaDistribution.shape[0],json.GammaDistribution.scale[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else if(json.StudentTDistribution){if(json.StudentTDistribution.degreesOfFreedom!=null&&json.StudentTDistribution.nonCentralityParameter!=null){return new StudentTDistribution(json.StudentTDistribution.degreesOfFreedom[0],json.StudentTDistribution.nonCentralityParameter[0]);}else if(json.StudentTDistribution.degreesOfFreedom!=null){return new StudentTDistribution(json.StudentTDistribution.degreesOfFreedom[0]);}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}else{throw"Malformed JSON provided to DistributionBuilder "+json;}}

View File

@@ -1,89 +0,0 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('Sliders', [], function () {
return Sliders;
function Sliders(gstId, state) {
var c1, paramName, allParamNames, sliderDiv;
allParamNames = state.getAllParameterNames();
for (c1 = 0; c1 < allParamNames.length; c1 += 1) {
paramName = allParamNames[c1];
sliderDiv = $('#' + gstId + '_slider_' + paramName);
if (sliderDiv.length === 1) {
createSlider(sliderDiv, paramName);
} else if (sliderDiv.length > 1) {
console.log('ERROR: Found more than one slider for the parameter "' + paramName + '".');
console.log('sliderDiv.length = ', sliderDiv.length);
} // else {
// console.log('MESSAGE: Did not find a slider for the parameter "' + paramName + '".');
// }
}
function createSlider(sliderDiv, paramName) {
var paramObj;
paramObj = state.getParamObj(paramName);
// Check that the retrieval went OK.
if (paramObj === undefined) {
console.log('ERROR: Could not get a paramObj for parameter "' + paramName + '".');
return;
}
// Create a jQuery UI slider from the slider DIV. We will set
// starting parameters, and will also attach a handler to update
// the 'state' on the 'slide' event.
sliderDiv.slider({
'min': paramObj.min,
'max': paramObj.max,
'value': paramObj.value,
'step': paramObj.step
});
// Tell the parameter object stored in state that we have a slider
// that is attached to it. Next time when the parameter changes, it
// will also update the value of this slider.
paramObj.sliderDiv = sliderDiv;
// Atach callbacks to update the slider's parameter.
paramObj.sliderDiv.on('slide', sliderOnSlide);
paramObj.sliderDiv.on('slidechange', sliderOnChange);
return;
// Update the 'state' - i.e. set the value of the parameter this
// slider is attached to to a new value.
//
// This will cause the plot to be redrawn each time after the user
// drags the slider handle and releases it.
function sliderOnSlide(event, ui) {
// Last parameter passed to setParameterValue() will be 'true'
// so that the function knows we are a slider, and it can
// change the our value back in the case when the new value is
// invalid for some reason.
if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'slide') === undefined) {
console.log('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".');
}
}
function sliderOnChange(event, ui) {
if (state.setParameterValue(paramName, ui.value, sliderDiv, true, 'change') === undefined) {
console.log('ERROR: Could not update the parameter named "' + paramName + '" with the value "' + ui.value + '".');
}
}
}
}
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -1,395 +0,0 @@
// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
// define() functions from Require JS available inside the anonymous function.
(function (requirejs, require, define) {
define('State', [], function () {
var stateInst;
// Since there will be (can be) multiple GST on a page, and each will have
// a separate state, we will create a factory constructor function. The
// constructor will expect the ID of the DIV with the GST contents, and the
// configuration object (parsed from a JSON string). It will return an
// object containing methods to set and get the private state properties.
stateInst = 0;
// This module defines and returns a factory constructor.
return State;
function State(gstId, config) {
var parameters, allParameterNames, allParameterValues,
plotDiv, dynamicEl, dynamicElByElId;
dynamicEl = [];
dynamicElByElId = {};
stateInst += 1;
// console.log('MESSAGE: Creating state instance # ' + stateInst + '.');
// Initially, there are no parameters to track. So, we will instantiate
// an empty object.
//
// As we parse the JSON config object, we will add parameters as
// named properties. For example
//
// parameters.a = {...};
//
// will be created for the parameter 'a'.
parameters = {};
// Check that the required parameters config object is available.
if ($.isPlainObject(config.parameters) === false) {
console.log('ERROR: Expected config.parameters to be an object. It is not.');
console.log('config.parameters = ', config.parameters);
return;
}
// If config.parameters.param is an array, pass it to the processor
// element by element.
if ($.isArray(config.parameters.param) === true) {
(function (c1) {
while (c1 < config.parameters.param.length) {
processParameter(config.parameters.param[c1]);
c1 += 1;
}
}(0));
}
// If config.parameters.param is an object, pass this object to the
// processor directly.
else if ($.isPlainObject(config.parameters.param) === true) {
processParameter(config.parameters.param);
}
// If config.parameters.param is some other type, report an error and
// do not continue.
else {
console.log('ERROR: config.parameters.param is of an unsupported type.');
console.log('config.parameters.param = ', config.parameters.param);
return;
}
// Instead of building these arrays every time when some component
// requests them, we will create them in the beginning, and then update
// each element individually when some parameter's value changes.
//
// Then we can just return the required array, instead of iterating
// over all of the properties of the 'parameters' object, and
// extracting their names/values one by one.
allParameterNames = [];
allParameterValues = [];
// Populate 'allParameterNames', and 'allParameterValues' with data.
generateHelperArrays();
// The constructor will return an object with methods to operate on
// it's private properties.
return {
'getParameterValue': getParameterValue,
'setParameterValue': setParameterValue,
'getParamObj': getParamObj,
'getAllParameterNames': getAllParameterNames,
'getAllParameterValues': getAllParameterValues,
'bindUpdatePlotEvent': bindUpdatePlotEvent,
'addDynamicEl': addDynamicEl,
// plde is an abbreviation for Plot Label Dynamic Elements.
plde: []
};
function getAllParameterNames() {
return allParameterNames;
}
function getAllParameterValues() {
return allParameterValues;
}
function getParamObj(paramName) {
if (parameters.hasOwnProperty(paramName) === false) {
console.log('ERROR: Object parameters does not have a property named "' + paramName + '".');
return;
}
return parameters[paramName];
}
function bindUpdatePlotEvent(newPlotDiv, callback) {
plotDiv = newPlotDiv;
plotDiv.bind('update_plot', callback);
}
function addDynamicEl(el, func, elId, updateOnEvent) {
var newLength;
newLength = dynamicEl.push({
'el': el,
'func': func,
'elId': elId,
'updateOnEvent': updateOnEvent
});
if (typeof dynamicElByElId[elId] !== 'undefined') {
console.log(
'ERROR: Duplicate dynamic element ID "' + elId + '" found.'
);
} else {
dynamicElByElId[elId] = dynamicEl[newLength - 1];
}
}
function getParameterValue(paramName) {
// If the name of the constant is not tracked by state, return an
// 'undefined' value.
if (parameters.hasOwnProperty(paramName) === false) {
console.log('ERROR: Object parameters does not have a property named "' + paramName + '".');
return;
}
return parameters[paramname].value;
}
// ####################################################################
//
// Function: setParameterValue(paramName, paramValue, element)
// --------------------------------------------------
//
//
// This function can be called from a callback, registered by a slider
// or a text input, when specific events ('slide' or 'change') are
// triggered.
//
// The 'paramName' is the name of the parameter in 'parameters' object
// whose value must be updated to the new value of 'paramValue'.
//
// Before we update the value, we must check that:
//
// 1.) the parameter named as 'paramName' actually exists in the
// 'parameters' object;
// 2.) the value 'paramValue' is a valid floating-point number, and
// it lies within the range specified by the 'min' and 'max'
// properties of the stored parameter object.
//
// If 'paramName' and 'paramValue' turn out to be valid, we will update
// the stored value in the parameter with the new value, and also
// update all of the text inputs and the slider that correspond to this
// parameter (if any), so that they reflect the new parameter's value.
// Finally, the helper array 'allParameterValues' will also be updated
// to reflect the change.
//
// If something went wrong (for example the new value is outside the
// allowed range), then we will reset the 'element' to display the
// original value.
//
// ####################################################################
function setParameterValue(paramName, paramValue, element, slider, updateOnEvent) {
var paramValueNum, c1;
// If a parameter with the name specified by the 'paramName'
// parameter is not tracked by state, do not do anything.
if (parameters.hasOwnProperty(paramName) === false) {
console.log('ERROR: Object parameters does not have a property named "' + paramName + '".');
return;
}
// Try to convert the passed value to a valid floating-point
// number.
paramValueNum = parseFloat(paramValue);
// We are interested only in valid float values. NaN, -INF,
// +INF we will disregard.
if (isFinite(paramValueNum) === false) {
console.log('ERROR: New parameter value is not a floating-point number.');
console.log('paramValue = ', paramValue);
return;
}
if (paramValueNum < parameters[paramName].min) {
paramValueNum = parameters[paramName].min;
} else if (paramValueNum > parameters[paramName].max) {
paramValueNum = parameters[paramName].max;
}
parameters[paramName].value = paramValueNum;
// Update all text inputs with the new parameter's value.
for (c1 = 0; c1 < parameters[paramName].inputDivs.length; c1 += 1) {
parameters[paramName].inputDivs[c1].val(paramValueNum);
}
// Update the single slider with the new parameter's value.
if ((slider === false) && (parameters[paramName].sliderDiv !== null)) {
parameters[paramName].sliderDiv.slider('value', paramValueNum);
}
// Update the helper array with the new parameter's value.
allParameterValues[parameters[paramName].helperArrayIndex] = paramValueNum;
for (c1 = 0; c1 < dynamicEl.length; c1++) {
if (
((updateOnEvent !== undefined) && (dynamicEl[c1].updateOnEvent === updateOnEvent)) ||
(updateOnEvent === undefined)
) {
// If we have a DOM element, call the function "paste" the answer into the DIV.
if (dynamicEl[c1].el !== null) {
dynamicEl[c1].el.html(dynamicEl[c1].func.apply(window, allParameterValues));
}
// If we DO NOT have an element, simply call the function. The function can then
// manipulate all the DOM elements it wants, without the fear of them being overwritten
// by us afterwards.
else {
dynamicEl[c1].func.apply(window, allParameterValues);
}
}
}
// If we have a plot DIV to work with, tell to update.
if (plotDiv !== undefined) {
plotDiv.trigger('update_plot');
}
return true;
} // End-of: function setParameterValue
// ####################################################################
//
// Function: processParameter(obj)
// -------------------------------
//
//
// This function will be run once for each instance of a GST when
// parsing the JSON config object.
//
// 'newParamObj' must be empty from the start for each invocation of
// this function, that's why we will declare it locally.
//
// We will parse the passed object 'obj' and populate the 'newParamObj'
// object with required properties.
//
// Since there will be many properties that are of type floating-point
// number, we will have a separate function for parsing them.
//
// processParameter() will fail right away if 'obj' does not have a
// '@var' property which represents the name of the parameter we want
// to process.
//
// If, after all of the properties have been processed, we reached the
// end of the function successfully, the 'newParamObj' will be added to
// the 'parameters' object (that is defined in the scope of State()
// function) as a property named as the name of the parameter.
//
// If at least one of the properties from 'obj' does not get correctly
// parsed, then the parameter represented by 'obj' will be disregarded.
// It will not be available to user-defined plotting functions, and
// things will most likely break. We will notify the user about this.
//
// ####################################################################
function processParameter(obj) {
var paramName, newParamObj;
if (typeof obj['@var'] !== 'string') {
console.log('ERROR: Expected obj["@var"] to be a string. It is not.');
console.log('obj["@var"] = ', obj['@var']);
return;
}
paramName = obj['@var'];
newParamObj = {};
if (
(processFloat('@min', 'min') === false) ||
(processFloat('@max', 'max') === false) ||
(processFloat('@step', 'step') === false) ||
(processFloat('@initial', 'value') === false)
) {
console.log('ERROR: A required property is missing. Not creating parameter "' + paramName + '"');
return;
}
// Pointers to text input and slider DIV elements that this
// parameter will be attached to. Initially there are none. When we
// will create text inputs and sliders, we will update these
// properties.
newParamObj.inputDivs = [];
newParamObj.sliderDiv = null;
// Everything went well, so save the new parameter object.
parameters[paramName] = newParamObj;
return;
function processFloat(attrName, newAttrName) {
var attrValue;
if (typeof obj[attrName] !== 'string') {
console.log('ERROR: Expected obj["' + attrName + '"] to be a string. It is not.');
console.log('obj["' + attrName + '"] = ', obj[attrName]);
return false;
} else {
attrValue = parseFloat(obj[attrName]);
if (isFinite(attrValue) === false) {
console.log('ERROR: Expected obj["' + attrName + '"] to be a valid floating-point number. It is not.');
console.log('obj["' + attrName + '"] = ', obj[attrName]);
return false;
}
}
newParamObj[newAttrName] = attrValue;
return true;
} // End-of: function processFloat
} // End-of: function processParameter
// ####################################################################
//
// Function: generateHelperArrays()
// -------------------------------
//
//
// Populate 'allParameterNames' and 'allParameterValues' with data.
// Link each parameter object with the corresponding helper array via
// an index 'helperArrayIndex'. It will be the same for both of the
// arrays.
//
// NOTE: It is important to remember to update these helper arrays
// whenever a new parameter is added (or one is removed), or when a
// parameter's value changes.
//
// ####################################################################
function generateHelperArrays() {
var paramName, c1;
c1 = 0;
for (paramName in parameters) {
allParameterNames.push(paramName);
allParameterValues.push(parameters[paramName].value);
parameters[paramName].helperArrayIndex = c1;
c1 += 1;
}
}
} // End-of: function State
});
// End of wrapper for RequireJS. As you can see, we are passing
// namespaced Require JS variables to an anonymous function. Within
// it, you can use the standard requirejs(), require(), and define()
// functions as if they were in the global namespace.
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)

View File

@@ -82,12 +82,13 @@ class @HTMLEditingDescriptor
image_advtab: true,
# We may want to add "styleselect" when we collect all styles used throughout the LMS
toolbar: "formatselect | fontselect | bold italic underline forecolor wrapAsCode | bullist numlist outdent indent blockquote | link unlink image | code",
block_formats: interpolate("%(paragraph)s=p;%(preformatted)s=pre;%(heading1)s=h1;%(heading2)s=h2;%(heading3)s=h3", {
block_formats: interpolate("%(paragraph)s=p;%(preformatted)s=pre;%(heading3)s=h3;%(heading4)s=h4;%(heading5)s=h5;%(heading6)s=h6", {
paragraph: gettext("Paragraph"),
preformatted: gettext("Preformatted"),
heading1: gettext("Heading 1"),
heading2: gettext("Heading 2"),
heading3: gettext("Heading 3")
heading3: gettext("Heading 3"),
heading4: gettext("Heading 4"),
heading5: gettext("Heading 5"),
heading6: gettext("Heading 6")
}, true),
width: '100%',
height: '400px',

View File

@@ -18,6 +18,8 @@ class @Sequence
bind: ->
@$('#sequence-list a').click @goto
@el.on 'bookmark:add', @addBookmarkIconToActiveNavItem
@el.on 'bookmark:remove', @removeBookmarkIconFromActiveNavItem
initProgress: ->
@progressTable = {} # "#problem_#{id}" -> progress
@@ -102,8 +104,9 @@ class @Sequence
@mark_active new_position
current_tab = @contents.eq(new_position - 1)
@content_container.html(current_tab.text()).attr("aria-labelledby", current_tab.attr("aria-labelledby"))
bookmarked = if @el.find('.active .bookmark-icon').hasClass('bookmarked') then true else false
@content_container.html(current_tab.text()).attr("aria-labelledby", current_tab.attr("aria-labelledby")).data('bookmarked', bookmarked)
XBlock.initializeBlocks(@content_container, @requestToken)
window.update_schematics() # For embedded circuit simulator exercises in 6.002x
@@ -116,6 +119,8 @@ class @Sequence
sequence_links = @content_container.find('a.seqnav')
sequence_links.click @goto
@el.find('.path').html(@el.find('.nav-item.active').data('path'))
@sr_container.focus();
# @$("a.active").blur()
@@ -180,3 +185,13 @@ class @Sequence
element.removeClass("inactive")
.removeClass("visited")
.addClass("active")
addBookmarkIconToActiveNavItem: (event) =>
event.preventDefault()
@el.find('.nav-item.active .bookmark-icon').removeClass('is-hidden').addClass('bookmarked')
@el.find('.nav-item.active .bookmark-icon-sr').text(gettext('Bookmarked'))
removeBookmarkIconFromActiveNavItem: (event) =>
event.preventDefault()
@el.find('.nav-item.active .bookmark-icon').removeClass('bookmarked').addClass('is-hidden')
@el.find('.nav-item.active .bookmark-icon-sr').text('')

View File

@@ -139,7 +139,8 @@ function (HTML5Video, Resizer) {
rel: 0,
showinfo: 0,
enablejsapi: 1,
modestbranding: 1
modestbranding: 1,
cc_load_policy: 0
};
if (!state.isFlashMode()) {

View File

@@ -1,11 +1,12 @@
(function (define) {
// VideoCaption module.
// VideoCaption module.
'use strict';
define(
'video/09_video_caption.js',
['video/00_sjson.js', 'video/00_async_process.js'],
function (Sjson, AsyncProcess) {
/**
* @desc VideoCaption module exports a function.
*
@@ -29,11 +30,14 @@
'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption',
'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy',
'handleKeypress', 'handleKeypressLink', 'openLanguageMenu', 'closeLanguageMenu',
'previousLanguageMenuItem', 'nextLanguageMenuItem'
'previousLanguageMenuItem', 'nextLanguageMenuItem', 'handleCaptionToggle',
'showClosedCaptions', 'hideClosedCaptions', 'toggleClosedCaptions',
'updateCaptioningCookie', 'handleCaptioningCookie', 'handleTranscriptToggle'
);
this.state = state;
this.state.videoCaption = this;
this.renderElements();
this.handleCaptioningCookie();
return $.Deferred().resolve().promise();
};
@@ -41,6 +45,14 @@
VideoCaption.prototype = {
langTemplate: [
'<div class="grouped-controls">',
'<button class="control toggle-captions" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-cc" aria-hidden="true"></span>',
'<span class="sr control-text">',
gettext('Turn on closed captioning'),
'</span>',
'</span>',
'</button>',
'<button class="control toggle-transcript" aria-disabled="false">',
'<span class="icon-fallback-img">',
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
@@ -66,12 +78,13 @@
].join(''),
template: [
'<ol id="transcript-captions" class="subtitles" aria-label="',
'<div class="subtitles" role="region" aria-label="',
/* jshint maxlen:200 */
gettext('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.'),
'">',
'<li></li>',
'</ol>'
'<ol id="transcript-captions" class="subtitles-menu">',
'</ol>',
'</div>'
].join(''),
destroy: function () {
@@ -106,7 +119,10 @@
this.loaded = false;
this.subtitlesEl = $(this.template);
this.subtitlesMenuEl = this.subtitlesEl.find('.subtitles-menu');
this.container = $(this.langTemplate);
this.captionControlEl = this.container.find('.toggle-captions');
this.captionDisplayEl = this.state.el.find('.closed-captions');
this.transcriptControlEl = this.container.find('.toggle-transcript');
this.languageChooserEl = this.container.find('.lang');
this.menuChooserEl = this.languageChooserEl.parent();
@@ -129,16 +145,26 @@
'keydown'
].join(' ');
this.transcriptControlEl.on('click', this.toggle);
this.subtitlesEl
.on({
mouseenter: this.onMouseEnter,
mouseleave: this.onMouseLeave,
mousemove: this.onMovement,
mousewheel: this.onMovement,
DOMMouseScroll: this.onMovement
})
.on(events, 'li[data-index]', this.onCaptionHandler);
this.captionControlEl.on({
click: this.toggleClosedCaptions,
keydown: this.handleCaptionToggle
});
this.transcriptControlEl.on({
click: this.toggle,
keydown: this.handleTranscriptToggle
});
this.subtitlesMenuEl.on({
mouseenter: this.onMouseEnter,
mouseleave: this.onMouseLeave,
mousemove: this.onMovement,
mousewheel: this.onMovement,
DOMMouseScroll: this.onMovement
})
.on(events, 'li[data-index]', this.onCaptionHandler);
this.container.on({
mouseenter: this.onContainerMouseEnter,
mouseleave: this.onContainerMouseLeave
});
if (this.showLanguageMenu) {
this.languageChooserEl.on({
@@ -148,11 +174,6 @@
this.languageChooserEl.on({
keydown: this.handleKeypressLink
}, '.control-lang');
this.container.on({
mouseenter: this.onContainerMouseEnter,
mouseleave: this.onContainerMouseLeave
});
}
state.el
@@ -168,7 +189,7 @@
});
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
this.subtitlesEl.on('scroll', state.videoControl.showControls);
this.subtitlesMenuEl.on('scroll', state.videoControl.showControls);
}
},
@@ -176,6 +197,30 @@
this.updatePlayTime(time);
},
handleCaptionToggle: function(event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode;
switch(keyCode) {
case KEY.SPACE:
case KEY.ENTER:
event.preventDefault();
this.toggleClosedCaptions(event);
}
},
handleTranscriptToggle: function(event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode;
switch(keyCode) {
case KEY.SPACE:
case KEY.ENTER:
event.preventDefault();
this.toggle(event);
}
},
handleKeypressLink: function(event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode,
@@ -188,7 +233,7 @@
index = this.languageChooserEl.find('li').index(focused);
total = this.languageChooserEl.find('li').size() - 1;
this.previousLanguageMenuItem(event, index, total);
this.previousLanguageMenuItem(event, index);
break;
case KEY.DOWN:
@@ -241,13 +286,13 @@
if (index === total) {
this.languageChooserEl
.find('.control-lang').first()
.focus();
.focus();
} else {
this.languageChooserEl
.find('li:eq(' + index + ')')
.next()
.find('.control-lang')
.focus();
.focus();
}
return false;
@@ -256,11 +301,7 @@
previousLanguageMenuItem: function(event, index) {
event.preventDefault();
if (event.altKey) {
return true;
}
if (event.shiftKey) {
if (event.altKey || event.shiftKey) {
return true;
}
@@ -287,11 +328,13 @@
this.state.el.trigger('language_menu:show');
button
.addClass('is-opened');
menu
.find('.control-lang').last()
.focus();
.focus();
},
closeLanguageMenu: function(event) {
@@ -300,6 +343,7 @@
var button = this.languageChooserEl;
this.state.el.trigger('language_menu:hide');
button
.removeClass('is-opened')
.find('.language-menu')
@@ -473,7 +517,7 @@
state.el.removeClass('is-captions-rendered');
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
// occurred, then we hide the captions panel, and the "Transcript" button
this.fetchXHR = $.ajaxWithPrefix({
url: url,
notifyOnError: false,
@@ -491,10 +535,10 @@
}
} else {
if (state.isTouch) {
self.subtitlesEl.find('li').html(
self.subtitlesEl.find('.subtitles-menu').html(
gettext(
'Caption will be displayed when ' +
'you start playing the video.'
'<li>Transcript will be displayed when ' +
'you start playing the video.</li>'
)
);
} else {
@@ -706,9 +750,9 @@
};
this.rendered = false;
this.subtitlesEl.empty();
this.subtitlesMenuEl.empty();
this.setSubtitlesHeight();
this.buildCaptions(this.subtitlesEl, start, captions).done(onRender);
this.buildCaptions(this.subtitlesMenuEl, start, captions).done(onRender);
},
/**
@@ -718,7 +762,7 @@
*/
addPaddings: function () {
this.subtitlesEl
this.subtitlesMenuEl
.prepend(
$('<li class="spacing">')
.height(this.topSpacingHeight())
@@ -936,6 +980,7 @@
.addClass('current');
this.currentIndex = newIndex;
this.captionDisplayEl.text(this.subtitlesEl.find("li[data-index='" + newIndex + "']").text());
this.scrollCaption();
}
}
@@ -1017,6 +1062,82 @@
}
},
handleCaptioningCookie: function() {
if ($.cookie('show_closed_captions') === 'true') {
this.state.showClosedCaptions = true;
this.showClosedCaptions();
// keep it going until turned off
$.cookie('show_closed_captions', 'true', {
expires: 3650,
path: '/'
});
} else {
this.hideClosedCaptions();
}
},
toggleClosedCaptions: function(event) {
event.preventDefault();
if (this.state.el.hasClass('has-captions')) {
this.state.showClosedCaptions = false;
this.updateCaptioningCookie(false);
this.hideClosedCaptions();
} else {
this.state.showClosedCaptions = true;
this.updateCaptioningCookie(true);
this.showClosedCaptions();
}
},
showClosedCaptions: function() {
this.state.el.addClass('has-captions');
this.captionDisplayEl
.show()
.addClass('is-visible');
this.captionControlEl
.addClass('is-active')
.find('.control-text')
.text(gettext('Hide closed captions'));
if (this.subtitlesEl.find('.current').text()) {
this.captionDisplayEl
.text(this.subtitlesEl.find('.current').text());
} else {
this.captionDisplayEl
.text(gettext('(Caption will be displayed when you start playing the video.)'));
}
},
hideClosedCaptions: function() {
this.state.el.removeClass('has-captions');
this.captionDisplayEl
.hide()
.removeClass('is-visible');
this.captionControlEl
.removeClass('is-active')
.find('.control-text')
.text(gettext('Turn on closed captioning'));
},
updateCaptioningCookie: function(method) {
if (method) {
$.cookie('show_closed_captions', 'true', {
expires: 3650,
path: '/'
});
} else {
$.cookie('show_closed_captions', null, {
path: '/'
});
}
},
/**
* @desc Shows/Hides captions and updates the cookie.
*

View File

@@ -316,6 +316,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
fragment.add_content(self.system.render_template('vert_module.html', {
'items': contents,
'xblock_context': context,
'show_bookmark_button': False,
}))
return fragment

View File

@@ -11,6 +11,13 @@ class ItemWriteConflictError(Exception):
pass
class MultipleCourseBlocksFound(Exception):
"""
Raise this exception when Iterating over the course blocks return multiple course blocks.
"""
pass
class InsufficientSpecificationError(Exception):
pass

View File

@@ -265,6 +265,26 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
store = self._get_modulestore_for_courselike(course_key)
return store.get_items(course_key, **kwargs)
@strip_key
def get_course_summaries(self, **kwargs):
"""
Returns a list containing the course information in CourseSummary objects.
Information contains `location`, `display_name`, `locator` of the courses in this modulestore.
"""
course_summaries = {}
for store in self.modulestores:
for course_summary in store.get_course_summaries(**kwargs):
course_id = self._clean_locator_for_mapping(locator=course_summary.id)
# Check if course is indeed unique. Save it in result if unique
if course_id in course_summaries:
log.warning(
u"Modulestore %s have duplicate courses %s; skipping from result.", store, course_id
)
else:
course_summaries[course_id] = course_summary
return course_summaries.values()
@strip_key
def get_courses(self, **kwargs):
'''

View File

@@ -39,6 +39,7 @@ from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceVa
from xblock.runtime import KvsFieldData
from xmodule.assetstore import AssetMetadata, CourseAssetsFromStorage
from xmodule.course_module import CourseSummary
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import null_error_tracker, exc_info_to_str
from xmodule.exceptions import HeartbeatFailure
@@ -268,8 +269,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
)
if parent_url:
parent = self._convert_reference_to_key(parent_url)
if not parent and category != 'course':
# try looking it up just-in-time (but not if we're working with a root node (course).
if not parent and category not in _DETACHED_CATEGORIES + ['course']:
# try looking it up just-in-time (but not if we're working with a detached block).
parent = self.modulestore.get_parent_location(
as_published(location),
ModuleStoreEnum.RevisionOption.published_only if location.revision is None
@@ -968,6 +969,40 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
not (category == 'course' and depth == 0)
return apply_cached_metadata
@autoretry_read()
def get_course_summaries(self, **kwargs):
"""
Returns a list of `CourseSummary`. This accepts an optional parameter of 'org' which
will apply an efficient filter to only get courses with the specified ORG
"""
def extract_course_summary(course):
"""
Extract course information from the course block for mongo.
"""
return {
field: course['metadata'][field]
for field in CourseSummary.course_info_fields
if field in course['metadata']
}
course_org_filter = kwargs.get('org')
query = {'_id.category': 'course'}
if course_org_filter:
query['_id.org'] = course_org_filter
course_records = self.collection.find(query, {'metadata': True})
courses_summaries = []
for course in course_records:
if not (course['_id']['org'] == 'edx' and course['_id']['course'] == 'templates'):
locator = SlashSeparatedCourseKey(course['_id']['org'], course['_id']['course'], course['_id']['name'])
course_summary = extract_course_summary(course)
courses_summaries.append(
CourseSummary(locator, **course_summary)
)
return courses_summaries
@autoretry_read()
def get_courses(self, **kwargs):
'''

View File

@@ -6,7 +6,7 @@ from .exceptions import (ItemNotFoundError, NoPathToItem)
LOGGER = getLogger(__name__)
def path_to_location(modulestore, usage_key):
def path_to_location(modulestore, usage_key, full_path=False):
'''
Try to find a course_id/chapter/section[/position] path to location in
modulestore. The courseware insists that the first level in the course is
@@ -15,6 +15,7 @@ def path_to_location(modulestore, usage_key):
Args:
modulestore: which store holds the relevant objects
usage_key: :class:`UsageKey` the id of the location to which to generate the path
full_path: :class:`Bool` if True, return the full path to location. Default is False.
Raises
ItemNotFoundError if the location doesn't exist.
@@ -81,6 +82,9 @@ def path_to_location(modulestore, usage_key):
if path is None:
raise NoPathToItem(usage_key)
if full_path:
return path
n = len(path)
course_id = path[0].course_key
# pull out the location names

Some files were not shown because too many files have changed in this diff Show More