Merge pull request #11133 from edx/rc/2016-01-05
Release Candidate rc/2016-01-05
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -49,6 +49,7 @@ class CourseMetadata(object):
|
||||
'is_proctored_enabled',
|
||||
'is_time_limited',
|
||||
'is_practice_exam',
|
||||
'exam_review_rules',
|
||||
'self_paced'
|
||||
]
|
||||
|
||||
|
||||
@@ -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 #####
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
32
cms/static/js/spec/views/login_studio_spec.js
Normal file
32
cms/static/js/spec/views/login_studio_spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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"));
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ define(
|
||||
file: file,
|
||||
date: moment().valueOf(),
|
||||
completed: completed || false
|
||||
}));
|
||||
}), {path: window.location.pathname});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
12
cms/templates/js/mock/login.underscore
Normal file
12
cms/templates/js/mock/login.underscore
Normal 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>
|
||||
10
cms/templates/js/settings-tab-section.underscore
Normal file
10
cms/templates/js/settings-tab-section.underscore
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
%>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,10 +13,6 @@ class CourseEnrollmentError(Exception):
|
||||
self.data = data
|
||||
|
||||
|
||||
class CourseNotFoundError(CourseEnrollmentError):
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFoundError(CourseEnrollmentError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
78
common/djangoapps/student/tests/test_activate_account.py
Normal file
78
common/djangoapps/student/tests/test_activate_account.py
Normal 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)
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
51
common/djangoapps/util/markup.py
Normal file
51
common/djangoapps/util/markup.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
68
common/djangoapps/util/tests/test_markup.py
Normal file
68
common/djangoapps/util/tests/test_markup.py
Normal 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"<hello>"),
|
||||
(u"It's cool", u"It's cool"),
|
||||
(u'"cool," she said.', u'"cool," she said.'),
|
||||
(u"Stop & Shop", u"Stop & Shop"),
|
||||
(u"<a>нтмℓ-єѕ¢αρє∂</a>", u"<a>нтмℓ-єѕ¢αρє∂</a>"),
|
||||
)
|
||||
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 & 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&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 & B & C")
|
||||
|
||||
def test_ungettext(self):
|
||||
for i in [1, 2]:
|
||||
out = ungettext("1 & {}", "2 & {}", i).format(HTML("<>"))
|
||||
self.assertEqual(out, "{} & <>".format(i))
|
||||
@@ -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
|
||||
|
||||
@@ -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>⁄<sub>2</sub>H<sup>+</sup>+<sup>3</sup>⁄<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>⁄<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>⁄<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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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('<', '<').replace('>', '>')
|
||||
)
|
||||
|
||||
|
||||
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('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
def number_for_course_location(location):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
!*.js
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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;}}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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',
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -139,7 +139,8 @@ function (HTML5Video, Resizer) {
|
||||
rel: 0,
|
||||
showinfo: 0,
|
||||
enablejsapi: 1,
|
||||
modestbranding: 1
|
||||
modestbranding: 1,
|
||||
cc_load_policy: 0
|
||||
};
|
||||
|
||||
if (!state.isFlashMode()) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
'''
|
||||
|
||||
@@ -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):
|
||||
'''
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user